From 4d9fbd73223ad5aa79053bc459c217380e28a1c4 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 19 Sep 2021 21:05:42 +0200 Subject: [PATCH] Feature/full delete (#765) * Started with a few repositories. * Get rid unnecessary allocations. * New repositories and cancellation token improvements. * First untested implementation. * Fix infrastructure tests. * Warnings fixed. * All tests green again. * New tests. * Better cancellation. * More tests. * Backup fix. * Markdown everything * A few more tests and fixes. * Fix app deletion flag. * Provide app to client creator. * Started with migration. * Temp. * Minor change to rename app deletion. * Toggle for deleter. * Fix build. --- backend/.editorconfig | 69 +++++ .../ApplicationInsightsPlugin.cs | 2 +- .../Squidex.Extensions/APM/Otlp/OtlpPlugin.cs | 2 +- .../StackdriverExceptionHandler.cs | 2 +- .../APM/Stackdriver/StackdriverPlugin.cs | 2 +- .../Actions/Algolia/AlgoliaActionHandler.cs | 3 +- .../AzureQueue/AzureQueueActionHandler.cs | 5 +- .../Actions/Comment/CommentActionHandler.cs | 3 +- .../CreateContentActionHandler.cs | 3 +- .../Discourse/DiscourseActionHandler.cs | 3 +- .../ElasticSearchActionHandler.cs | 3 +- .../Actions/Email/EmailActionHandler.cs | 3 +- .../Actions/Fastly/FastlyActionHandler.cs | 3 +- .../Actions/Kafka/KafkaActionHandler.cs | 5 +- .../Actions/Kafka/KafkaProducer.cs | 8 +- .../Actions/Medium/MediumActionHandler.cs | 3 +- .../Notification/NotificationActionHandler.cs | 3 +- .../Prerender/PrerenderActionHandler.cs | 3 +- .../Squidex.Extensions/Actions/RuleHelper.cs | 3 +- .../Actions/Script/ScriptActionHandler.cs | 3 +- .../Actions/SignalR/SignalRActionHandler.cs | 3 +- .../Actions/Slack/SlackActionHandler.cs | 3 +- .../Actions/Twitter/TweetActionHandler.cs | 3 +- .../Actions/Webhook/WebhookActionHandler.cs | 6 +- .../Assets/Azure/AzureMetadataSource.cs | 2 +- .../AssetStore/MemoryAssetStorePlugin.cs | 2 +- .../Squidex.Extensions.csproj | 4 + backend/i18n/frontend_en.json | 12 +- backend/i18n/frontend_it.json | 10 +- backend/i18n/frontend_nl.json | 10 +- backend/i18n/frontend_zh.json | 10 +- backend/i18n/source/frontend_en.json | 12 +- backend/i18n/source/frontend_it.json | 5 - backend/i18n/source/frontend_nl.json | 5 - backend/i18n/source/frontend_zh.json | 5 - backend/src/Migrations/MigrationPath.cs | 23 +- backend/src/Migrations/Migrations.csproj | 4 + .../src/Migrations/Migrations/ClearRules.cs | 3 +- .../src/Migrations/Migrations/ClearSchemas.cs | 3 +- .../Migrations/ConvertEventStore.cs | 3 +- .../Migrations/ConvertEventStoreAppId.cs | 3 +- .../Migrations/Migrations/CreateAssetSlugs.cs | 9 +- .../MongoDb/AddAppIdToEventStream.cs | 20 +- .../Migrations/MongoDb/ConvertDocumentIds.cs | 30 +- .../MongoDb/ConvertOldSnapshotStores.cs | 5 +- .../MongoDb/ConvertRuleEventsJson.cs | 3 +- .../MongoDb/DeleteContentCollections.cs | 3 +- .../Migrations/MongoDb/RenameAssetMetadata.cs | 3 +- .../MongoDb/RenameAssetSlugField.cs | 3 +- .../MongoDb/RestructureContentCollection.cs | 7 +- .../Migrations/PopulateGrainIndexes.cs | 189 ------------ .../src/Migrations/Migrations/RebuildApps.cs | 3 +- .../Migrations/RebuildAssetFolders.cs | 3 +- .../Migrations/Migrations/RebuildAssets.cs | 3 +- .../Migrations/Migrations/RebuildContents.cs | 3 +- .../src/Migrations/Migrations/RebuildRules.cs | 34 +++ .../Migrations/Migrations/RebuildSchemas.cs | 34 +++ .../Migrations/Migrations/RebuildSnapshots.cs | 3 +- .../Migrations/StartEventConsumers.cs | 3 +- .../Migrations/StopEventConsumers.cs | 3 +- .../src/Migrations/OldEvents/AppArchived.cs | 24 ++ .../Migrations/OldEvents/AppClientChanged.cs | 5 +- backend/src/Migrations/RebuildRunner.cs | 14 +- backend/src/Migrations/RebuilderExtensions.cs | 18 +- .../Contents/GeoJsonValue.cs | 2 +- .../Contents/Status.cs | 4 +- .../Partitioning.cs | 2 +- .../EnrichedEvents/EnrichedCommentEvent.cs | 2 +- .../Rules/Rule.cs | 2 +- .../Schemas/Schema.cs | 2 +- .../Squidex.Domain.Apps.Core.Model.csproj | 4 + .../GenerateEdmSchema/EdmSchemaExtensions.cs | 5 +- .../HandleRules/IRuleActionHandler.cs | 3 +- .../HandleRules/IRuleService.cs | 9 +- .../HandleRules/IRuleTriggerHandler.cs | 6 +- .../HandleRules/Result.cs | 3 +- .../HandleRules/RuleActionHandler.cs | 8 +- .../HandleRules/RuleEventFormatter.cs | 11 +- .../HandleRules/RuleService.cs | 8 +- .../Scripting/ContentWrapper/JsonMapper.cs | 3 +- .../Scripting/Extensions/HttpJintExtension.cs | 2 +- .../Scripting/IScriptEngine.cs | 6 +- .../Scripting/JintScriptEngine.cs | 4 +- .../Scripting/JintUser.cs | 2 +- .../Scripting/ScriptOptions.cs | 2 +- ...Squidex.Domain.Apps.Core.Operations.csproj | 4 + .../Extensions/DateTimeFluidExtension.cs | 3 +- .../Validators/PatternValidator.cs | 15 +- .../Apps/MongoAppEntity.cs | 46 +++ .../Apps/MongoAppRepository.cs | 86 ++++++ .../Assets/MongoAssetFolderRepository.cs | 2 +- ...ongoAssetFolderRepository_SnapshotStore.cs | 43 +-- .../Assets/MongoAssetRepository.cs | 5 +- .../MongoAssetRepository_SnapshotStore.cs | 43 +-- .../Contents/MongoContentCollection.cs | 44 +-- .../Contents/MongoContentRepository.cs | 15 +- .../MongoContentRepository_SnapshotStore.cs | 74 +++-- .../Contents/Operations/QueryByIds.cs | 2 +- .../Contents/Operations/QueryByQuery.cs | 9 +- .../Contents/Operations/QueryReferences.cs | 2 +- .../Contents/Operations/QueryScheduled.cs | 12 +- .../FullText/MongoTextIndex.cs | 84 +++--- .../FullText/MongoTextIndexerState.cs | 33 +- .../History/MongoHistoryEventRepository.cs | 25 +- .../Rules/MongoRuleEntity.cs | 36 +++ .../Rules/MongoRuleEventEntity.cs | 21 +- .../Rules/MongoRuleEventRepository.cs | 86 ++++-- .../Rules/MongoRuleRepository.cs | 58 ++++ .../Rules/MongoRuleStatisticsCollection.cs | 16 +- .../Schemas/MongoSchemaEntity.cs | 41 +++ .../Schemas/MongoSchemaRepository.cs | 73 +++++ .../Schemas/MongoSchemasHash.cs | 19 +- .../Schemas/MongoSchemasHashEntity.cs | 3 +- ...quidex.Domain.Apps.Entities.MongoDb.csproj | 4 + .../AppProvider.cs | 59 ++-- .../AppProviderExtensions.cs | 6 +- .../Apps/AppEventDeleter.cs | 31 ++ .../Apps/AppPermanentDeleter.cs | 114 +++++++ .../Apps/AppUISettings.cs | 25 +- .../Apps/AppUISettingsGrain.cs | 7 + .../Apps/AppUsageDeleter.cs | 29 ++ .../Apps/BackupApps.cs | 84 +++--- .../Commands/{ArchiveApp.cs => DeleteApp.cs} | 2 +- .../Apps/DefaultAppLogStore.cs | 17 +- .../Diagnostics/OrleansAppsHealthCheck.cs | 9 +- .../Apps/DomainObject/AppCommandMiddleware.cs | 4 +- .../DomainObject/AppDomainObject.State.cs | 6 +- .../Apps/DomainObject/AppDomainObject.cs | 14 +- .../Apps/IAppEntity.cs | 2 +- .../Apps/IAppImageStore.cs | 6 +- .../Apps/IAppLogStore.cs | 6 +- .../Apps/IAppUISettingsGrain.cs | 2 + .../Apps/Indexes/AppsByNameIndexGrain.cs | 27 -- .../Apps/Indexes/AppsByUserIndexGrain.cs | 27 -- .../Apps/Indexes/AppsCacheGrain.cs | 89 ++++++ .../Apps/Indexes/AppsIndex.cs | 222 +++++--------- .../Apps/Indexes/IAppsByUserIndexGrain.cs | 17 -- ...ByNameIndexGrain.cs => IAppsCacheGrain.cs} | 10 +- .../Apps/Indexes/IAppsIndex.cs | 28 +- .../Apps/Plans/ConfigAppPlansProvider.cs | 2 +- .../Plans/RestrictAppsCommandMiddleware.cs | 4 +- .../Apps/Repositories/IAppRepository.cs | 23 ++ .../Apps/RolePermissionsProvider.cs | 2 +- .../Assets/AssetChangedTriggerHandler.cs | 4 +- .../Assets/AssetPermanentDeleter.cs | 26 +- .../Assets/AssetTagsDeleter.cs | 30 ++ .../Assets/AssetUsageTracker.cs | 12 +- .../Assets/AssetsJintExtension.cs | 5 +- .../Assets/BackupAssets.cs | 65 ++-- .../Assets/DefaultAssetFileStore.cs | 95 +++--- .../DomainObject/AssetCommandMiddleware.cs | 2 +- .../DomainObject/AssetDomainObject.State.cs | 2 + .../AssetFolderDomainObject.State.cs | 2 + .../AssetsBulkUpdateCommandMiddleware.cs | 5 +- .../Assets/IAssetEnricher.cs | 6 +- .../Assets/IAssetFileStore.cs | 21 +- .../Assets/IAssetQueryService.cs | 24 +- .../Assets/ImageAssetMetadataSource.cs | 4 +- .../Assets/Queries/AssetLoader.cs | 2 +- .../Assets/Queries/AssetQueryService.cs | 3 +- .../Assets/RebuildFiles.cs | 6 +- .../Assets/RecursiveDeleter.cs | 11 +- .../Repositories/IAssetFolderRepository.cs | 9 +- .../Assets/Repositories/IAssetRepository.cs | 24 +- .../Assets/SvgAssetMetadataSource.cs | 8 +- .../Backup/BackupGrain.cs | 26 +- .../Backup/BackupReader.cs | 31 +- .../Backup/BackupService.cs | 52 ++-- .../Backup/BackupWriter.cs | 35 ++- .../Backup/CompatibilityExtensions.cs | 2 + .../Backup/DefaultBackupArchiveStore.cs | 11 +- .../Backup/IBackupArchiveStore.cs | 9 +- .../Backup/IBackupGrain.cs | 2 + .../Backup/IBackupHandler.cs | 13 +- .../Backup/IBackupReader.cs | 13 +- .../Backup/IBackupService.cs | 13 +- .../Backup/IBackupWriter.cs | 12 +- .../Backup/RestoreGrain.cs | 23 +- .../Backup/TempFolderBackupArchiveLocation.cs | 4 +- .../Backup/UserMapping.cs | 17 +- .../DomainObject/CommentsCommandMiddleware.cs | 2 +- .../DomainObject/Guards/GuardComments.cs | 5 +- .../Contents/BackupContents.cs | 30 +- .../Contents/ContentChangedTriggerHandler.cs | 4 +- .../Contents/ContentSchedulerGrain.cs | 82 +++-- .../Contents/ContentsSearchSource.cs | 9 +- .../Contents/Counter/CounterDeleter.cs | 32 ++ .../Contents/Counter/CounterGrain.cs | 7 + .../Contents/Counter/ICounterGrain.cs | 2 + .../DomainObject/ContentCommandMiddleware.cs | 2 +- .../DomainObject/ContentDomainObject.State.cs | 2 + .../ContentsBulkUpdateCommandMiddleware.cs | 7 +- .../DomainObject/Guards/SecurityExtensions.cs | 2 +- .../Contents/GraphQL/CachingGraphQLService.cs | 3 +- .../Contents/GraphQL/DefaultDocumentWriter.cs | 3 +- .../GraphQL/GraphQLExecutionContext.cs | 43 +-- .../GraphQL/Types/Assets/AssetActions.cs | 9 +- .../Types/Contents/ComponentGraphType.cs | 2 +- .../GraphQL/Types/Contents/ContentActions.cs | 18 +- .../GraphQL/Types/Contents/FieldVisitor.cs | 8 +- .../Types/Primitives/EntityResolvers.cs | 2 +- .../Contents/IContentQueryService.cs | 15 +- .../Contents/Queries/ContentEnricher.cs | 2 +- .../Contents/Queries/ContentLoader.cs | 2 +- .../Contents/Queries/ContentQueryService.cs | 25 +- .../Contents/Queries/IContentEnricher.cs | 6 +- .../Contents/Queries/IContentEnricherStep.cs | 6 +- .../Contents/Queries/QueryExecutionContext.cs | 33 +- .../Repositories/IContentRepository.cs | 27 +- .../Text/Elastic/ElasticSearchMapping.cs | 3 +- .../Text/Elastic/ElasticSearchTextIndex.cs | 21 +- .../Contents/Text/ITextIndex.cs | 13 +- .../Text/State/CachingTextIndexerState.cs | 16 +- .../Contents/Text/State/ITextIndexerState.cs | 10 +- .../Text/State/InMemoryTextIndexerState.cs | 10 +- .../Contents/Text/State/TextContentState.cs | 7 +- .../Contents/Text/TextIndexingProcess.cs | 1 + .../DomainObjectState.cs | 2 - .../History/HistoryService.cs | 6 +- .../History/IHistoryService.cs | 4 +- .../History/ParsedHistoryEvent.cs | 2 +- .../Repositories/IHistoryEventRepository.cs | 10 +- .../IAppProvider.cs | 28 +- .../Squidex.Domain.Apps.Entities/IDeleter.cs | 28 ++ .../Notifications/NotificationEmailSender.cs | 16 +- .../Rules/BackupRules.cs | 30 +- .../DomainObject/RuleDomainObject.State.cs | 2 + .../Rules/IRuleEnricher.cs | 7 +- .../Rules/IRuleQueryService.cs | 4 +- ...ByAppIndexGrain.cs => IRulesCacheGrain.cs} | 10 +- .../Rules/Indexes/IRulesIndex.cs | 8 +- .../Rules/Indexes/RulesByAppIndexGrain.cs | 27 -- .../Rules/Indexes/RulesCacheGrain.cs | 58 ++++ .../Rules/Indexes/RulesIndex.cs | 38 +-- .../Rules/Queries/RuleEnricher.cs | 11 +- .../Rules/Queries/RuleQueryService.cs | 8 +- .../Repositories/IRuleEventRepository.cs | 39 ++- .../Rules/Repositories/IRuleRepository.cs} | 19 +- .../Rules/RuleCommandMiddleware.cs | 2 +- .../Rules/Runner/DefaultRuleRunnerService.cs | 26 +- .../Rules/Runner/IRuleRunnerService.cs | 14 +- .../Rules/Runner/RuleRunnerGrain.cs | 22 +- .../Schemas/BackupSchemas.cs | 32 +- .../DomainObject/Guards/GuardHelper.cs | 7 +- .../DomainObject/SchemaDomainObject.State.cs | 2 + .../Schemas/ISchemasHash.cs | 7 +- ...AppIndexGrain.cs => ISchemasCacheGrain.cs} | 12 +- .../Schemas/Indexes/ISchemasIndex.cs | 14 +- .../Schemas/Indexes/SchemasByAppIndexGrain.cs | 27 -- .../Schemas/Indexes/SchemasCacheGrain.cs | 84 ++++++ .../Schemas/Indexes/SchemasIndex.cs | 145 ++++----- .../Repositories/ISchemaRepository.cs} | 10 +- .../Schemas/SchemasSearchSource.cs | 5 +- .../Search/ISearchManager.cs | 3 +- .../Search/ISearchSource.cs | 3 +- .../Squidex.Domain.Apps.Entities.csproj | 4 + .../Apps/{AppArchived.cs => AppDeleted.cs} | 4 +- .../Rules/RuleUpdated.cs | 6 +- .../Squidex.Domain.Apps.Events.csproj | 4 + .../MongoRoleStore.cs | 30 +- .../MongoUserStore.cs | 171 +++++++---- .../Squidex.Domain.Users.MongoDb.csproj | 4 + .../Squidex.Domain.Users/DefaultKeyStore.cs | 2 +- .../DefaultUserPictureStore.cs | 6 +- .../DefaultUserResolver.cs | 42 +-- .../DefaultUserService.cs | 57 ++-- .../DefaultXmlRepository.cs | 17 +- .../Squidex.Domain.Users/IUserPictureStore.cs | 6 +- .../src/Squidex.Domain.Users/IUserService.cs | 55 ++-- .../InMemory/InMemoryApplicationStore.cs | 108 ++++--- .../InMemory/InMemoryScopeStore.cs | 84 ++++-- .../Squidex.Domain.Users.csproj | 4 + .../Diagnostics/CosmosDbHealthCheck.cs | 3 +- .../EventSourcing/CosmosDbEventStore.cs | 3 +- .../EventSourcing/CosmosDbSubscription.cs | 3 +- .../EventSourcing/FilterExtensions.cs | 3 +- .../Diagnostics/GetEventStoreHealthCheck.cs | 3 +- .../EventSourcing/GetEventStore.cs | 3 +- .../Diagnostics/MongoDBHealthCheck.cs | 3 +- .../EventSourcing/FilterExtensions.cs | 5 +- .../EventSourcing/MongoEventStore.cs | 6 +- .../MongoEventStoreSubscription.cs | 2 +- .../EventSourcing/MongoEventStore_Reader.cs | 67 ++--- .../EventSourcing/MongoEventStore_Writer.cs | 38 ++- .../EventSourcing/StreamPosition.cs | 11 +- .../Log/MongoRequest.cs | 10 + .../Log/MongoRequestLogRepository.cs | 28 +- .../Migrations/MongoMigrationStatus.cs | 26 +- .../MongoDb/BsonHelper.cs | 6 +- .../MongoDb/MongoExtensions.cs | 87 ++---- .../MongoDb/MongoRepositoryBase.cs | 10 +- .../Squidex.Infrastructure.MongoDb.csproj | 4 + .../States/MongoSnapshotStore.cs | 96 +----- .../States/MongoSnapshotStoreBase.cs | 131 ++++++++ .../States/MongoState.cs | 10 +- .../UsageTracking/MongoUsageRepository.cs | 27 +- .../CQRS/Events/RabbitMqEventConsumer.cs | 3 +- .../Squidex.Infrastructure.RabbitMq.csproj | 4 + .../RedisPubSub.cs | 3 +- .../Commands/InMemoryCommandBus.cs | 6 +- .../src/Squidex.Infrastructure/Commands/Is.cs | 3 +- .../Commands/Rebuilder.cs | 52 ++-- .../Commands/SnapshotList.cs | 2 +- .../Diagnostics/GCHealthCheck.cs | 3 +- .../Diagnostics/OrleansHealthCheck.cs | 3 +- .../src/Squidex.Infrastructure/DomainId.cs | 4 +- .../Email/IEmailSender.cs | 3 +- .../Email/SmtpEmailSender.cs | 6 +- .../EventConsumersHealthCheck.cs | 3 +- .../EventSourcing/Grains/BatchSubscriber.cs | 59 ++-- .../EventSourcing/IEventStore.cs | 34 ++- .../EventSourcing/RetrySubscription.cs | 5 +- .../Squidex.Infrastructure/GravatarHelper.cs | 3 +- .../Newtonsoft/NewtonsoftJsonSerializer.cs | 2 +- .../Json/Objects/JsonValue.cs | 2 +- .../src/Squidex.Infrastructure/Language.cs | 4 +- .../LanguagesInitializer.cs | 3 +- .../Log/BackgroundRequestLogStore.cs | 49 ++- .../Log/IRequestLogRepository.cs | 9 +- .../Log/IRequestLogStore.cs | 10 +- .../Migrations/IMigration.cs | 3 +- .../Migrations/IMigrationStatus.cs | 13 +- .../Migrations/Migrator.cs | 13 +- .../src/Squidex.Infrastructure/NamedId{T}.cs | 2 +- .../Orleans/GrainBootstrap.cs | 3 +- .../Orleans/GrainState.cs | 3 +- .../{IdsIndexState.cs => IUniqueNameGrain.cs} | 9 +- .../Orleans/Indexes/IUniqueNameIndexGrain.cs | 35 --- .../Orleans/Indexes/IdsIndexGrain.cs | 60 ---- .../Orleans/Indexes/UniqueNameGrain.cs | 45 +++ .../Orleans/Indexes/UniqueNameIndexGrain.cs | 135 --------- .../Queries/ClrValue.cs | 2 +- .../Queries/FilterNodeVisitor.cs | 2 +- .../Queries/OData/EdmModelExtensions.cs | 4 +- .../Squidex.Infrastructure/Queries/Query.cs | 3 +- .../src/Squidex.Infrastructure/RandomHash.cs | 8 +- .../src/Squidex.Infrastructure/RefToken.cs | 2 +- .../Reflection/SimpleMapper.cs | 2 +- .../Security/Permission.cs | 4 +- .../Squidex.Infrastructure.csproj | 8 +- .../States/BatchPersistence.cs | 2 +- .../States/ISnapshotStore.cs | 19 +- .../States/Persistence.cs | 2 +- .../StringExtensions.cs | 4 +- .../Tasks/AsyncHelper.cs | 6 +- .../Tasks/TaskExtensions.cs | 5 +- .../Translations/ResourcesLocalizer.cs | 8 +- .../UsageTracking/ApiUsageTracker.cs | 31 +- .../UsageTracking/BackgroundUsageTracker.cs | 42 ++- .../UsageTracking/CachingUsageTracker.cs | 29 +- .../UsageTracking/IApiUsageTracker.cs | 16 +- .../UsageTracking/IUsageRepository.cs | 13 +- .../UsageTracking/IUsageTracker.cs | 16 +- .../Validation/AbsoluteUrlAttribute.cs | 3 +- .../Identity/SquidexClaimsExtensions.cs | 4 +- backend/src/Squidex.Shared/Permissions.cs | 5 +- .../src/Squidex.Shared/Squidex.Shared.csproj | 4 + .../src/Squidex.Shared/Users/ClientUser.cs | 3 +- .../src/Squidex.Shared/Users/IUserResolver.cs | 22 +- .../src/Squidex.Web/ApiExceptionConverter.cs | 4 +- .../ETagCommandMiddleware.cs | 2 +- .../EnrichWithContentIdCommandMiddleware.cs | 3 +- backend/src/Squidex.Web/ETagExtensions.cs | 5 +- backend/src/Squidex.Web/FileCallbackResult.cs | 3 +- .../src/Squidex.Web/Pipeline/AppResolver.cs | 2 +- .../src/Squidex.Web/Pipeline/CachingFilter.cs | 2 + .../Squidex.Web/Pipeline/CachingManager.cs | 5 +- .../Pipeline/RequestExceptionMiddleware.cs | 3 +- ...eSiteCookiesServiceCollectionExtensions.cs | 14 +- .../Squidex.Web/Pipeline/UsageMiddleware.cs | 2 + .../Squidex.Web/Pipeline/UsagePipeWriter.cs | 3 +- .../Pipeline/UsageResponseBodyFeature.cs | 6 +- .../src/Squidex.Web/Pipeline/UsageStream.cs | 9 +- backend/src/Squidex.Web/Squidex.Web.csproj | 4 + .../Api/Config/OpenApi/ErrorDtoProcessor.cs | 4 +- .../Api/Config/OpenApi/SecurityProcessor.cs | 3 +- .../OpenApi/XmlResponseTypesProcessor.cs | 2 +- .../Api/Config/OpenApi/XmlTagProcessor.cs | 3 +- .../Api/Controllers/Apps/AppsController.cs | 65 ++-- .../Assets/AssetContentController.cs | 88 +++--- .../Controllers/Assets/AssetsController.cs | 3 +- .../Backups/BackupContentController.cs | 2 +- .../Comments/CommentsController.cs | 3 +- .../UserNotificationsController.cs | 6 +- .../Contents/ContentOpenApiController.cs | 4 +- .../Contents/Generator/OperationBuilder.cs | 3 +- .../Generator/SchemasOpenApiGenerator.cs | 6 +- .../Controllers/Contents/Models/ContentDto.cs | 2 +- .../Controllers/History/HistoryController.cs | 2 +- .../Api/Controllers/News/NewsController.cs | 2 +- .../News/Service/FeaturesService.cs | 6 +- .../Api/Controllers/Rules/RulesController.cs | 24 +- .../Controllers/Schemas/SchemasController.cs | 6 +- .../Statistics/UsagesController.cs | 2 +- .../Api/Controllers/Users/UsersController.cs | 12 +- .../Frontend/Middlewares/IndexExtensions.cs | 4 +- .../Frontend/Middlewares/IndexMiddleware.cs | 2 +- .../Frontend/Middlewares/NotifoMiddleware.cs | 7 +- .../Frontend/Middlewares/WebpackMiddleware.cs | 6 +- backend/src/Squidex/Areas/Frontend/Startup.cs | 2 +- .../Config/ApplicationManager.cs | 6 +- .../Config/CreateAdminInitializer.cs | 3 +- .../Config/DynamicApplicationStore.cs | 6 +- .../Config/TokenStoreInitializer.cs | 12 +- .../Controllers/Account/AccountController.cs | 2 +- .../Controllers/Connect/ConnectController.cs | 6 +- .../Controllers/Profile/ProfileController.cs | 2 +- .../src/Squidex/Config/Domain/AppsServices.cs | 23 +- .../Squidex/Config/Domain/AssetServices.cs | 33 +- .../Squidex/Config/Domain/BackupsServices.cs | 7 +- .../Squidex/Config/Domain/ContentsServices.cs | 9 +- .../Squidex/Config/Domain/LoggingServices.cs | 3 +- .../Config/Domain/MigrationServices.cs | 9 +- .../Config/Domain/SerializationInitializer.cs | 3 +- .../Squidex/Config/Domain/StoreServices.cs | 32 +- .../Config/Startup/LogConfigurationHost.cs | 6 +- .../Config/Startup/MigrationRebuilderHost.cs | 6 +- .../Squidex/Config/Startup/MigratorHost.cs | 6 +- .../Pipeline/Robots/RobotsTxtMiddleware.cs | 2 +- .../Squidex/Pipeline/Squid/SquidMiddleware.cs | 13 +- backend/src/Squidex/Squidex.csproj | 18 +- backend/src/Squidex/Startup.cs | 2 +- backend/src/Squidex/appsettings.json | 5 +- .../Model/Apps/RolesTests.cs | 5 +- .../HandleRules/EventEnricherTests.cs | 10 +- .../RuleEventFormatterCompareTests.cs | 4 +- .../HandleRules/RuleEventFormatterTests.cs | 8 +- .../Operations/Scripting/MockupHttpHandler.cs | 3 +- .../ValidateContent/AssetsFieldTests.cs | 2 +- .../ValidateContent/ReferencesFieldTests.cs | 2 +- .../Validators/PatternValidatorTests.cs | 2 +- .../Squidex.Domain.Apps.Core.Tests.csproj | 4 + .../TestHelpers/AExtensions.cs | 5 + .../AppProviderExtensionsTests.cs | 17 +- .../AppProviderTests.cs | 164 ++++++++++ .../Apps/AppEventDeleter.cs | 51 ++++ .../Apps/AppPermanentDeleterTests.cs | 138 +++++++++ .../Apps/AppUISettingsTests.cs | 23 ++ .../Apps/AppUsageDeleterTests.cs | 51 ++++ .../Apps/BackupAppsTests.cs | 137 ++++----- .../Apps/DefaultAppLogStoreTests.cs | 59 ++-- .../Apps/DomainObject/AppDomainObjectTests.cs | 14 +- .../Guards/GuardAppContributorsTests.cs | 24 +- .../Apps/DomainObject/Guards/GuardAppTests.cs | 2 +- .../Apps/Indexes/AppsCacheGrainTests.cs | 131 ++++++++ .../Apps/Indexes/AppsIndexIntegrationTests.cs | 265 ---------------- .../Apps/Indexes/AppsIndexTests.cs | 282 ++++-------------- .../InvitationEventConsumerTests.cs | 10 +- .../InviteUserCommandMiddlewareTests.cs | 13 +- .../RestrictAppsCommandMiddlewareTests.cs | 13 +- .../Apps/Plans/UsageGateTests.cs | 2 +- .../Apps/Plans/UsageNotifierGrainTests.cs | 4 +- .../Apps/RolePermissionsProviderTests.cs | 2 +- .../Assets/AssetPermanentDeleterTests.cs | 51 ++-- .../Assets/AssetTagsDeleterTests.cs | 51 ++++ .../Assets/AssetUsageTrackerTests.cs | 8 +- .../Assets/AssetsFluidExtensionTests.cs | 2 +- .../Assets/AssetsJintExtensionTests.cs | 2 +- .../Assets/BackupAssetsTests.cs | 78 +++-- .../Assets/DefaultAssetFileStoreTests.cs | 107 +++++-- .../AssetCommandMiddlewareTests.cs | 6 +- .../DomainObject/AssetDomainObjectTests.cs | 2 +- .../AssetFolderDomainObjectTests.cs | 2 +- .../Assets/MongoDb/AssetsQueryFixture.cs | 2 +- .../Backup/BackupCompatibilityTests.cs | 9 +- .../Backup/BackupReaderWriterTests.cs | 20 +- .../Backup/BackupServiceTests.cs | 15 + .../Backup/DefaultBackupArchiveStoreTests.cs | 16 +- .../Backup/UserMappingTests.cs | 23 +- .../Comments/CommentTriggerHandlerTests.cs | 7 +- .../CommentsCommandMiddlewareTests.cs | 9 +- .../DomainObject/CommentsGrainTests.cs | 4 +- .../Contents/BackupContentsTests.cs | 36 +-- .../Contents/ContentSchedulerGrainTests.cs | 129 ++++++++ .../Contents/ContentsSearchSourceTests.cs | 34 ++- .../Contents/Counter/CounterDeleterTests.cs | 51 ++++ .../DefaultWorkflowsValidatorTests.cs | 4 +- .../DomainObject/ContentDomainObjectTests.cs | 4 +- .../Contents/DynamicContentWorkflowTests.cs | 2 +- .../Contents/GraphQL/GraphQLTestBase.cs | 4 +- .../Contents/MongoDb/ContentsQueryFixture.cs | 5 +- .../MongoDb/ContentsQueryIntegrationTests.cs | 2 +- .../Contents/Queries/ContentEnricherTests.cs | 34 ++- .../Queries/ContentQueryParserTests.cs | 18 +- .../Queries/ContentQueryServiceTests.cs | 58 ++-- .../Contents/ReferencesFluidExtensionTests.cs | 2 +- .../Contents/ReferencesJintExtensionTests.cs | 2 +- .../Text/CachingTextIndexerStateTests.cs | 46 +-- .../Contents/Text/TextIndexerTests_Elastic.cs | 2 +- .../Contents/Text/TextIndexerTests_Mongo.cs | 2 +- .../Rules/BackupRulesTests.cs | 55 ++-- .../DomainObject/Guards/GuardRuleTests.cs | 2 +- .../Triggers/ContentChangedTriggerTests.cs | 6 +- .../RuleCommandMiddlewareTests.cs | 6 +- .../Rules/Indexes/RulesCacheGrainTests.cs | 102 +++++++ .../Rules/Indexes/RulesIndexTests.cs | 27 +- .../Rules/Queries/RuleEnricherTests.cs | 10 +- .../Rules/Queries/RuleQueryServiceTests.cs | 11 +- .../Rules/RuleDequeuerGrainTests.cs | 12 +- .../Rules/RuleEnqueuerTests.cs | 13 +- .../Schemas/BackupSchemasTests.cs | 58 ++-- .../Schemas/Indexes/SchemasCacheGrainTests.cs | 102 +++++++ .../Indexes/SchemasIndexIntegrationTests.cs | 189 ------------ .../Schemas/Indexes/SchemasIndexTests.cs | 102 +++---- .../Schemas/MongoDb/SchemasHashFixture.cs | 2 +- .../Schemas/SchemasSearchSourceTests.cs | 8 +- .../DefaultKeyStoreTests.cs | 14 +- .../DefaultUserResolverTests.cs | 54 ++-- .../DefaultUserServiceTests.cs | 3 +- .../DefaultXmlRepositoryTests.cs | 21 +- .../Squidex.Domain.Users.Tests.csproj | 4 + .../Commands/DomainObjectTests.cs | 5 +- .../EventSourcing/EventStoreTests.cs | 25 +- .../EventSourcing/MongoEventStoreFixture.cs | 2 +- .../Newtonsoft/ReadOnlyCollectionTests.cs | 5 +- .../LanguagesInitializerTests.cs | 6 +- .../Log/BackgroundRequestLogStoreTests.cs | 77 ++++- .../Migrations/MigratorTests.cs | 80 ++--- .../MongoDb/MongoQueryTests.cs | 2 +- .../Orleans/ActivationLimiterTests.cs | 2 +- .../Orleans/BootstrapTests.cs | 8 +- .../Orleans/ExceptionWrapperFilterTests.cs | 8 +- .../Orleans/Indexes/IdsIndexGrainTests.cs | 102 ------- .../Orleans/Indexes/UniqueNameGrainTests.cs | 63 ++++ .../Indexes/UniqueNameIndexGrainTests.cs | 197 ------------ .../Orleans/JsonExternalSerializerTests.cs | 4 +- .../Queries/QueryFromJsonTests.cs | 16 +- .../Reflection/ReflectionExtensionTests.cs | 2 +- .../Squidex.Infrastructure.Tests.csproj | 4 + .../States/PersistenceBatchTests.cs | 24 +- .../States/PersistenceEventSourcingTests.cs | 58 ++-- .../States/PersistenceSnapshotTests.cs | 31 +- .../UsageTracking/ApiUsageTrackerTests.cs | 34 ++- .../BackgroundUsageTrackerTests.cs | 75 +++-- .../UsageTracking/CachingUsageTrackerTests.cs | 44 ++- .../ApiExceptionFilterAttributeTests.cs | 2 + .../Pipeline/AppResolverTests.cs | 22 +- .../Pipeline/CachingKeysMiddlewareTests.cs | 4 +- .../Pipeline/SchemaResolverTests.cs | 18 +- .../Pipeline/UsageMiddlewareTests.cs | 27 +- .../Squidex.Web.Tests.csproj | 4 + .../pages/more/more-page.component.html | 14 +- .../pages/more/more-page.component.ts | 2 +- .../app/shared/services/apps.service.spec.ts | 2 +- 544 files changed, 6609 insertions(+), 4485 deletions(-) delete mode 100644 backend/src/Migrations/Migrations/PopulateGrainIndexes.cs create mode 100644 backend/src/Migrations/Migrations/RebuildRules.cs create mode 100644 backend/src/Migrations/Migrations/RebuildSchemas.cs create mode 100644 backend/src/Migrations/OldEvents/AppArchived.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/AppEventDeleter.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/AppPermanentDeleter.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/AppUsageDeleter.cs rename backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/{ArchiveApp.cs => DeleteApp.cs} (89%) delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexGrain.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsCacheGrain.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByUserIndexGrain.cs rename backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/{IAppsByNameIndexGrain.cs => IAppsCacheGrain.cs} (63%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/AssetTagsDeleter.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterDeleter.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/IDeleter.cs rename backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/{IRulesByAppIndexGrain.cs => IRulesCacheGrain.cs} (64%) delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexGrain.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesCacheGrain.cs rename backend/src/{Squidex.Infrastructure/Orleans/Indexes/IIdsIndexGrain.cs => Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs} (60%) rename backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/{ISchemasByAppIndexGrain.cs => ISchemasCacheGrain.cs} (60%) delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexGrain.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasCacheGrain.cs rename backend/src/{Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexState.cs => Squidex.Domain.Apps.Entities/Schemas/Repositories/ISchemaRepository.cs} (58%) rename backend/src/Squidex.Domain.Apps.Events/Apps/{AppArchived.cs => AppDeleted.cs} (84%) create mode 100644 backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStoreBase.cs rename backend/src/Squidex.Infrastructure/Orleans/Indexes/{IdsIndexState.cs => IUniqueNameGrain.cs} (66%) delete mode 100644 backend/src/Squidex.Infrastructure/Orleans/Indexes/IUniqueNameIndexGrain.cs delete mode 100644 backend/src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexGrain.cs create mode 100644 backend/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameGrain.cs delete mode 100644 backend/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexGrain.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/AppProviderTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppEventDeleter.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppPermanentDeleterTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUsageDeleterTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsCacheGrainTests.cs delete mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexIntegrationTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetTagsDeleterTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentSchedulerGrainTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterDeleterTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesCacheGrainTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasCacheGrainTests.cs delete mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexIntegrationTests.cs delete mode 100644 backend/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/IdsIndexGrainTests.cs create mode 100644 backend/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/UniqueNameGrainTests.cs delete mode 100644 backend/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/UniqueNameIndexGrainTests.cs diff --git a/backend/.editorconfig b/backend/.editorconfig index 672edebf1..5bfe47788 100644 --- a/backend/.editorconfig +++ b/backend/.editorconfig @@ -34,6 +34,75 @@ dotnet_diagnostic.IDE0066.severity = none # IDE0090: Use 'new(...)' dotnet_diagnostic.IDE0090.severity = none +# MA0002: IEqualityComparer or IComparer is missing +dotnet_diagnostic.MA0002.severity = none + +# MA0003: Add argument name to improve readability +dotnet_diagnostic.MA0003.severity = none + +# MA0004: Use Task.ConfigureAwait(false) +dotnet_diagnostic.MA0004.severity = none + +# MA0006: Use String.Equals instead of equality operator +dotnet_diagnostic.MA0006.severity = none + +# MA0007: Add a comma after the last value +dotnet_diagnostic.MA0007.severity = none + +# MA0008: Add StructLayoutAttribute +dotnet_diagnostic.MA0008.severity = none + +# MA0009: Add regex evaluation timeout +dotnet_diagnostic.MA0009.severity = none + +# MA0016: Prefer return collection abstraction instead of implementation +dotnet_diagnostic.MA0016.severity = none + +# MA0018: Do not declare static members on generic types +dotnet_diagnostic.MA0018.severity = none + +# MA0025: Implement the functionality instead of throwing NotImplementedException +dotnet_diagnostic.MA0025.severity = none + +# MA0028: Optimize StringBuilder usage +dotnet_diagnostic.MA0028.severity = none + +# MA0029: Combine LINQ methods +dotnet_diagnostic.MA0029.severity = none + +# MA0031: Optimize Enumerable.Count() usage +dotnet_diagnostic.MA0031.severity = none + +# MA0036: Make class static +dotnet_diagnostic.MA0036.severity = none + +# MA0038: Make method static +dotnet_diagnostic.MA0038.severity = none + +# MA0039: Do not write your own certificate validation method +dotnet_diagnostic.MA0039.severity = none + +# MA0048: File name must match type name +dotnet_diagnostic.MA0048.severity = none + +# MA0049: Type name should not match containing namespace +dotnet_diagnostic.MA0049.severity = none + +# MA0051: Method is too long +dotnet_diagnostic.MA0051.severity = none + +# MA0069: Non-constant static fields should not be visible +dotnet_diagnostic.MA0069.severity = none + +# MA0071: Avoid using redundant else +dotnet_diagnostic.MA0071.severity = none + +# MA0076: Do not use implicit culture-sensitive ToString in interpolated strings +dotnet_diagnostic.MA0076.severity = none + +# MA0097: A class that implements IComparable or IComparable should override comparison operators +dotnet_diagnostic.MA0097.severity = none + # RECS0129: Removes 'internal' modifiers that are not required dotnet_diagnostic.RECS0129.severity = none diff --git a/backend/extensions/Squidex.Extensions/APM/ApplicationInsights/ApplicationInsightsPlugin.cs b/backend/extensions/Squidex.Extensions/APM/ApplicationInsights/ApplicationInsightsPlugin.cs index f654665ea..95d33d1c3 100644 --- a/backend/extensions/Squidex.Extensions/APM/ApplicationInsights/ApplicationInsightsPlugin.cs +++ b/backend/extensions/Squidex.Extensions/APM/ApplicationInsights/ApplicationInsightsPlugin.cs @@ -16,7 +16,7 @@ namespace Squidex.Extensions.APM.ApplicationInsights { public sealed class ApplicationInsightsPlugin : IPlugin { - private class Configurator : ITelemetryConfigurator + private sealed class Configurator : ITelemetryConfigurator { private readonly IConfiguration config; diff --git a/backend/extensions/Squidex.Extensions/APM/Otlp/OtlpPlugin.cs b/backend/extensions/Squidex.Extensions/APM/Otlp/OtlpPlugin.cs index 88113bc38..3f0ac304b 100644 --- a/backend/extensions/Squidex.Extensions/APM/Otlp/OtlpPlugin.cs +++ b/backend/extensions/Squidex.Extensions/APM/Otlp/OtlpPlugin.cs @@ -16,7 +16,7 @@ namespace Squidex.Extensions.APM.Datadog { public sealed class OtlpPlugin : IPlugin { - private class Configurator : ITelemetryConfigurator + private sealed class Configurator : ITelemetryConfigurator { private readonly IConfiguration config; diff --git a/backend/extensions/Squidex.Extensions/APM/Stackdriver/StackdriverExceptionHandler.cs b/backend/extensions/Squidex.Extensions/APM/Stackdriver/StackdriverExceptionHandler.cs index 83ab5c536..c62aa1021 100644 --- a/backend/extensions/Squidex.Extensions/APM/Stackdriver/StackdriverExceptionHandler.cs +++ b/backend/extensions/Squidex.Extensions/APM/Stackdriver/StackdriverExceptionHandler.cs @@ -30,7 +30,7 @@ namespace Squidex.Extensions.APM.Stackdriver public string GetHttpMethod() { - return httpContextAccessor.HttpContext?.Request?.Method?.ToString() ?? string.Empty; + return httpContextAccessor.HttpContext?.Request?.Method ?? string.Empty; } public string GetUri() diff --git a/backend/extensions/Squidex.Extensions/APM/Stackdriver/StackdriverPlugin.cs b/backend/extensions/Squidex.Extensions/APM/Stackdriver/StackdriverPlugin.cs index 9b462cf9b..388ce91ea 100644 --- a/backend/extensions/Squidex.Extensions/APM/Stackdriver/StackdriverPlugin.cs +++ b/backend/extensions/Squidex.Extensions/APM/Stackdriver/StackdriverPlugin.cs @@ -18,7 +18,7 @@ namespace Squidex.Extensions.APM.Stackdriver { public sealed class StackdriverPlugin : IPlugin { - private class Configurator : ITelemetryConfigurator + private sealed class Configurator : ITelemetryConfigurator { private readonly string projectId; diff --git a/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs index 8c05e4c0a..7dca8ea31 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs @@ -94,7 +94,8 @@ namespace Squidex.Extensions.Actions.Algolia return ("Ignore", new AlgoliaJob()); } - protected override async Task ExecuteJobAsync(AlgoliaJob job, CancellationToken ct = default) + protected override async Task ExecuteJobAsync(AlgoliaJob job, + CancellationToken ct = default) { if (string.IsNullOrWhiteSpace(job.AppId)) { diff --git a/backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs index 76154ef73..9f6cf1586 100644 --- a/backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs @@ -1,4 +1,4 @@ -// ========================================================================== +// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschraenkt) @@ -58,7 +58,8 @@ namespace Squidex.Extensions.Actions.AzureQueue return (ruleDescription, ruleJob); } - protected override async Task ExecuteJobAsync(AzureQueueJob job, CancellationToken ct = default) + protected override async Task ExecuteJobAsync(AzureQueueJob job, + CancellationToken ct = default) { var queue = await clients.GetClientAsync((job.QueueConnectionString, job.QueueName)); diff --git a/backend/extensions/Squidex.Extensions/Actions/Comment/CommentActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Comment/CommentActionHandler.cs index c0efcc7bc..d2c522eb1 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Comment/CommentActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Comment/CommentActionHandler.cs @@ -54,7 +54,8 @@ namespace Squidex.Extensions.Actions.Comment return ("Ignore", new CreateComment()); } - protected override async Task ExecuteJobAsync(CreateComment job, CancellationToken ct = default) + protected override async Task ExecuteJobAsync(CreateComment job, + CancellationToken ct = default) { if (job.CommentsId == default) { diff --git a/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentActionHandler.cs index 1a5996b8e..6c83957b6 100644 --- a/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentActionHandler.cs @@ -72,7 +72,8 @@ namespace Squidex.Extensions.Actions.CreateContent return (Description, ruleJob); } - protected override async Task ExecuteJobAsync(Command job, CancellationToken ct = default) + protected override async Task ExecuteJobAsync(Command job, + CancellationToken ct = default) { var command = job; diff --git a/backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs index 2f38070f8..90bca6540 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs @@ -65,7 +65,8 @@ namespace Squidex.Extensions.Actions.Discourse return (description, ruleJob); } - protected override async Task ExecuteJobAsync(DiscourseJob job, CancellationToken ct = default) + protected override async Task ExecuteJobAsync(DiscourseJob job, + CancellationToken ct = default) { using (var httpClient = httpClientFactory.CreateClient()) { diff --git a/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs index 9b7f167fa..f7606719f 100644 --- a/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs @@ -100,7 +100,8 @@ namespace Squidex.Extensions.Actions.ElasticSearch return ("Ignore", new ElasticSearchJob()); } - protected override async Task ExecuteJobAsync(ElasticSearchJob job, CancellationToken ct = default) + protected override async Task ExecuteJobAsync(ElasticSearchJob job, + CancellationToken ct = default) { if (string.IsNullOrWhiteSpace(job.ServerHost)) { diff --git a/backend/extensions/Squidex.Extensions/Actions/Email/EmailActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Email/EmailActionHandler.cs index 6574e8f71..c8cf69b09 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Email/EmailActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Email/EmailActionHandler.cs @@ -41,7 +41,8 @@ namespace Squidex.Extensions.Actions.Email return (description, ruleJob); } - protected override async Task ExecuteJobAsync(EmailJob job, CancellationToken ct = default) + protected override async Task ExecuteJobAsync(EmailJob job, + CancellationToken ct = default) { using (var smtpClient = new SmtpClient()) { diff --git a/backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs index a28425109..c779006d5 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs @@ -46,7 +46,8 @@ namespace Squidex.Extensions.Actions.Fastly return (Description, ruleJob); } - protected override async Task ExecuteJobAsync(FastlyJob job, CancellationToken ct = default) + protected override async Task ExecuteJobAsync(FastlyJob job, + CancellationToken ct = default) { using (var httpClient = httpClientFactory.CreateClient()) { diff --git a/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaActionHandler.cs index 53a9bb293..ba197ef69 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaActionHandler.cs @@ -74,7 +74,7 @@ namespace Squidex.Extensions.Actions.Kafka foreach (var line in lines) { - var indexEqual = line.IndexOf('='); + var indexEqual = line.IndexOf('=', StringComparison.Ordinal); if (indexEqual > 0 && indexEqual < line.Length - 1) { @@ -90,7 +90,8 @@ namespace Squidex.Extensions.Actions.Kafka return headersDictionary; } - protected override async Task ExecuteJobAsync(KafkaJob job, CancellationToken ct = default) + protected override async Task ExecuteJobAsync(KafkaJob job, + CancellationToken ct = default) { try { diff --git a/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducer.cs b/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducer.cs index b951a7937..b91407032 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducer.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducer.cs @@ -112,7 +112,8 @@ namespace Squidex.Extensions.Actions.Kafka .WriteProperty("reason", error.Reason)); } - public async Task SendAsync(KafkaJob job, CancellationToken ct) + public async Task SendAsync(KafkaJob job, + CancellationToken ct) { if (!string.IsNullOrWhiteSpace(job.Schema)) { @@ -130,7 +131,8 @@ namespace Squidex.Extensions.Actions.Kafka } } - private static async Task ProduceAsync(IProducer producer, Message message, KafkaJob job, CancellationToken ct) + private static async Task ProduceAsync(IProducer producer, Message message, KafkaJob job, + CancellationToken ct) { message.Key = job.MessageKey; @@ -146,7 +148,7 @@ namespace Squidex.Extensions.Actions.Kafka if (!string.IsNullOrWhiteSpace(job.PartitionKey) && job.PartitionCount > 0) { - var partition = Math.Abs(job.PartitionKey.GetHashCode()) % job.PartitionCount; + var partition = Math.Abs(job.PartitionKey.GetHashCode(StringComparison.Ordinal)) % job.PartitionCount; await producer.ProduceAsync(new TopicPartition(job.TopicName, partition), message, ct); } diff --git a/backend/extensions/Squidex.Extensions/Actions/Medium/MediumActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Medium/MediumActionHandler.cs index 1dd9b93ec..037865455 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Medium/MediumActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Medium/MediumActionHandler.cs @@ -79,7 +79,8 @@ namespace Squidex.Extensions.Actions.Medium } } - protected override async Task ExecuteJobAsync(MediumJob job, CancellationToken ct = default) + protected override async Task ExecuteJobAsync(MediumJob job, + CancellationToken ct = default) { using (var httpClient = httpClientFactory.CreateClient()) { diff --git a/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs index 890d58239..3f8bf7f39 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs @@ -72,7 +72,8 @@ namespace Squidex.Extensions.Actions.Notification return ("Ignore", new CreateComment()); } - protected override async Task ExecuteJobAsync(CreateComment job, CancellationToken ct = default) + protected override async Task ExecuteJobAsync(CreateComment job, + CancellationToken ct = default) { if (job.CommentsId == default) { diff --git a/backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderActionHandler.cs index 3e35e6a9c..fef0970b8 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderActionHandler.cs @@ -34,7 +34,8 @@ namespace Squidex.Extensions.Actions.Prerender return ($"Recache {url}", new PrerenderJob { RequestBody = requestBody }); } - protected override async Task ExecuteJobAsync(PrerenderJob job, CancellationToken ct = default) + protected override async Task ExecuteJobAsync(PrerenderJob job, + CancellationToken ct = default) { using (var httpClient = httpClientFactory.CreateClient()) { diff --git a/backend/extensions/Squidex.Extensions/Actions/RuleHelper.cs b/backend/extensions/Squidex.Extensions/Actions/RuleHelper.cs index 5a6921491..139c8b185 100644 --- a/backend/extensions/Squidex.Extensions/Actions/RuleHelper.cs +++ b/backend/extensions/Squidex.Extensions/Actions/RuleHelper.cs @@ -45,7 +45,8 @@ namespace Squidex.Extensions.Actions return @event is EnrichedAssetEvent { Type: EnrichedAssetEventType.Deleted }; } - public static async Task OneWayRequestAsync(this HttpClient client, HttpRequestMessage request, string requestBody = null, CancellationToken ct = default) + public static async Task OneWayRequestAsync(this HttpClient client, HttpRequestMessage request, string requestBody = null, + CancellationToken ct = default) { HttpResponseMessage response = null; try diff --git a/backend/extensions/Squidex.Extensions/Actions/Script/ScriptActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Script/ScriptActionHandler.cs index debe5d3d1..fe06e6fe3 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Script/ScriptActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Script/ScriptActionHandler.cs @@ -30,7 +30,8 @@ namespace Squidex.Extensions.Actions.Script return Task.FromResult(($"Run a script", job)); } - protected override async Task ExecuteJobAsync(ScriptJob job, CancellationToken ct = default) + protected override async Task ExecuteJobAsync(ScriptJob job, + CancellationToken ct = default) { var vars = new ScriptVars { diff --git a/backend/extensions/Squidex.Extensions/Actions/SignalR/SignalRActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/SignalR/SignalRActionHandler.cs index ddf21016d..383f7b98e 100644 --- a/backend/extensions/Squidex.Extensions/Actions/SignalR/SignalRActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/SignalR/SignalRActionHandler.cs @@ -67,7 +67,8 @@ namespace Squidex.Extensions.Actions.SignalR return (ruleDescription, ruleJob); } - protected override async Task ExecuteJobAsync(SignalRJob job, CancellationToken ct = default) + protected override async Task ExecuteJobAsync(SignalRJob job, + CancellationToken ct = default) { var signalR = await clients.GetClientAsync((job.ConnectionString, job.HubName)); diff --git a/backend/extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs index 36b55a3ec..3014610ae 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs @@ -40,7 +40,8 @@ namespace Squidex.Extensions.Actions.Slack return (Description, ruleJob); } - protected override async Task ExecuteJobAsync(SlackJob job, CancellationToken ct = default) + protected override async Task ExecuteJobAsync(SlackJob job, + CancellationToken ct = default) { using (var httpClient = httpClientFactory.CreateClient()) { diff --git a/backend/extensions/Squidex.Extensions/Actions/Twitter/TweetActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Twitter/TweetActionHandler.cs index 54ba308c2..1a3c12882 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Twitter/TweetActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Twitter/TweetActionHandler.cs @@ -39,7 +39,8 @@ namespace Squidex.Extensions.Actions.Twitter return (Description, ruleJob); } - protected override async Task ExecuteJobAsync(TweetJob job, CancellationToken ct = default) + protected override async Task ExecuteJobAsync(TweetJob job, + CancellationToken ct = default) { var tokens = Tokens.Create( twitterOptions.ClientId, diff --git a/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs index f69eea162..0efe04e86 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; using System.Net.Http; using System.Text; @@ -73,7 +74,7 @@ namespace Squidex.Extensions.Actions.Webhook foreach (var line in lines) { - var indexEqual = line.IndexOf('='); + var indexEqual = line.IndexOf('=', StringComparison.Ordinal); if (indexEqual > 0 && indexEqual < line.Length - 1) { @@ -89,7 +90,8 @@ namespace Squidex.Extensions.Actions.Webhook return headersDictionary; } - protected override async Task ExecuteJobAsync(WebhookJob job, CancellationToken ct = default) + protected override async Task ExecuteJobAsync(WebhookJob job, + CancellationToken ct = default) { using (var httpClient = httpClientFactory.CreateClient()) { diff --git a/backend/extensions/Squidex.Extensions/Assets/Azure/AzureMetadataSource.cs b/backend/extensions/Squidex.Extensions/Assets/Azure/AzureMetadataSource.cs index 28cf4a257..c66a62444 100644 --- a/backend/extensions/Squidex.Extensions/Assets/Azure/AzureMetadataSource.cs +++ b/backend/extensions/Squidex.Extensions/Assets/Azure/AzureMetadataSource.cs @@ -56,7 +56,7 @@ namespace Squidex.Extensions.Assets.Azure { if (command.Type == AssetType.Image && command.File.FileSize <= MaxSize) { - using (var stream = command.File.OpenRead()) + await using (var stream = command.File.OpenRead()) { var result = await client.AnalyzeImageInStreamAsync(stream, features); diff --git a/backend/extensions/Squidex.Extensions/Samples/AssetStore/MemoryAssetStorePlugin.cs b/backend/extensions/Squidex.Extensions/Samples/AssetStore/MemoryAssetStorePlugin.cs index 95f167944..293da57f2 100644 --- a/backend/extensions/Squidex.Extensions/Samples/AssetStore/MemoryAssetStorePlugin.cs +++ b/backend/extensions/Squidex.Extensions/Samples/AssetStore/MemoryAssetStorePlugin.cs @@ -24,7 +24,7 @@ namespace Squidex.Extensions.Samples.AssetStore { builder.Use(async (context, next) => { - if (context.Request.Path.StartsWithSegments("/api/assets/memory")) + if (context.Request.Path.StartsWithSegments("/api/assets/memory", StringComparison.Ordinal)) { context.Response.StatusCode = 200; diff --git a/backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj b/backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj index 2485ca0ee..167f3dbac 100644 --- a/backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj +++ b/backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj @@ -16,6 +16,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json index 95767d5f8..6aa12476c 100644 --- a/backend/i18n/frontend_en.json +++ b/backend/i18n/frontend_en.json @@ -12,11 +12,6 @@ "apps.appNameWarning": "The app name cannot be changed later.", "apps.appsButtonCreate": "Apps Overview", "apps.appsButtonFallbackTitle": "Apps Overview", - "apps.archive": "Archive App", - "apps.archiveConfirmText": "Do you really want to archive this app?", - "apps.archiveConfirmTitle": "Archive App", - "apps.archiveFailed": "Failed to archive app. Please reload.", - "apps.archiveWarning": "Once you archive an app, there is no going back. Please be certain.", "apps.create": "Create App", "apps.createBlankApp": "New App", "apps.createBlankAppDescription": "Create a new blank app without content and schemas.", @@ -26,9 +21,14 @@ "apps.createProfileApp": "New Profile Sample", "apps.createProfileAppDescription": "Create your profile page.", "apps.createWithTemplate": "Create {template} Sample", + "apps.delete": "Delete App", + "apps.deleteConfirmText": "Do you really want to delete this app?", + "apps.deleteConfirmTitle": "I understand. Delete my App", + "apps.deleteFailed": "Failed to delete app. Please reload.", + "apps.deleteWarning": "Once you delete an app, there is no going back. All your data will be deleted in the background.", "apps.empty": "You are not collaborating on any apps yet", "apps.generalSettings": "General", - "apps.generalSettingsDangerZone": "General", + "apps.generalSettingsDangerZone": "Danger Zone", "apps.image": "Image", "apps.imageDrop": "Drop to upload", "apps.leave": "Leave app", diff --git a/backend/i18n/frontend_it.json b/backend/i18n/frontend_it.json index b1676bc47..1935fb3a5 100644 --- a/backend/i18n/frontend_it.json +++ b/backend/i18n/frontend_it.json @@ -12,11 +12,6 @@ "apps.appNameWarning": "Il nome della app non potrà essere cambiato in un secondo momento.", "apps.appsButtonCreate": "Nuova App", "apps.appsButtonFallbackTitle": "Lista App", - "apps.archive": "Archivia l'App", - "apps.archiveConfirmText": "Rimuovi il pattern", - "apps.archiveConfirmTitle": "Sei sicuro di voler archiviare questa app?", - "apps.archiveFailed": "Non è stato possibile archiviare l'app. Per favore ricarica.", - "apps.archiveWarning": "Una volta archiviata una App, non è possibile tornare indietro. Sii certo.", "apps.create": "Crea un'App", "apps.createBlankApp": "Nuova App.", "apps.createBlankAppDescription": "Crea una app vuota senza contenuti o schema.", @@ -26,6 +21,11 @@ "apps.createProfileApp": "Nuovo Profilo", "apps.createProfileAppDescription": "Crea la tua pagina del profilo.", "apps.createWithTemplate": "Create un esempio di {template}", + "apps.delete": "Delete App", + "apps.deleteConfirmText": "Do you really want to delete this app?", + "apps.deleteConfirmTitle": "I understand. Delete my App", + "apps.deleteFailed": "Failed to delete app. Please reload.", + "apps.deleteWarning": "Once you delete an app, there is no going back. All your data will be deleted in the background.", "apps.empty": "Non stai ancora collaborando su nessuna app", "apps.generalSettings": "Generale", "apps.generalSettingsDangerZone": "Generale", diff --git a/backend/i18n/frontend_nl.json b/backend/i18n/frontend_nl.json index 2669a1cf3..11179f499 100644 --- a/backend/i18n/frontend_nl.json +++ b/backend/i18n/frontend_nl.json @@ -12,11 +12,6 @@ "apps.appNameWarning": "De app-naam kan later niet worden gewijzigd.", "apps.appsButtonCreate": "Apps-overzicht", "apps.appsButtonFallbackTitle": "Apps-overzicht", - "apps.archive": "App archiveren", - "apps.archiveConfirmText": "Patroon verwijderen", - "apps.archiveConfirmTitle": "Wil je deze app echt archiveren?", - "apps.archiveFailed": "Kan app niet archiveren. Laad opnieuw.", - "apps.archiveWarning": "Zodra je een app archiveert, is er geen weg meer terug. Wees alsjeblieft zeker.", "apps.create": "App maken", "apps.createBlankApp": "Nieuwe app.", "apps.createBlankAppDescription": "Maak een nieuwe lege app zonder inhoud en schema's.", @@ -26,6 +21,11 @@ "apps.createProfileApp": "Nieuw profielvoorbeeld", "apps.createProfileAppDescription": "Maak uw profielpagina.", "apps.createWithTemplate": "Maak {sjabloon} voorbeeld", + "apps.delete": "Delete App", + "apps.deleteConfirmText": "Do you really want to delete this app?", + "apps.deleteConfirmTitle": "I understand. Delete my App", + "apps.deleteFailed": "Failed to delete app. Please reload.", + "apps.deleteWarning": "Once you delete an app, there is no going back. All your data will be deleted in the background.", "apps.empty": "Je werkt nog niet samen aan een app", "apps.generalSettings": "Algemeen", "apps.generalSettingsDangerZone": "Algemeen", diff --git a/backend/i18n/frontend_zh.json b/backend/i18n/frontend_zh.json index 67d3a5302..c6bde0dea 100644 --- a/backend/i18n/frontend_zh.json +++ b/backend/i18n/frontend_zh.json @@ -12,11 +12,6 @@ "apps.appNameWarning": "以后不能更改应用名称。", "apps.appsButtonCreate": "应用概览", "apps.appsButtonFallbackTitle": "应用概览", - "apps.archive": "存档应用", - "apps.archiveConfirmText": "你真的要存档这个应用程序吗?", - "apps.archiveConfirmTitle": "存档应用程序", - "apps.archiveFailed": "存档应用失败。请重新加载。", - "apps.archiveWarning": "一旦你归档了一个应用程序,就没有回头路了。请确定。", "apps.create": "创建应用程序", "apps.createBlankApp": "新应用程序", "apps.createBlankAppDescription": "创建一个没有内容和Schemas的新空白应用程序。", @@ -26,6 +21,11 @@ "apps.createProfileApp": "新配置文件示例", "apps.createProfileAppDescription": "创建您的个人资料页面。", "apps.createWithTemplate": "创建 {template} 示例", + "apps.delete": "Delete App", + "apps.deleteConfirmText": "Do you really want to delete this app?", + "apps.deleteConfirmTitle": "I understand. Delete my App", + "apps.deleteFailed": "Failed to delete app. Please reload.", + "apps.deleteWarning": "Once you delete an app, there is no going back. All your data will be deleted in the background.", "apps.empty": "您还没有与任何应用协作", "apps.generalSettings": "通用", "apps.generalSettingsDangerZone": "通用", diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json index 95767d5f8..6aa12476c 100644 --- a/backend/i18n/source/frontend_en.json +++ b/backend/i18n/source/frontend_en.json @@ -12,11 +12,6 @@ "apps.appNameWarning": "The app name cannot be changed later.", "apps.appsButtonCreate": "Apps Overview", "apps.appsButtonFallbackTitle": "Apps Overview", - "apps.archive": "Archive App", - "apps.archiveConfirmText": "Do you really want to archive this app?", - "apps.archiveConfirmTitle": "Archive App", - "apps.archiveFailed": "Failed to archive app. Please reload.", - "apps.archiveWarning": "Once you archive an app, there is no going back. Please be certain.", "apps.create": "Create App", "apps.createBlankApp": "New App", "apps.createBlankAppDescription": "Create a new blank app without content and schemas.", @@ -26,9 +21,14 @@ "apps.createProfileApp": "New Profile Sample", "apps.createProfileAppDescription": "Create your profile page.", "apps.createWithTemplate": "Create {template} Sample", + "apps.delete": "Delete App", + "apps.deleteConfirmText": "Do you really want to delete this app?", + "apps.deleteConfirmTitle": "I understand. Delete my App", + "apps.deleteFailed": "Failed to delete app. Please reload.", + "apps.deleteWarning": "Once you delete an app, there is no going back. All your data will be deleted in the background.", "apps.empty": "You are not collaborating on any apps yet", "apps.generalSettings": "General", - "apps.generalSettingsDangerZone": "General", + "apps.generalSettingsDangerZone": "Danger Zone", "apps.image": "Image", "apps.imageDrop": "Drop to upload", "apps.leave": "Leave app", diff --git a/backend/i18n/source/frontend_it.json b/backend/i18n/source/frontend_it.json index 2de7fed67..7175fa4bd 100644 --- a/backend/i18n/source/frontend_it.json +++ b/backend/i18n/source/frontend_it.json @@ -12,11 +12,6 @@ "apps.appNameWarning": "Il nome della app non potrà essere cambiato in un secondo momento.", "apps.appsButtonCreate": "Nuova App", "apps.appsButtonFallbackTitle": "Lista App", - "apps.archive": "Archivia l'App", - "apps.archiveConfirmText": "Rimuovi il pattern", - "apps.archiveConfirmTitle": "Sei sicuro di voler archiviare questa app?", - "apps.archiveFailed": "Non è stato possibile archiviare l'app. Per favore ricarica.", - "apps.archiveWarning": "Una volta archiviata una App, non è possibile tornare indietro. Sii certo.", "apps.create": "Crea un'App", "apps.createBlankApp": "Nuova App.", "apps.createBlankAppDescription": "Crea una app vuota senza contenuti o schema.", diff --git a/backend/i18n/source/frontend_nl.json b/backend/i18n/source/frontend_nl.json index 8daea83a7..f387f172e 100644 --- a/backend/i18n/source/frontend_nl.json +++ b/backend/i18n/source/frontend_nl.json @@ -12,11 +12,6 @@ "apps.appNameWarning": "De app-naam kan later niet worden gewijzigd.", "apps.appsButtonCreate": "Apps-overzicht", "apps.appsButtonFallbackTitle": "Apps-overzicht", - "apps.archive": "App archiveren", - "apps.archiveConfirmText": "Patroon verwijderen", - "apps.archiveConfirmTitle": "Wil je deze app echt archiveren?", - "apps.archiveFailed": "Kan app niet archiveren. Laad opnieuw.", - "apps.archiveWarning": "Zodra je een app archiveert, is er geen weg meer terug. Wees alsjeblieft zeker.", "apps.create": "App maken", "apps.createBlankApp": "Nieuwe app.", "apps.createBlankAppDescription": "Maak een nieuwe lege app zonder inhoud en schema's.", diff --git a/backend/i18n/source/frontend_zh.json b/backend/i18n/source/frontend_zh.json index 57a2be534..b8136400b 100644 --- a/backend/i18n/source/frontend_zh.json +++ b/backend/i18n/source/frontend_zh.json @@ -12,11 +12,6 @@ "apps.appNameWarning": "以后不能更改应用名称。", "apps.appsButtonCreate": "应用概览", "apps.appsButtonFallbackTitle": "应用概览", - "apps.archive": "存档应用", - "apps.archiveConfirmText": "你真的要存档这个应用程序吗?", - "apps.archiveConfirmTitle": "存档应用程序", - "apps.archiveFailed": "存档应用失败。请重新加载。", - "apps.archiveWarning": "一旦你归档了一个应用程序,就没有回头路了。请确定。", "apps.create": "创建应用程序", "apps.createBlankApp": "新应用程序", "apps.createBlankAppDescription": "创建一个没有内容和Schemas的新空白应用程序。", diff --git a/backend/src/Migrations/MigrationPath.cs b/backend/src/Migrations/MigrationPath.cs index 24a8eb610..398a5f91d 100644 --- a/backend/src/Migrations/MigrationPath.cs +++ b/backend/src/Migrations/MigrationPath.cs @@ -18,7 +18,7 @@ namespace Migrations { public sealed class MigrationPath : IMigrationPath { - private const int CurrentVersion = 25; + private const int CurrentVersion = 26; private readonly IServiceProvider serviceProvider; public MigrationPath(IServiceProvider serviceProvider) @@ -75,17 +75,12 @@ namespace Migrations // Version 12: Introduce roles. // Version 24: Improve a naming in the languages config. - if (version < 24) + // Version 26: Introduce full deletion. + if (version < 26) { - yield return serviceProvider.GetRequiredService(); - } - - // Version 14: Schema refactoring - // Version 22: Introduce domain id. - if (version < 22) - { - yield return serviceProvider.GetRequiredService(); - yield return serviceProvider.GetRequiredService(); + // yield return serviceProvider.GetRequiredService(); + // yield return serviceProvider.GetRequiredService(); + yield return serviceProvider.GetRequiredService(); } // Version 18: Rebuild assets. @@ -130,12 +125,6 @@ namespace Migrations yield return serviceProvider.GetRequiredService(); } - // Version 19: Unify indexes. - if (version < 19) - { - yield return serviceProvider.GetRequiredService(); - } - yield return serviceProvider.GetRequiredService(); } } diff --git a/backend/src/Migrations/Migrations.csproj b/backend/src/Migrations/Migrations.csproj index c5ee9fe59..c29900a13 100644 --- a/backend/src/Migrations/Migrations.csproj +++ b/backend/src/Migrations/Migrations.csproj @@ -5,6 +5,10 @@ enable + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/backend/src/Migrations/Migrations/ClearRules.cs b/backend/src/Migrations/Migrations/ClearRules.cs index bf8a06801..e3b0d00ca 100644 --- a/backend/src/Migrations/Migrations/ClearRules.cs +++ b/backend/src/Migrations/Migrations/ClearRules.cs @@ -22,7 +22,8 @@ namespace Migrations.Migrations this.store = store; } - public Task UpdateAsync(CancellationToken ct) + public Task UpdateAsync( + CancellationToken ct) { return store.ClearSnapshotsAsync(); } diff --git a/backend/src/Migrations/Migrations/ClearSchemas.cs b/backend/src/Migrations/Migrations/ClearSchemas.cs index e1100cdc2..bd0eacf39 100644 --- a/backend/src/Migrations/Migrations/ClearSchemas.cs +++ b/backend/src/Migrations/Migrations/ClearSchemas.cs @@ -22,7 +22,8 @@ namespace Migrations.Migrations this.store = store; } - public Task UpdateAsync(CancellationToken ct) + public Task UpdateAsync( + CancellationToken ct) { return store.ClearSnapshotsAsync(); } diff --git a/backend/src/Migrations/Migrations/ConvertEventStore.cs b/backend/src/Migrations/Migrations/ConvertEventStore.cs index c0060d63e..3e30bde3b 100644 --- a/backend/src/Migrations/Migrations/ConvertEventStore.cs +++ b/backend/src/Migrations/Migrations/ConvertEventStore.cs @@ -24,7 +24,8 @@ namespace Migrations.Migrations this.eventStore = eventStore; } - public async Task UpdateAsync(CancellationToken ct) + public async Task UpdateAsync( + CancellationToken ct) { if (eventStore is MongoEventStore mongoEventStore) { diff --git a/backend/src/Migrations/Migrations/ConvertEventStoreAppId.cs b/backend/src/Migrations/Migrations/ConvertEventStoreAppId.cs index 12918d389..d0f741754 100644 --- a/backend/src/Migrations/Migrations/ConvertEventStoreAppId.cs +++ b/backend/src/Migrations/Migrations/ConvertEventStoreAppId.cs @@ -26,7 +26,8 @@ namespace Migrations.Migrations this.eventStore = eventStore; } - public async Task UpdateAsync(CancellationToken ct) + public async Task UpdateAsync( + CancellationToken ct) { if (eventStore is MongoEventStore mongoEventStore) { diff --git a/backend/src/Migrations/Migrations/CreateAssetSlugs.cs b/backend/src/Migrations/Migrations/CreateAssetSlugs.cs index 8ecd2f467..3ae71e9d5 100644 --- a/backend/src/Migrations/Migrations/CreateAssetSlugs.cs +++ b/backend/src/Migrations/Migrations/CreateAssetSlugs.cs @@ -24,16 +24,17 @@ namespace Migrations.Migrations this.stateForAssets = stateForAssets; } - public Task UpdateAsync(CancellationToken ct) + public async Task UpdateAsync( + CancellationToken ct) { - return stateForAssets.ReadAllAsync(async (state, version) => + await foreach (var (state, version) in stateForAssets.ReadAllAsync(ct)) { state.Slug = state.FileName.ToAssetSlug(); var key = DomainId.Combine(state.AppId.Id, state.Id); - await stateForAssets.WriteAsync(key, state, version, version); - }, ct); + await stateForAssets.WriteAsync(key, state, version, version, ct); + } } } } diff --git a/backend/src/Migrations/Migrations/MongoDb/AddAppIdToEventStream.cs b/backend/src/Migrations/Migrations/MongoDb/AddAppIdToEventStream.cs index 8b852db8e..2153451b2 100644 --- a/backend/src/Migrations/Migrations/MongoDb/AddAppIdToEventStream.cs +++ b/backend/src/Migrations/Migrations/MongoDb/AddAppIdToEventStream.cs @@ -15,6 +15,7 @@ using MongoDB.Bson; using MongoDB.Driver; using Squidex.Infrastructure; using Squidex.Infrastructure.Migrations; +using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.Tasks; namespace Migrations.Migrations.MongoDb @@ -28,13 +29,14 @@ namespace Migrations.Migrations.MongoDb this.database = database; } - public async Task UpdateAsync(CancellationToken ct) + public async Task UpdateAsync( + CancellationToken ct) { const int SizeOfBatch = 1000; const int SizeOfQueue = 20; - var collectionOld = database.GetCollection("Events"); - var collectionNew = database.GetCollection("Events2"); + var collectionV1 = database.GetCollection("Events"); + var collectionV2 = database.GetCollection("Events2"); var batchBlock = new BatchBlock(SizeOfBatch, new GroupingDataflowBlockOptions { @@ -60,7 +62,7 @@ namespace Migrations.Migrations.MongoDb { if (!eventStream.StartsWith("app-", StringComparison.OrdinalIgnoreCase)) { - var indexOfType = eventStream.IndexOf('-'); + var indexOfType = eventStream.IndexOf('-', StringComparison.Ordinal); var indexOfId = indexOfType + 1; var indexOfOldId = eventStream.LastIndexOf("--", StringComparison.OrdinalIgnoreCase); @@ -104,7 +106,7 @@ namespace Migrations.Migrations.MongoDb if (writes.Count > 0) { - await collectionNew.BulkWriteAsync(writes, writeOptions); + await collectionV2.BulkWriteAsync(writes, writeOptions, ct); } } catch (OperationCanceledException ex) @@ -121,7 +123,13 @@ namespace Migrations.Migrations.MongoDb batchBlock.BidirectionalLinkTo(actionBlock); - await collectionOld.Find(new BsonDocument()).ForEachAsync(batchBlock.SendAsync, ct); + await foreach (var commit in collectionV1.Find(new BsonDocument()).ToAsyncEnumerable(ct: ct)) + { + if (!await batchBlock.SendAsync(commit, ct)) + { + break; + } + } batchBlock.Complete(); diff --git a/backend/src/Migrations/Migrations/MongoDb/ConvertDocumentIds.cs b/backend/src/Migrations/Migrations/MongoDb/ConvertDocumentIds.cs index e9d12cbfe..71f6c68ef 100644 --- a/backend/src/Migrations/Migrations/MongoDb/ConvertDocumentIds.cs +++ b/backend/src/Migrations/Migrations/MongoDb/ConvertDocumentIds.cs @@ -57,7 +57,8 @@ namespace Migrations.Migrations.MongoDb return this; } - public async Task UpdateAsync(CancellationToken ct) + public async Task UpdateAsync( + CancellationToken ct) { switch (scope) { @@ -72,25 +73,26 @@ namespace Migrations.Migrations.MongoDb } } - private static async Task RebuildAsync(IMongoDatabase database, Action? extraAction, string collectionNameOld, CancellationToken ct) + private static async Task RebuildAsync(IMongoDatabase database, Action? extraAction, string collectionNameV1, + CancellationToken ct) { const int SizeOfBatch = 1000; const int SizeOfQueue = 10; - string collectionNameNew; + string collectionNameV2; - collectionNameNew = $"{collectionNameOld}2"; - collectionNameNew = collectionNameNew.Replace("State_", "States_"); + collectionNameV2 = $"{collectionNameV1}2"; + collectionNameV2 = collectionNameV2.Replace("State_", "States_", StringComparison.Ordinal); - var collectionOld = database.GetCollection(collectionNameOld); - var collectionNew = database.GetCollection(collectionNameNew); + var collectionV1 = database.GetCollection(collectionNameV1); + var collectionV2 = database.GetCollection(collectionNameV2); - if (!await collectionOld.AnyAsync()) + if (!await collectionV1.AnyAsync(ct: ct)) { return; } - await collectionNew.DeleteManyAsync(new BsonDocument(), ct); + await collectionV2.DeleteManyAsync(new BsonDocument(), ct); var batchBlock = new BatchBlock(SizeOfBatch, new GroupingDataflowBlockOptions { @@ -138,7 +140,7 @@ namespace Migrations.Migrations.MongoDb if (writes.Count > 0) { - await collectionNew.BulkWriteAsync(writes, writeOptions); + await collectionV2.BulkWriteAsync(writes, writeOptions, ct); } } catch (OperationCanceledException ex) @@ -155,7 +157,13 @@ namespace Migrations.Migrations.MongoDb batchBlock.BidirectionalLinkTo(actionBlock); - await collectionOld.Find(new BsonDocument()).ForEachAsync(batchBlock.SendAsync, ct); + await foreach (var document in collectionV1.Find(new BsonDocument()).ToAsyncEnumerable(ct: ct)) + { + if (!await batchBlock.SendAsync(document, ct)) + { + break; + } + } batchBlock.Complete(); diff --git a/backend/src/Migrations/Migrations/MongoDb/ConvertOldSnapshotStores.cs b/backend/src/Migrations/Migrations/MongoDb/ConvertOldSnapshotStores.cs index a22ffaefd..7d638e172 100644 --- a/backend/src/Migrations/Migrations/MongoDb/ConvertOldSnapshotStores.cs +++ b/backend/src/Migrations/Migrations/MongoDb/ConvertOldSnapshotStores.cs @@ -23,7 +23,8 @@ namespace Migrations.Migrations.MongoDb this.database = database; } - public Task UpdateAsync(CancellationToken ct) + public Task UpdateAsync( + CancellationToken ct) { var collections = new[] { @@ -39,7 +40,7 @@ namespace Migrations.Migrations.MongoDb return Task.WhenAll( collections .Select(x => database.GetCollection(x)) - .Select(x => x.UpdateManyAsync(filter, update))); + .Select(x => x.UpdateManyAsync(filter, update, cancellationToken: ct))); } } } diff --git a/backend/src/Migrations/Migrations/MongoDb/ConvertRuleEventsJson.cs b/backend/src/Migrations/Migrations/MongoDb/ConvertRuleEventsJson.cs index d0f4905ca..c8e21eee0 100644 --- a/backend/src/Migrations/Migrations/MongoDb/ConvertRuleEventsJson.cs +++ b/backend/src/Migrations/Migrations/MongoDb/ConvertRuleEventsJson.cs @@ -22,7 +22,8 @@ namespace Migrations.Migrations.MongoDb collection = database.GetCollection("RuleEvents"); } - public async Task UpdateAsync(CancellationToken ct) + public async Task UpdateAsync( + CancellationToken ct) { foreach (var document in collection.Find(new BsonDocument()).ToEnumerable(ct)) { diff --git a/backend/src/Migrations/Migrations/MongoDb/DeleteContentCollections.cs b/backend/src/Migrations/Migrations/MongoDb/DeleteContentCollections.cs index f2c33f57f..4754d6baf 100644 --- a/backend/src/Migrations/Migrations/MongoDb/DeleteContentCollections.cs +++ b/backend/src/Migrations/Migrations/MongoDb/DeleteContentCollections.cs @@ -21,7 +21,8 @@ namespace Migrations.Migrations.MongoDb this.database = database; } - public async Task UpdateAsync(CancellationToken ct) + public async Task UpdateAsync( + CancellationToken ct) { await database.DropCollectionAsync("States_Contents", ct); await database.DropCollectionAsync("States_Contents_Archive", ct); diff --git a/backend/src/Migrations/Migrations/MongoDb/RenameAssetMetadata.cs b/backend/src/Migrations/Migrations/MongoDb/RenameAssetMetadata.cs index 1050e82fb..f42c8f52a 100644 --- a/backend/src/Migrations/Migrations/MongoDb/RenameAssetMetadata.cs +++ b/backend/src/Migrations/Migrations/MongoDb/RenameAssetMetadata.cs @@ -22,7 +22,8 @@ namespace Migrations.Migrations.MongoDb this.database = database; } - public async Task UpdateAsync(CancellationToken ct) + public async Task UpdateAsync( + CancellationToken ct) { var collection = database.GetCollection("States_Assets"); diff --git a/backend/src/Migrations/Migrations/MongoDb/RenameAssetSlugField.cs b/backend/src/Migrations/Migrations/MongoDb/RenameAssetSlugField.cs index 78e52b518..9d5c28e4d 100644 --- a/backend/src/Migrations/Migrations/MongoDb/RenameAssetSlugField.cs +++ b/backend/src/Migrations/Migrations/MongoDb/RenameAssetSlugField.cs @@ -22,7 +22,8 @@ namespace Migrations.Migrations.MongoDb this.database = database; } - public Task UpdateAsync(CancellationToken ct) + public Task UpdateAsync( + CancellationToken ct) { var collection = database.GetCollection("States_Assets"); diff --git a/backend/src/Migrations/Migrations/MongoDb/RestructureContentCollection.cs b/backend/src/Migrations/Migrations/MongoDb/RestructureContentCollection.cs index 30e86e3d4..56d20bca6 100644 --- a/backend/src/Migrations/Migrations/MongoDb/RestructureContentCollection.cs +++ b/backend/src/Migrations/Migrations/MongoDb/RestructureContentCollection.cs @@ -23,16 +23,17 @@ namespace Migrations.Migrations.MongoDb this.contentDatabase = contentDatabase; } - public async Task UpdateAsync(CancellationToken ct) + public async Task UpdateAsync( + CancellationToken ct) { - if (await contentDatabase.CollectionExistsAsync("State_Content_Draft")) + if (await contentDatabase.CollectionExistsAsync("State_Content_Draft", ct)) { await contentDatabase.DropCollectionAsync("State_Contents", ct); await contentDatabase.DropCollectionAsync("State_Content_Published", ct); await contentDatabase.RenameCollectionAsync("State_Content_Draft", "State_Contents", cancellationToken: ct); } - if (await contentDatabase.CollectionExistsAsync("State_Contents")) + if (await contentDatabase.CollectionExistsAsync("State_Contents", ct)) { var collection = contentDatabase.GetCollection("State_Contents"); diff --git a/backend/src/Migrations/Migrations/PopulateGrainIndexes.cs b/backend/src/Migrations/Migrations/PopulateGrainIndexes.cs deleted file mode 100644 index 2f6fa1cd7..000000000 --- a/backend/src/Migrations/Migrations/PopulateGrainIndexes.cs +++ /dev/null @@ -1,189 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Squidex.Domain.Apps.Entities.Apps.Indexes; -using Squidex.Domain.Apps.Entities.Rules.Indexes; -using Squidex.Domain.Apps.Entities.Schemas.Indexes; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Domain.Apps.Events.Rules; -using Squidex.Domain.Apps.Events.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Migrations; - -namespace Migrations.Migrations -{ - public class PopulateGrainIndexes : IMigration - { - private readonly IAppsIndex indexApps; - private readonly IRulesIndex indexRules; - private readonly ISchemasIndex indexSchemas; - private readonly IEventDataFormatter eventDataFormatter; - private readonly IEventStore eventStore; - - public PopulateGrainIndexes(IAppsIndex indexApps, IRulesIndex indexRules, ISchemasIndex indexSchemas, - IEventDataFormatter eventDataFormatter, - IEventStore eventStore) - { - this.indexApps = indexApps; - this.indexRules = indexRules; - this.indexSchemas = indexSchemas; - this.eventDataFormatter = eventDataFormatter; - this.eventStore = eventStore; - } - - public Task UpdateAsync(CancellationToken ct) - { - return Task.WhenAll( - RebuildAppIndexes(ct), - RebuildRuleIndexes(ct), - RebuildSchemaIndexes(ct)); - } - - private async Task RebuildAppIndexes(CancellationToken ct) - { - var appsByName = new Dictionary(); - var appsByUser = new Dictionary>(); - - bool HasApp(NamedId appId, bool consistent, out DomainId id) - { - return appsByName!.TryGetValue(appId.Name, out id) && (!consistent || id == appId.Id); - } - - HashSet Index(string contributorId) - { - return appsByUser!.GetOrAddNew(contributorId); - } - - void RemoveApp(NamedId appId, bool consistent) - { - if (HasApp(appId, consistent, out var id)) - { - foreach (var apps in appsByUser!.Values) - { - apps.Remove(id); - } - - appsByName!.Remove(appId.Name); - } - } - - await foreach (var storedEvent in eventStore.QueryAllAsync("^app\\-", ct: ct)) - { - var @event = eventDataFormatter.ParseIfKnown(storedEvent); - - if (@event != null) - { - switch (@event.Payload) - { - case AppCreated created: - { - RemoveApp(created.AppId, false); - - appsByName[created.Name] = created.AppId.Id; - break; - } - - case AppContributorAssigned contributorAssigned: - { - if (HasApp(contributorAssigned.AppId, true, out _)) - { - Index(contributorAssigned.ContributorId).Add(contributorAssigned.AppId.Id); - } - - break; - } - - case AppContributorRemoved contributorRemoved: - Index(contributorRemoved.ContributorId).Remove(contributorRemoved.AppId.Id); - break; - case AppArchived archived: - RemoveApp(archived.AppId, true); - break; - } - } - } - - await indexApps.RebuildAsync(appsByName); - - foreach (var (contributorId, apps) in appsByUser) - { - await indexApps.RebuildByContributorsAsync(contributorId, apps); - } - } - - private async Task RebuildRuleIndexes(CancellationToken ct) - { - var rulesByApp = new Dictionary>(); - - HashSet Index(RuleEvent @event) - { - return rulesByApp!.GetOrAddNew(@event.AppId.Id); - } - - await foreach (var storedEvent in eventStore.QueryAllAsync("^rule\\-", ct: ct)) - { - var @event = eventDataFormatter.ParseIfKnown(storedEvent); - - if (@event != null) - { - switch (@event.Payload) - { - case RuleCreated created: - Index(created).Add(created.RuleId); - break; - case RuleDeleted deleted: - Index(deleted).Remove(deleted.RuleId); - break; - } - } - } - - foreach (var (appId, rules) in rulesByApp) - { - await indexRules.RebuildAsync(appId, rules); - } - } - - private async Task RebuildSchemaIndexes(CancellationToken ct) - { - var schemasByApp = new Dictionary>(); - - Dictionary Index(SchemaEvent @event) - { - return schemasByApp!.GetOrAddNew(@event.AppId.Id); - } - - await foreach (var storedEvent in eventStore.QueryAllAsync("^schema\\-", ct: ct)) - { - var @event = eventDataFormatter.ParseIfKnown(storedEvent); - - if (@event != null) - { - switch (@event.Payload) - { - case SchemaCreated created: - Index(created)[created.SchemaId.Name] = created.SchemaId.Id; - break; - case SchemaDeleted deleted: - Index(deleted).Remove(deleted.SchemaId.Name); - break; - } - } - } - - foreach (var (appId, schemas) in schemasByApp) - { - await indexSchemas.RebuildAsync(appId, schemas); - } - } - } -} diff --git a/backend/src/Migrations/Migrations/RebuildApps.cs b/backend/src/Migrations/Migrations/RebuildApps.cs index 56e812d98..ffc193696 100644 --- a/backend/src/Migrations/Migrations/RebuildApps.cs +++ b/backend/src/Migrations/Migrations/RebuildApps.cs @@ -25,7 +25,8 @@ namespace Migrations.Migrations this.rebuildOptions = rebuildOptions.Value; } - public Task UpdateAsync(CancellationToken ct) + public Task UpdateAsync( + CancellationToken ct) { return rebuilder.RebuildAppsAsync(rebuildOptions.BatchSize, ct); } diff --git a/backend/src/Migrations/Migrations/RebuildAssetFolders.cs b/backend/src/Migrations/Migrations/RebuildAssetFolders.cs index 74a0f71c4..24bb986b2 100644 --- a/backend/src/Migrations/Migrations/RebuildAssetFolders.cs +++ b/backend/src/Migrations/Migrations/RebuildAssetFolders.cs @@ -25,7 +25,8 @@ namespace Migrations.Migrations this.rebuildOptions = rebuildOptions.Value; } - public Task UpdateAsync(CancellationToken ct) + public Task UpdateAsync( + CancellationToken ct) { return rebuilder.RebuildAssetFoldersAsync(rebuildOptions.BatchSize, ct); } diff --git a/backend/src/Migrations/Migrations/RebuildAssets.cs b/backend/src/Migrations/Migrations/RebuildAssets.cs index 9a639e7ac..98ac9a0f5 100644 --- a/backend/src/Migrations/Migrations/RebuildAssets.cs +++ b/backend/src/Migrations/Migrations/RebuildAssets.cs @@ -25,7 +25,8 @@ namespace Migrations.Migrations this.rebuildOptions = rebuildOptions.Value; } - public Task UpdateAsync(CancellationToken ct) + public Task UpdateAsync( + CancellationToken ct) { return rebuilder.RebuildAssetsAsync(rebuildOptions.BatchSize, ct); } diff --git a/backend/src/Migrations/Migrations/RebuildContents.cs b/backend/src/Migrations/Migrations/RebuildContents.cs index 771252a6d..6fc6cddbb 100644 --- a/backend/src/Migrations/Migrations/RebuildContents.cs +++ b/backend/src/Migrations/Migrations/RebuildContents.cs @@ -25,7 +25,8 @@ namespace Migrations.Migrations this.rebuildOptions = rebuildOptions.Value; } - public Task UpdateAsync(CancellationToken ct) + public Task UpdateAsync( + CancellationToken ct) { return rebuilder.RebuildContentAsync(rebuildOptions.BatchSize, ct); } diff --git a/backend/src/Migrations/Migrations/RebuildRules.cs b/backend/src/Migrations/Migrations/RebuildRules.cs new file mode 100644 index 000000000..7f4da766e --- /dev/null +++ b/backend/src/Migrations/Migrations/RebuildRules.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Migrations; + +namespace Migrations.Migrations +{ + public sealed class RebuildRules : IMigration + { + private readonly Rebuilder rebuilder; + private readonly RebuildOptions rebuildOptions; + + public RebuildRules(Rebuilder rebuilder, + IOptions rebuildOptions) + { + this.rebuilder = rebuilder; + this.rebuildOptions = rebuildOptions.Value; + } + + public Task UpdateAsync( + CancellationToken ct) + { + return rebuilder.RebuildRulesAsync(rebuildOptions.BatchSize, ct); + } + } +} diff --git a/backend/src/Migrations/Migrations/RebuildSchemas.cs b/backend/src/Migrations/Migrations/RebuildSchemas.cs new file mode 100644 index 000000000..01f7a2270 --- /dev/null +++ b/backend/src/Migrations/Migrations/RebuildSchemas.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Migrations; + +namespace Migrations.Migrations +{ + public sealed class RebuildSchemas : IMigration + { + private readonly Rebuilder rebuilder; + private readonly RebuildOptions rebuildOptions; + + public RebuildSchemas(Rebuilder rebuilder, + IOptions rebuildOptions) + { + this.rebuilder = rebuilder; + this.rebuildOptions = rebuildOptions.Value; + } + + public Task UpdateAsync( + CancellationToken ct) + { + return rebuilder.RebuildSchemasAsync(rebuildOptions.BatchSize, ct); + } + } +} diff --git a/backend/src/Migrations/Migrations/RebuildSnapshots.cs b/backend/src/Migrations/Migrations/RebuildSnapshots.cs index cf6b74965..bc5e350ac 100644 --- a/backend/src/Migrations/Migrations/RebuildSnapshots.cs +++ b/backend/src/Migrations/Migrations/RebuildSnapshots.cs @@ -25,7 +25,8 @@ namespace Migrations.Migrations this.rebuildOptions = rebuildOptions.Value; } - public async Task UpdateAsync(CancellationToken ct) + public async Task UpdateAsync( + CancellationToken ct) { await rebuilder.RebuildAppsAsync(rebuildOptions.BatchSize, ct); await rebuilder.RebuildSchemasAsync(rebuildOptions.BatchSize, ct); diff --git a/backend/src/Migrations/Migrations/StartEventConsumers.cs b/backend/src/Migrations/Migrations/StartEventConsumers.cs index 8e635c215..4551da9ea 100644 --- a/backend/src/Migrations/Migrations/StartEventConsumers.cs +++ b/backend/src/Migrations/Migrations/StartEventConsumers.cs @@ -23,7 +23,8 @@ namespace Migrations.Migrations eventConsumerManager = grainFactory.GetGrain(SingleGrain.Id); } - public Task UpdateAsync(CancellationToken ct) + public Task UpdateAsync( + CancellationToken ct) { return eventConsumerManager.StartAllAsync(); } diff --git a/backend/src/Migrations/Migrations/StopEventConsumers.cs b/backend/src/Migrations/Migrations/StopEventConsumers.cs index 152793f49..a3f4b40be 100644 --- a/backend/src/Migrations/Migrations/StopEventConsumers.cs +++ b/backend/src/Migrations/Migrations/StopEventConsumers.cs @@ -23,7 +23,8 @@ namespace Migrations.Migrations eventConsumerManager = grainFactory.GetGrain(SingleGrain.Id); } - public Task UpdateAsync(CancellationToken ct) + public Task UpdateAsync( + CancellationToken ct) { return eventConsumerManager.StopAllAsync(); } diff --git a/backend/src/Migrations/OldEvents/AppArchived.cs b/backend/src/Migrations/OldEvents/AppArchived.cs new file mode 100644 index 000000000..f71fc057d --- /dev/null +++ b/backend/src/Migrations/OldEvents/AppArchived.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Migrations; +using Squidex.Infrastructure.Reflection; + +namespace Migrations.OldEvents +{ + [EventType(nameof(AppArchived))] + public sealed class AppArchived : AppEvent, IMigrated + { + public IEvent Migrate() + { + return SimpleMapper.Map(this, new AppDeleted()); + } + } +} diff --git a/backend/src/Migrations/OldEvents/AppClientChanged.cs b/backend/src/Migrations/OldEvents/AppClientChanged.cs index e7a69fe03..22e35a353 100644 --- a/backend/src/Migrations/OldEvents/AppClientChanged.cs +++ b/backend/src/Migrations/OldEvents/AppClientChanged.cs @@ -23,7 +23,10 @@ namespace Migrations.OldEvents public IEvent Migrate() { - var permission = IsReader ? AppClientPermission.Reader : AppClientPermission.Editor; + var permission = + IsReader ? + AppClientPermission.Reader : + AppClientPermission.Editor; return SimpleMapper.Map(this, new AppClientUpdated { Permission = permission }); } diff --git a/backend/src/Migrations/RebuildRunner.cs b/backend/src/Migrations/RebuildRunner.cs index 4f83184e7..5a6620103 100644 --- a/backend/src/Migrations/RebuildRunner.cs +++ b/backend/src/Migrations/RebuildRunner.cs @@ -8,7 +8,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Options; -using Migrations.Migrations; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Infrastructure.Commands; @@ -18,22 +17,20 @@ namespace Migrations { private readonly RebuildFiles rebuildFiles; private readonly Rebuilder rebuilder; - private readonly PopulateGrainIndexes populateGrainIndexes; private readonly RebuildOptions rebuildOptions; public RebuildRunner( IOptions rebuildOptions, Rebuilder rebuilder, - RebuildFiles rebuildFiles, - PopulateGrainIndexes populateGrainIndexes) + RebuildFiles rebuildFiles) { this.rebuildFiles = rebuildFiles; this.rebuilder = rebuilder; this.rebuildOptions = rebuildOptions.Value; - this.populateGrainIndexes = populateGrainIndexes; } - public async Task RunAsync(CancellationToken ct) + public async Task RunAsync( + CancellationToken ct) { var batchSize = rebuildOptions.CalculateBatchSize(); @@ -67,11 +64,6 @@ namespace Migrations { await rebuilder.RebuildContentAsync(batchSize, ct); } - - if (rebuildOptions.Indexes) - { - await populateGrainIndexes.UpdateAsync(ct); - } } } } diff --git a/backend/src/Migrations/RebuilderExtensions.cs b/backend/src/Migrations/RebuilderExtensions.cs index d6e3847c2..472694ad1 100644 --- a/backend/src/Migrations/RebuilderExtensions.cs +++ b/backend/src/Migrations/RebuilderExtensions.cs @@ -18,32 +18,38 @@ namespace Migrations { public static class RebuilderExtensions { - public static Task RebuildAppsAsync(this Rebuilder rebuilder, int batchSize, CancellationToken ct = default) + public static Task RebuildAppsAsync(this Rebuilder rebuilder, int batchSize, + CancellationToken ct = default) { return rebuilder.RebuildAsync("^app\\-", batchSize, ct); } - public static Task RebuildSchemasAsync(this Rebuilder rebuilder, int batchSize, CancellationToken ct = default) + public static Task RebuildSchemasAsync(this Rebuilder rebuilder, int batchSize, + CancellationToken ct = default) { return rebuilder.RebuildAsync("^schema\\-", batchSize, ct); } - public static Task RebuildRulesAsync(this Rebuilder rebuilder, int batchSize, CancellationToken ct = default) + public static Task RebuildRulesAsync(this Rebuilder rebuilder, int batchSize, + CancellationToken ct = default) { return rebuilder.RebuildAsync("^rule\\-", batchSize, ct); } - public static Task RebuildAssetsAsync(this Rebuilder rebuilder, int batchSize, CancellationToken ct = default) + public static Task RebuildAssetsAsync(this Rebuilder rebuilder, int batchSize, + CancellationToken ct = default) { return rebuilder.RebuildAsync("^asset\\-", batchSize, ct); } - public static Task RebuildAssetFoldersAsync(this Rebuilder rebuilder, int batchSize, CancellationToken ct = default) + public static Task RebuildAssetFoldersAsync(this Rebuilder rebuilder, int batchSize, + CancellationToken ct = default) { return rebuilder.RebuildAsync("^assetFolder\\-", batchSize, ct); } - public static Task RebuildContentAsync(this Rebuilder rebuilder, int batchSize, CancellationToken ct = default) + public static Task RebuildContentAsync(this Rebuilder rebuilder, int batchSize, + CancellationToken ct = default) { return rebuilder.RebuildAsync("^content\\-", batchSize, ct); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/GeoJsonValue.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/GeoJsonValue.cs index 6cdf281f6..55f2434c9 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/GeoJsonValue.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/GeoJsonValue.cs @@ -34,7 +34,7 @@ namespace Squidex.Domain.Apps.Core.Contents stream.Position = 0; - geoJSON = serializer.Deserialize(stream, null, leaveOpen: true); + geoJSON = serializer.Deserialize(stream, null, true); return GeoJsonParseResult.Success; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs index ffb4acccf..a81a09c4f 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs @@ -36,12 +36,12 @@ namespace Squidex.Domain.Apps.Core.Contents public bool Equals(Status other) { - return string.Equals(Name, other.Name); + return string.Equals(Name, other.Name, StringComparison.Ordinal); } public override int GetHashCode() { - return Name.GetHashCode(); + return Name.GetHashCode(StringComparison.Ordinal); } public override string ToString() diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Partitioning.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Partitioning.cs index 1362b38bd..e4d29bc3a 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Partitioning.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Partitioning.cs @@ -38,7 +38,7 @@ namespace Squidex.Domain.Apps.Core public override int GetHashCode() { - return Key.GetHashCode(); + return Key.GetHashCode(StringComparison.Ordinal); } public override string ToString() diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedCommentEvent.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedCommentEvent.cs index d5a50418f..6f67ce361 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedCommentEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedCommentEvent.cs @@ -25,7 +25,7 @@ namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents [IgnoreDataMember] public override long Partition { - get => MentionedUser?.Id.GetHashCode() ?? 0; + get => MentionedUser?.Id.GetHashCode(StringComparison.Ordinal) ?? 0; } public bool ShouldSerializeMentionedUser() diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs index 6a0c4659e..8dc88d521 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs @@ -34,7 +34,7 @@ namespace Squidex.Domain.Apps.Core.Rules [Pure] public Rule Rename(string newName) { - if (string.Equals(Name, newName)) + if (string.Equals(Name, newName, StringComparison.Ordinal)) { return this; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs index 67a2d46fc..11d7bf00f 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs @@ -206,7 +206,7 @@ namespace Squidex.Domain.Apps.Core.Schemas [Pure] public Schema ChangeCategory(string? category) { - if (string.Equals(Category, category)) + if (string.Equals(Category, category, StringComparison.Ordinal)) { return this; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj b/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj index 4383c8bf5..807ea3145 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj @@ -9,6 +9,10 @@ True + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs index 2f98f6a7e..74164e963 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using Microsoft.OData.Edm; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure; @@ -18,12 +19,12 @@ namespace Squidex.Domain.Apps.Core.GenerateEdmSchema { public static string EscapeEdmField(this string field) { - return field.Replace("-", "_"); + return field.Replace("-", "_", StringComparison.Ordinal); } public static string UnescapeEdmField(this string field) { - return field.Replace("_", "-"); + return field.Replace("_", "-", StringComparison.Ordinal); } public static EdmComplexType BuildEdmType(this Schema schema, bool withHidden, PartitionResolver partitionResolver, EdmTypeFactory typeFactory, diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs index 09039d51f..f38a3d860 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleActionHandler.cs @@ -21,6 +21,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules Task<(string Description, object Data)> CreateJobAsync(EnrichedEvent @event, RuleAction action); - Task ExecuteJobAsync(object data, CancellationToken ct = default); + Task ExecuteJobAsync(object data, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleService.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleService.cs index 20f0d565f..51d1ce036 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleService.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleService.cs @@ -20,10 +20,13 @@ namespace Squidex.Domain.Apps.Core.HandleRules string GetName(AppEvent @event); - IAsyncEnumerable CreateSnapshotJobsAsync(RuleContext context, CancellationToken ct = default); + IAsyncEnumerable CreateSnapshotJobsAsync(RuleContext context, + CancellationToken ct = default); - IAsyncEnumerable CreateJobsAsync(Envelope @event, RuleContext context, CancellationToken ct = default); + IAsyncEnumerable CreateJobsAsync(Envelope @event, RuleContext context, + CancellationToken ct = default); - Task<(Result Result, TimeSpan Elapsed)> InvokeAsync(string actionName, string job); + Task<(Result Result, TimeSpan Elapsed)> InvokeAsync(string actionName, string job, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs index e5fee620a..f6ce861ac 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/IRuleTriggerHandler.cs @@ -24,12 +24,14 @@ namespace Squidex.Domain.Apps.Core.HandleRules get => false; } - IAsyncEnumerable CreateSnapshotEventsAsync(RuleContext context, CancellationToken ct) + IAsyncEnumerable CreateSnapshotEventsAsync(RuleContext context, + CancellationToken ct) { return AsyncEnumerable.Empty(); } - IAsyncEnumerable CreateEnrichedEventsAsync(Envelope @event, RuleContext context, CancellationToken ct); + IAsyncEnumerable CreateEnrichedEventsAsync(Envelope @event, RuleContext context, + CancellationToken ct); string? GetName(AppEvent @event) { diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Result.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Result.cs index 55daa8db2..dc33e7143 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Result.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Result.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Globalization; using System.Text; namespace Squidex.Domain.Apps.Core.HandleRules @@ -41,7 +42,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules } dumpBuilder.AppendLine(); - dumpBuilder.AppendFormat("Elapsed {0}.", elapsed); + dumpBuilder.AppendFormat(CultureInfo.InvariantCulture, "Elapsed {0}.", elapsed); dumpBuilder.AppendLine(); Dump = dumpBuilder.ToString(); diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs index 197875e53..1233f7f39 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleActionHandler.cs @@ -61,7 +61,8 @@ namespace Squidex.Domain.Apps.Core.HandleRules return (description, data!); } - async Task IRuleActionHandler.ExecuteJobAsync(object data, CancellationToken ct) + async Task IRuleActionHandler.ExecuteJobAsync(object data, + CancellationToken ct) { var typedData = (TData)data; @@ -70,7 +71,9 @@ namespace Squidex.Domain.Apps.Core.HandleRules protected virtual Task<(string Description, TData Data)> CreateJobAsync(EnrichedEvent @event, TAction action) { +#pragma warning disable MA0042 // Do not use blocking calls in an async method return Task.FromResult(CreateJob(@event, action)); +#pragma warning restore MA0042 // Do not use blocking calls in an async method } protected virtual (string Description, TData Data) CreateJob(EnrichedEvent @event, TAction action) @@ -78,6 +81,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules throw new NotImplementedException(); } - protected abstract Task ExecuteJobAsync(TData job, CancellationToken ct = default); + protected abstract Task ExecuteJobAsync(TData job, + CancellationToken ct = default); } } 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 e73b04bd0..de8de3cc1 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text; using System.Text.RegularExpressions; @@ -25,8 +26,8 @@ namespace Squidex.Domain.Apps.Core.HandleRules public class RuleEventFormatter { private const string GlobalFallback = "null"; - private static readonly Regex RegexPatternOld = new Regex(@"^(?(?[^_]*)_(?[^\s]*))", RegexOptions.Compiled); - private static readonly Regex RegexPatternNew = new Regex(@"^\{(?(?[\w]+)_(?[\w\.\-]+))[\s]*(\|[\s]*(?[^\?}]+))?(\?[\s]*(?[^\}\s]+))?[\s]*\}", RegexOptions.Compiled); + 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 IEnumerable formatters; private readonly ITemplateEngine templateEngine; @@ -111,7 +112,9 @@ namespace Squidex.Domain.Apps.Core.HandleRules ["event"] = @event }; +#pragma warning disable MA0042 // Do not use blocking calls in an async method var result = scriptEngine.Execute(vars, script).ToString(); +#pragma warning restore MA0042 // Do not use blocking calls in an async method if (result == "undefined") { @@ -285,7 +288,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules if (instant.Success) { - text = instant.Value.ToUnixTimeMilliseconds().ToString(); + text = instant.Value.ToUnixTimeMilliseconds().ToString(CultureInfo.InvariantCulture); } break; @@ -297,7 +300,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules if (instant.Success) { - text = instant.Value.ToUnixTimeSeconds().ToString(); + text = instant.Value.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture); } break; 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 edfd68755..8351728f1 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs @@ -364,7 +364,8 @@ namespace Squidex.Domain.Apps.Core.HandleRules return @event.GetType().Name; } - public async Task<(Result Result, TimeSpan Elapsed)> InvokeAsync(string actionName, string job) + public async Task<(Result Result, TimeSpan Elapsed)> InvokeAsync(string actionName, string job, + CancellationToken ct = default) { var actionWatch = ValueStopwatch.StartNew(); @@ -379,7 +380,10 @@ namespace Squidex.Domain.Apps.Core.HandleRules using (var cts = new CancellationTokenSource(GetTimeoutInMs())) { - result = await actionHandler.ExecuteJobAsync(deserialized, cts.Token).WithCancellation(cts.Token); + using (var combined = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, ct)) + { + result = await actionHandler.ExecuteJobAsync(deserialized, combined.Token).WithCancellation(combined.Token); + } } } catch (Exception ex) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs index fb27d87fd..ade8c19fc 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Globalization; using Jint; using Jint.Native; using Jint.Native.Object; @@ -107,7 +108,7 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper for (var i = 0; i < arr.Length; i++) { - result.Add(Map(arr.Get(i.ToString()))); + result.Add(Map(arr.Get(i.ToString(CultureInfo.InvariantCulture)))); } return result; diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs index 50df4e643..9c7c07578 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/HttpJintExtension.cs @@ -137,7 +137,7 @@ namespace Squidex.Domain.Apps.Core.Scripting.Extensions private static async Task ParseResponse(ExecutionContext context, HttpResponseMessage response) { - var responseString = await response.Content.ReadAsStringAsync(); + var responseString = await response.Content.ReadAsStringAsync(context.CancellationToken); context.CancellationToken.ThrowIfCancellationRequested(); diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs index 7eca9c6a7..0f2fe16a5 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/IScriptEngine.cs @@ -14,9 +14,11 @@ namespace Squidex.Domain.Apps.Core.Scripting { public interface IScriptEngine { - Task ExecuteAsync(ScriptVars vars, string script, ScriptOptions options = default, CancellationToken ct = default); + Task ExecuteAsync(ScriptVars vars, string script, ScriptOptions options = default, + CancellationToken ct = default); - Task TransformAsync(ScriptVars vars, string script, ScriptOptions options = default, CancellationToken ct = default); + Task TransformAsync(ScriptVars vars, string script, ScriptOptions options = default, + CancellationToken ct = default); IJsonValue Execute(ScriptVars vars, string script, ScriptOptions options = default); diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs index 087fb3f4d..25e5398e2 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs @@ -80,7 +80,7 @@ namespace Squidex.Domain.Apps.Core.Scripting { var tcs = new TaskCompletionSource(); - using (combined.Token.Register(() => tcs.TrySetCanceled())) + await using (combined.Token.Register(() => tcs.TrySetCanceled(combined.Token))) { var context = CreateEngine(options) @@ -118,7 +118,7 @@ namespace Squidex.Domain.Apps.Core.Scripting { var tcs = new TaskCompletionSource(); - using (combined.Token.Register(() => tcs.TrySetCanceled())) + await using (combined.Token.Register(() => tcs.TrySetCanceled(combined.Token))) { var context = CreateEngine(options) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs index 063cae2b9..b7ba22b06 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs @@ -51,7 +51,7 @@ namespace Squidex.Domain.Apps.Core.Scripting private static JsValue CreateUser(Engine engine, string id, bool isClient, string email, string? name, IEnumerable allClaims) { var claims = - allClaims.GroupBy(x => x.Type.Split(ClaimSeparators).Last()) + allClaims.GroupBy(x => x.Type.Split(ClaimSeparators)[^1]) .ToDictionary( x => x.Key, x => x.Select(y => y.Value).ToArray()); diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptOptions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptOptions.cs index 8eb74efdc..52c8174cf 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptOptions.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptOptions.cs @@ -15,7 +15,7 @@ namespace Squidex.Domain.Apps.Core.Scripting public bool AsContext { get; set; } - public override string ToString() + public override readonly string ToString() { return $"CanReject={CanReject}, CanDisallow={CanDisallow}, AsContext={AsContext}"; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj b/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj index b9ca82aab..22d2c0f61 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj @@ -18,6 +18,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/DateTimeFluidExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/DateTimeFluidExtension.cs index 5077a1949..dc1f64e88 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/DateTimeFluidExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/DateTimeFluidExtension.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Globalization; using Fluid; using Fluid.Values; using NodaTime; @@ -86,7 +87,7 @@ namespace Squidex.Domain.Apps.Core.Templates.Extensions private static FluidValue Format(FilterArguments arguments, DateTimeOffset value) { - var formatted = value.ToString(arguments.At(0).ToStringValue()); + var formatted = value.ToString(arguments.At(0).ToStringValue(), CultureInfo.InvariantCulture); return new StringValue(formatted); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs index 0ec6b052a..c44a61779 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/PatternValidator.cs @@ -1,4 +1,4 @@ -// ========================================================================== +// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschraenkt) @@ -19,13 +19,20 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators private readonly Regex regex; private readonly string? errorMessage; - public PatternValidator(string pattern, string? errorMessage = null) + public PatternValidator(string pattern, string? errorMessage = null, bool capture = false) { Guard.NotNullOrEmpty(pattern, nameof(pattern)); this.errorMessage = errorMessage; - regex = new Regex($"^{pattern}$", RegexOptions.None, Timeout); + var options = RegexOptions.None; + + if (!capture) + { + options |= RegexOptions.ExplicitCapture; + } + + regex = new Regex($"^{pattern}$", options, Timeout); } public Task ValidateAsync(object? value, ValidationContext context, AddError addError) @@ -58,4 +65,4 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators return Task.CompletedTask; } } -} \ No newline at end of file +} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs new file mode 100644 index 000000000..f511bd7ea --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs @@ -0,0 +1,46 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using MongoDB.Bson.Serialization.Attributes; +using Squidex.Domain.Apps.Entities.Apps.DomainObject; +using Squidex.Infrastructure; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Apps +{ + public sealed class MongoAppEntity : MongoState + { + [BsonRequired] + [BsonElement("_an")] + public string IndexedName { get; set; } + + [BsonRequired] + [BsonElement("_ui")] + public string[] IndexedUserIds { get; set; } + + [BsonRequired] + [BsonElement("_dl")] + public bool IndexedDeleted { get; set; } + + public override void Prepare() + { + var users = new HashSet + { + Document.CreatedBy.Identifier + }; + + users.AddRange(Document.Contributors.Keys); + users.AddRange(Document.Clients.Keys); + + IndexedUserIds = users.ToArray(); + IndexedDeleted = Document.IsDeleted; + IndexedName = Document.Name; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs new file mode 100644 index 000000000..a8424ac71 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs @@ -0,0 +1,86 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Driver; +using Newtonsoft.Json; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Apps.DomainObject; +using Squidex.Domain.Apps.Entities.Apps.Repositories; +using Squidex.Infrastructure; +using Squidex.Infrastructure.MongoDb; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Apps +{ + public sealed class MongoAppRepository : MongoSnapshotStoreBase, IAppRepository, IDeleter + { + public MongoAppRepository(IMongoDatabase database, JsonSerializer jsonSerializer) + : base(database, jsonSerializer) + { + } + + protected override Task SetupCollectionAsync(IMongoCollection collection, + CancellationToken ct) + { + return collection.Indexes.CreateManyAsync(new[] + { + new CreateIndexModel( + Index + .Ascending(x => x.IndexedName)), + new CreateIndexModel( + Index + .Ascending(x => x.IndexedUserIds)) + }, ct); + } + + Task IDeleter.DeleteAppAsync(IAppEntity app, + CancellationToken ct) + { + return Collection.DeleteManyAsync(Filter.Eq(x => x.DocumentId, app.Id), ct); + } + + public async Task> QueryIdsAsync(string contributorId, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoAppRepository/QueryIdsAsync")) + { + var find = Collection.Find(x => x.IndexedUserIds.Contains(contributorId) && !x.IndexedDeleted); + + return await QueryAsync(find, ct); + } + } + + public async Task> QueryIdsAsync(IEnumerable names, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoAppRepository/QueryAsync")) + { + var find = Collection.Find(x => names.Contains(x.IndexedName) && !x.IndexedDeleted); + + return await QueryAsync(find, ct); + } + } + + private static async Task> QueryAsync(IFindFluent find, + CancellationToken ct) + { + var entities = await find.Only(x => x.DocumentId, x => x.IndexedName).ToListAsync(ct); + + return entities.Select(x => + { + var indexedId = DomainId.Create(x["_id"].AsString); + var indexedName = x["_an"].AsString; + + return new { indexedName, indexedId }; + }).ToDictionary(x => x.indexedName, x => x.indexedId); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository.cs index e68d7bd39..90a5c85de 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository.cs @@ -30,7 +30,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets } protected override Task SetupCollectionAsync(IMongoCollection collection, - CancellationToken ct = default) + CancellationToken ct) { return collection.Indexes.CreateManyAsync(new[] { diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository_SnapshotStore.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository_SnapshotStore.cs index b406b3e18..276fb1e91 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository_SnapshotStore.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository_SnapshotStore.cs @@ -5,13 +5,13 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using MongoDB.Bson; using MongoDB.Driver; +using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Assets.DomainObject; using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; @@ -20,15 +20,28 @@ using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.MongoDb.Assets { - public sealed partial class MongoAssetFolderRepository : ISnapshotStore + public sealed partial class MongoAssetFolderRepository : ISnapshotStore, IDeleter { - async Task<(AssetFolderDomainObject.State Value, bool Valid, long Version)> ISnapshotStore.ReadAsync(DomainId key) + Task IDeleter.DeleteAppAsync(IAppEntity app, + CancellationToken ct) + { + return Collection.DeleteManyAsync(Filter.Eq(x => x.IndexedAppId, app.Id), ct); + } + + IAsyncEnumerable<(AssetFolderDomainObject.State State, long Version)> ISnapshotStore.ReadAllAsync( + CancellationToken ct) + { + return Collection.Find(new BsonDocument(), Batching.Options).ToAsyncEnumerable(ct).Select(x => (Map(x), x.Version)); + } + + async Task<(AssetFolderDomainObject.State Value, bool Valid, long Version)> ISnapshotStore.ReadAsync(DomainId key, + CancellationToken ct) { using (Telemetry.Activities.StartActivity("MongoAssetFolderRepository/ReadAsync")) { var existing = await Collection.Find(x => x.DocumentId == key) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); if (existing != null) { @@ -39,17 +52,19 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets } } - async Task ISnapshotStore.WriteAsync(DomainId key, AssetFolderDomainObject.State value, long oldVersion, long newVersion) + async Task ISnapshotStore.WriteAsync(DomainId key, AssetFolderDomainObject.State value, long oldVersion, long newVersion, + CancellationToken ct) { using (Telemetry.Activities.StartActivity("MongoAssetFolderRepository/WriteAsync")) { var entity = Map(value); - await Collection.UpsertVersionedAsync(key, oldVersion, newVersion, entity); + await Collection.UpsertVersionedAsync(key, oldVersion, newVersion, entity, ct); } } - async Task ISnapshotStore.WriteManyAsync(IEnumerable<(DomainId Key, AssetFolderDomainObject.State Value, long Version)> snapshots) + async Task ISnapshotStore.WriteManyAsync(IEnumerable<(DomainId Key, AssetFolderDomainObject.State Value, long Version)> snapshots, + CancellationToken ct) { using (Telemetry.Activities.StartActivity("MongoAssetFolderRepository/WriteManyAsync")) { @@ -66,24 +81,16 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets return; } - await Collection.BulkWriteAsync(updates, BulkUnordered); + await Collection.BulkWriteAsync(updates, BulkUnordered, ct); } } - async Task ISnapshotStore.ReadAllAsync(Func callback, + async Task ISnapshotStore.RemoveAsync(DomainId key, CancellationToken ct) - { - using (Telemetry.Activities.StartActivity("MongoAssetFolderRepository/ReadAllAsync")) - { - await Collection.Find(new BsonDocument(), Batching.Options).ForEachAsync(x => callback(Map(x), x.Version), ct); - } - } - - async Task ISnapshotStore.RemoveAsync(DomainId key) { using (Telemetry.Activities.StartActivity("MongoAssetFolderRepository/RemoveAsync")) { - await Collection.DeleteOneAsync(x => x.DocumentId == key); + await Collection.DeleteOneAsync(x => x.DocumentId == key, ct); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs index 724d19f99..8382da768 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; @@ -39,7 +40,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets } protected override Task SetupCollectionAsync(IMongoCollection collection, - CancellationToken ct = default) + CancellationToken ct) { return collection.Indexes.CreateManyAsync(new[] { @@ -142,7 +143,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets return ResultList.Create(assetTotal, assetEntities); } } - catch (MongoQueryException ex) when (ex.Message.Contains("17406")) + catch (MongoQueryException ex) when (ex.Message.Contains("17406", StringComparison.Ordinal)) { throw new DomainException(T.Get("common.resultTooLarge")); } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs index 37c4d3ee8..bf5709757 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs @@ -5,13 +5,13 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using MongoDB.Bson; using MongoDB.Driver; +using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Assets.DomainObject; using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; @@ -20,15 +20,28 @@ using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.MongoDb.Assets { - public sealed partial class MongoAssetRepository : ISnapshotStore + public sealed partial class MongoAssetRepository : ISnapshotStore, IDeleter { - async Task<(AssetDomainObject.State Value, bool Valid, long Version)> ISnapshotStore.ReadAsync(DomainId key) + Task IDeleter.DeleteAppAsync(IAppEntity app, + CancellationToken ct) + { + return Collection.DeleteManyAsync(Filter.Eq(x => x.IndexedAppId, app.Id), ct); + } + + IAsyncEnumerable<(AssetDomainObject.State State, long Version)> ISnapshotStore.ReadAllAsync( + CancellationToken ct) + { + return Collection.Find(new BsonDocument(), Batching.Options).ToAsyncEnumerable(ct).Select(x => (Map(x), x.Version)); + } + + async Task<(AssetDomainObject.State Value, bool Valid, long Version)> ISnapshotStore.ReadAsync(DomainId key, + CancellationToken ct) { using (Telemetry.Activities.StartActivity("MongoAssetRepository/ReadAsync")) { var existing = await Collection.Find(x => x.DocumentId == key) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); if (existing != null) { @@ -39,17 +52,19 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets } } - async Task ISnapshotStore.WriteAsync(DomainId key, AssetDomainObject.State value, long oldVersion, long newVersion) + async Task ISnapshotStore.WriteAsync(DomainId key, AssetDomainObject.State value, long oldVersion, long newVersion, + CancellationToken ct) { using (Telemetry.Activities.StartActivity("MongoAssetRepository/WriteAsync")) { var entity = Map(value); - await Collection.UpsertVersionedAsync(key, oldVersion, newVersion, entity); + await Collection.UpsertVersionedAsync(key, oldVersion, newVersion, entity, ct); } } - async Task ISnapshotStore.WriteManyAsync(IEnumerable<(DomainId Key, AssetDomainObject.State Value, long Version)> snapshots) + async Task ISnapshotStore.WriteManyAsync(IEnumerable<(DomainId Key, AssetDomainObject.State Value, long Version)> snapshots, + CancellationToken ct) { using (Telemetry.Activities.StartActivity("MongoAssetRepository/WriteManyAsync")) { @@ -66,24 +81,16 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets return; } - await Collection.BulkWriteAsync(updates, BulkUnordered); + await Collection.BulkWriteAsync(updates, BulkUnordered, ct); } } - async Task ISnapshotStore.ReadAllAsync(Func callback, + async Task ISnapshotStore.RemoveAsync(DomainId key, CancellationToken ct) - { - using (Telemetry.Activities.StartActivity("MongoAssetRepository/ReadAllAsync")) - { - await Collection.Find(new BsonDocument(), Batching.Options).ForEachAsync(x => callback(Map(x), x.Version), ct); - } - } - - async Task ISnapshotStore.RemoveAsync(DomainId key) { using (Telemetry.Activities.StartActivity("MongoAssetRepository/RemoveAsync")) { - await Collection.DeleteOneAsync(x => x.DocumentId == key); + await Collection.DeleteOneAsync(x => x.DocumentId == key, ct); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs index 76d7b898f..0cf7a2528 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs @@ -103,6 +103,21 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents return queryAsStream.StreamAll(appId, schemaIds, ct); } + public IAsyncEnumerable QueryScheduledWithoutDataAsync(Instant now, + CancellationToken ct) + { + return queryScheduled.QueryAsync(now, ct); + } + + public async Task DeleteAppAsync(DomainId appId, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("MongoContentCollection/DeleteAppAsync")) + { + await Collection.DeleteManyAsync(Filter.Eq(x => x.IndexedAppId, appId), ct); + } + } + public async Task> QueryAsync(IAppEntity app, List schemas, Q q, CancellationToken ct) { @@ -165,15 +180,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents } } - public async Task QueryScheduledWithoutDataAsync(Instant now, Func callback, - CancellationToken ct) - { - using (Telemetry.Activities.StartActivity("MongoContentCollection/QueryScheduledWithoutDataAsync")) - { - await queryScheduled.QueryAsync(now, callback, ct); - } - } - public async Task> QueryIdsAsync(DomainId appId, HashSet ids, CancellationToken ct) { @@ -201,24 +207,28 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents } } - public async Task FindVersionAsync(DomainId documentId) + public async Task FindVersionAsync(DomainId documentId, + CancellationToken ct = default) { - var result = await Collection.Find(x => x.DocumentId == documentId).Only(x => x.Version).FirstOrDefaultAsync(); + var result = await Collection.Find(x => x.DocumentId == documentId).Only(x => x.Version).FirstOrDefaultAsync(ct); return result?["vs"].AsInt64 ?? EtagVersion.Empty; } - public Task UpsertVersionedAsync(DomainId documentId, long oldVersion, MongoContentEntity entity) + public Task UpsertVersionedAsync(DomainId documentId, long oldVersion, MongoContentEntity entity, + CancellationToken ct = default) { - return Collection.UpsertVersionedAsync(documentId, oldVersion, entity.Version, entity); + return Collection.UpsertVersionedAsync(documentId, oldVersion, entity.Version, entity, ct); } - public Task RemoveAsync(DomainId documentId) + public Task RemoveAsync(DomainId documentId, + CancellationToken ct = default) { - return Collection.DeleteOneAsync(x => x.DocumentId == documentId); + return Collection.DeleteOneAsync(x => x.DocumentId == documentId, ct); } - public Task InsertManyAsync(IReadOnlyList entities) + public Task InsertManyAsync(IReadOnlyList entities, + CancellationToken ct = default) { if (entities.Count == 0) { @@ -232,7 +242,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents IsUpsert = true }).ToList(); - return Collection.BulkWriteAsync(writes, BulkUnordered); + return Collection.BulkWriteAsync(writes, BulkUnordered, ct); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index 51ea67dea..863eb862e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -47,7 +47,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents this.appProvider = appProvider; } - public async Task InitializeAsync(CancellationToken ct = default) + public async Task InitializeAsync( + CancellationToken ct) { await collectionAll.InitializeAsync(ct); await collectionPublished.InitializeAsync(ct); @@ -59,6 +60,12 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents return collectionAll.StreamAll(appId, schemaIds, ct); } + public IAsyncEnumerable QueryScheduledWithoutDataAsync(Instant now, + CancellationToken ct = default) + { + return collectionAll.QueryScheduledWithoutDataAsync(now, ct); + } + public Task> QueryAsync(IAppEntity app, List schemas, Q q, SearchScope scope, CancellationToken ct = default) { @@ -130,12 +137,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents return collectionAll.ResetScheduledAsync(documentId, ct); } - public Task QueryScheduledWithoutDataAsync(Instant now, Func callback, - CancellationToken ct = default) - { - return collectionAll.QueryScheduledWithoutDataAsync(now, callback, ct); - } - public Task> QueryIdsAsync(DomainId appId, DomainId schemaId, FilterNode filterNode, CancellationToken ct = default) { diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs index c1f9aa78c..7fbf8027c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs @@ -5,12 +5,13 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.ExtractReferenceIds; +using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Contents.DomainObject; using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; @@ -18,43 +19,57 @@ using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.MongoDb.Contents { - public partial class MongoContentRepository : ISnapshotStore + public partial class MongoContentRepository : ISnapshotStore, IDeleter { - Task ISnapshotStore.ReadAllAsync(Func callback, + IAsyncEnumerable<(ContentDomainObject.State State, long Version)> ISnapshotStore.ReadAllAsync( CancellationToken ct) { - return Task.CompletedTask; + return AsyncEnumerable.Empty<(ContentDomainObject.State State, long Version)>(); } - async Task<(ContentDomainObject.State Value, bool Valid, long Version)> ISnapshotStore.ReadAsync(DomainId key) + async Task<(ContentDomainObject.State Value, bool Valid, long Version)> ISnapshotStore.ReadAsync(DomainId key, + CancellationToken ct) { using (Telemetry.Activities.StartActivity("MongoContentRepository/ReadAsync")) { - var version = await collectionAll.FindVersionAsync(key); + var version = await collectionAll.FindVersionAsync(key, ct); return (null!, false, version); } } - async Task ISnapshotStore.ClearAsync() + async Task IDeleter.DeleteAppAsync(IAppEntity app, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("MongoContentRepository/DeleteAppAsync")) + { + await collectionAll.DeleteAppAsync(app.Id, ct); + await collectionPublished.DeleteAppAsync(app.Id, ct); + } + } + + async Task ISnapshotStore.ClearAsync( + CancellationToken ct) { using (Telemetry.Activities.StartActivity("MongoContentRepository/ClearAsync")) { - await collectionAll.ClearAsync(); - await collectionPublished.ClearAsync(); + await collectionAll.ClearAsync(ct); + await collectionPublished.ClearAsync(ct); } } - async Task ISnapshotStore.RemoveAsync(DomainId key) + async Task ISnapshotStore.RemoveAsync(DomainId key, + CancellationToken ct) { using (Telemetry.Activities.StartActivity("MongoContentRepository/RemoveAsync")) { - await collectionAll.RemoveAsync(key); - await collectionPublished.RemoveAsync(key); + await collectionAll.RemoveAsync(key, ct); + await collectionPublished.RemoveAsync(key, ct); } } - async Task ISnapshotStore.WriteAsync(DomainId key, ContentDomainObject.State value, long oldVersion, long newVersion) + async Task ISnapshotStore.WriteAsync(DomainId key, ContentDomainObject.State value, long oldVersion, long newVersion, + CancellationToken ct) { using (Telemetry.Activities.StartActivity("MongoContentRepository/WriteAsync")) { @@ -64,12 +79,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents } await Task.WhenAll( - UpsertDraftContentAsync(value, oldVersion, newVersion), - UpsertOrDeletePublishedAsync(value, oldVersion, newVersion)); + UpsertDraftContentAsync(value, oldVersion, newVersion, ct), + UpsertOrDeletePublishedAsync(value, oldVersion, newVersion, ct)); } } - async Task ISnapshotStore.WriteManyAsync(IEnumerable<(DomainId Key, ContentDomainObject.State Value, long Version)> snapshots) + async Task ISnapshotStore.WriteManyAsync(IEnumerable<(DomainId Key, ContentDomainObject.State Value, long Version)> snapshots, + CancellationToken ct) { using (Telemetry.Activities.StartActivity("MongoContentRepository/WriteManyAsync")) { @@ -87,42 +103,46 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents } await Task.WhenAll( - collectionPublished.InsertManyAsync(entitiesPublished), - collectionAll.InsertManyAsync(entitiesAll)); + collectionPublished.InsertManyAsync(entitiesPublished, ct), + collectionAll.InsertManyAsync(entitiesAll, ct)); } } - private async Task UpsertOrDeletePublishedAsync(ContentDomainObject.State value, long oldVersion, long newVersion) + private async Task UpsertOrDeletePublishedAsync(ContentDomainObject.State value, long oldVersion, long newVersion, + CancellationToken ct = default) { if (ShouldWritePublished(value)) { - await UpsertPublishedContentAsync(value, oldVersion, newVersion); + await UpsertPublishedContentAsync(value, oldVersion, newVersion, ct); } else { - await DeletePublishedContentAsync(value.AppId.Id, value.Id); + await DeletePublishedContentAsync(value.AppId.Id, value.Id, ct); } } - private Task DeletePublishedContentAsync(DomainId appId, DomainId id) + private Task DeletePublishedContentAsync(DomainId appId, DomainId id, + CancellationToken ct = default) { var documentId = DomainId.Combine(appId, id); - return collectionPublished.RemoveAsync(documentId); + return collectionPublished.RemoveAsync(documentId, ct); } - private async Task UpsertDraftContentAsync(ContentDomainObject.State value, long oldVersion, long newVersion) + private async Task UpsertDraftContentAsync(ContentDomainObject.State value, long oldVersion, long newVersion, + CancellationToken ct = default) { var entity = await CreateDraftContentAsync(value, newVersion); - await collectionAll.UpsertVersionedAsync(entity.DocumentId, oldVersion, entity); + await collectionAll.UpsertVersionedAsync(entity.DocumentId, oldVersion, entity, ct); } - private async Task UpsertPublishedContentAsync(ContentDomainObject.State value, long oldVersion, long newVersion) + private async Task UpsertPublishedContentAsync(ContentDomainObject.State value, long oldVersion, long newVersion, + CancellationToken ct = default) { var entity = await CreatePublishedContentAsync(value, newVersion); - await collectionPublished.UpsertVersionedAsync(entity.DocumentId, oldVersion, entity); + await collectionPublished.UpsertVersionedAsync(entity.DocumentId, oldVersion, entity, ct); } private async Task CreatePublishedContentAsync(ContentDomainObject.State value, long newVersion) diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByIds.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByIds.cs index 273ba234d..42f67bfc6 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByIds.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByIds.cs @@ -88,7 +88,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations } else { - var first = documentIds.First(); + var first = documentIds[0]; filters.Add( Filter.Or( diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByQuery.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByQuery.cs index 201bcc344..c4cb3a9c2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByQuery.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryByQuery.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -64,7 +65,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations try { - var schema = await appProvider.GetSchemaAsync(appId, schemaId); + var schema = await appProvider.GetSchemaAsync(appId, schemaId, ct: ct); if (schema == null) { @@ -81,7 +82,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations { throw new DomainException(T.Get("common.resultTooLarge")); } - catch (MongoQueryException ex) when (ex.Message.Contains("17406")) + catch (MongoQueryException ex) when (ex.Message.Contains("17406", StringComparison.Ordinal)) { throw new DomainException(T.Get("common.resultTooLarge")); } @@ -117,7 +118,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations { throw new DomainException(T.Get("common.resultTooLarge")); } - catch (MongoQueryException ex) when (ex.Message.Contains("17406")) + catch (MongoQueryException ex) when (ex.Message.Contains("17406", StringComparison.Ordinal)) { throw new DomainException(T.Get("common.resultTooLarge")); } @@ -154,7 +155,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations { throw new DomainException(T.Get("common.resultTooLarge")); } - catch (MongoQueryException ex) when (ex.Message.Contains("17406")) + catch (MongoQueryException ex) when (ex.Message.Contains("17406", StringComparison.Ordinal)) { throw new DomainException(T.Get("common.resultTooLarge")); } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferences.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferences.cs index 4cb04cfb7..2f9bbdb41 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferences.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferences.cs @@ -16,7 +16,7 @@ using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations { - internal class QueryReferences : OperationBase + internal sealed class QueryReferences : OperationBase { private static readonly IResultList EmptyIds = ResultList.CreateFrom(0); private readonly QueryByIds queryByIds; diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryScheduled.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryScheduled.cs index cac31b146..12fdc8fcf 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryScheduled.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryScheduled.cs @@ -48,16 +48,12 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations return ResultList.Create(contentTotal, contentEntities); } - public Task QueryAsync(Instant now, Func callback, + public IAsyncEnumerable QueryAsync(Instant now, CancellationToken ct) { - Guard.NotNull(callback, nameof(callback)); - - return Collection.Find(x => x.ScheduledAt < now && x.IsDeleted != true).Not(x => x.Data) - .ForEachAsync(c => - { - callback(c); - }, ct); +#pragma warning disable MA0073 // Avoid comparison with bool constant + return Collection.Find(x => x.ScheduledAt < now && x.IsDeleted != true).Not(x => x.Data).ToAsyncEnumerable(ct); +#pragma warning restore MA0073 // Avoid comparison with bool constant } private static FilterDefinition CreateFilter(DomainId appId, IEnumerable schemaIds, Instant scheduledFrom, Instant scheduledTo) diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/FullText/MongoTextIndex.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/FullText/MongoTextIndex.cs index 43779a8ee..ac7ac868e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/FullText/MongoTextIndex.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/FullText/MongoTextIndex.cs @@ -19,7 +19,7 @@ using Squidex.Infrastructure.Tasks; namespace Squidex.Domain.Apps.Entities.MongoDb.FullText { - public sealed class MongoTextIndex : MongoRepositoryBase, ITextIndex + public sealed class MongoTextIndex : MongoRepositoryBase, ITextIndex, IDeleter { private const int Limit = 2000; private const int LimitHalf = 1000; @@ -62,7 +62,14 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.FullText return "TextIndex"; } - public Task ExecuteAsync(params IndexCommand[] commands) + async Task IDeleter.DeleteAppAsync(IAppEntity app, + CancellationToken ct) + { + await Collection.DeleteManyAsync(Filter.Eq(x => x.AppId, app.Id), ct); + } + + public Task ExecuteAsync(IndexCommand[] commands, + CancellationToken ct = default) { var writes = new List>(commands.Length); @@ -76,27 +83,30 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.FullText return Task.CompletedTask; } - return Collection.BulkWriteAsync(writes, BulkUnordered); + return Collection.BulkWriteAsync(writes, BulkUnordered, ct); } - public async Task?> SearchAsync(IAppEntity app, GeoQuery query, SearchScope scope) + public async Task?> SearchAsync(IAppEntity app, GeoQuery query, SearchScope scope, + CancellationToken ct = default) { + var findFilter = + Filter.And( + Filter.Eq(x => x.AppId, app.Id), + Filter.Eq(x => x.SchemaId, query.SchemaId), + Filter_ByScope(scope), + Filter.GeoWithinCenterSphere(x => x.GeoObject, query.Longitude, query.Latitude, query.Radius / 6378100)); + var byGeo = - await GetCollection(scope).Find( - Filter.And( - Filter.Eq(x => x.AppId, app.Id), - Filter.Eq(x => x.SchemaId, query.SchemaId), - Filter_ByScope(scope), - Filter.GeoWithinCenterSphere(x => x.GeoObject, query.Longitude, query.Latitude, query.Radius / 6378100))) - .Limit(Limit).Only(x => x.ContentId) - .ToListAsync(); + await GetCollection(scope).Find(findFilter).Limit(Limit).Only(x => x.ContentId) + .ToListAsync(ct); var field = Field.Of(x => nameof(x.ContentId)); return byGeo.Select(x => DomainId.Create(x[field].AsString)).Distinct().ToList(); } - public async Task?> SearchAsync(IAppEntity app, TextQuery query, SearchScope scope) + public async Task?> SearchAsync(IAppEntity app, TextQuery query, SearchScope scope, + CancellationToken ct = default) { var (queryText, filter) = query; @@ -107,50 +117,54 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.FullText if (filter == null) { - return await SearchByAppAsync(queryText, app, scope, Limit); + return await SearchByAppAsync(queryText, app, scope, Limit, ct); } else if (filter.Must) { - return await SearchBySchemaAsync(queryText, app, filter, scope, Limit); + return await SearchBySchemaAsync(queryText, app, filter, scope, Limit, ct); } else { var (bySchema, byApp) = await AsyncHelper.WhenAll( - SearchBySchemaAsync(queryText, app, filter, scope, LimitHalf), - SearchByAppAsync(queryText, app, scope, LimitHalf)); + SearchBySchemaAsync(queryText, app, filter, scope, LimitHalf, ct), + SearchByAppAsync(queryText, app, scope, LimitHalf, ct)); return bySchema.Union(byApp).Distinct().ToList(); } } - private async Task> SearchBySchemaAsync(string queryText, IAppEntity app, TextFilter filter, SearchScope scope, int limit) + private async Task> SearchBySchemaAsync(string queryText, IAppEntity app, TextFilter filter, SearchScope scope, int limit, + CancellationToken ct = default) { + var findFilter = + Filter.And( + Filter.Eq(x => x.AppId, app.Id), + Filter.In(x => x.SchemaId, filter.SchemaIds), + Filter_ByScope(scope), + Filter.Text(queryText, "none")); + var bySchema = - await GetCollection(scope).Find( - Filter.And( - Filter.Eq(x => x.AppId, app.Id), - Filter.In(x => x.SchemaId, filter.SchemaIds), - Filter_ByScope(scope), - Filter.Text(queryText, "none"))) - .Limit(limit).Only(x => x.ContentId) - .ToListAsync(); + await GetCollection(scope).Find(findFilter).Limit(limit).Only(x => x.ContentId) + .ToListAsync(ct); var field = Field.Of(x => nameof(x.ContentId)); return bySchema.Select(x => DomainId.Create(x[field].AsString)).Distinct().ToList(); } - private async Task> SearchByAppAsync(string queryText, IAppEntity app, SearchScope scope, int limit) + private async Task> SearchByAppAsync(string queryText, IAppEntity app, SearchScope scope, int limit, + CancellationToken ct = default) { + var findFilter = + Filter.And( + Filter.Eq(x => x.AppId, app.Id), + Filter.Exists(x => x.SchemaId), + Filter_ByScope(scope), + Filter.Text(queryText, "none")); + var bySchema = - await GetCollection(scope).Find( - Filter.And( - Filter.Eq(x => x.AppId, app.Id), - Filter.Exists(x => x.SchemaId), - Filter_ByScope(scope), - Filter.Text(queryText, "none"))) - .Limit(limit).Only(x => x.ContentId) - .ToListAsync(); + await GetCollection(scope).Find(findFilter).Limit(limit).Only(x => x.ContentId) + .ToListAsync(ct); var field = Field.Of(x => nameof(x.ContentId)); diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/FullText/MongoTextIndexerState.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/FullText/MongoTextIndexerState.cs index a59e40ab3..68ab9cbbb 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/FullText/MongoTextIndexerState.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/FullText/MongoTextIndexerState.cs @@ -7,16 +7,18 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using MongoDB.Bson.Serialization; using MongoDB.Driver; +using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Contents.Text.State; using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; namespace Squidex.Domain.Apps.Entities.MongoDb.FullText { - public sealed class MongoTextIndexerState : MongoRepositoryBase, ITextIndexerState + public sealed class MongoTextIndexerState : MongoRepositoryBase, ITextIndexerState, IDeleter { static MongoTextIndexerState() { @@ -24,6 +26,9 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.FullText { cm.MapIdField(x => x.UniqueContentId); + cm.MapProperty(x => x.AppId) + .SetElementName("a"); + cm.MapProperty(x => x.DocIdCurrent) .SetElementName("c"); @@ -40,19 +45,37 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.FullText { } + protected override Task SetupCollectionAsync(IMongoCollection collection, + CancellationToken ct) + { + return collection.Indexes.CreateManyAsync(new[] + { + new CreateIndexModel( + Index.Ascending(x => x.AppId)) + }, ct); + } + protected override string CollectionName() { return "TextIndexerState"; } - public async Task> GetAsync(HashSet ids) + async Task IDeleter.DeleteAppAsync(IAppEntity app, + CancellationToken ct) + { + await Collection.DeleteManyAsync(Filter.Eq(x => x.AppId, app.Id), ct); + } + + public async Task> GetAsync(HashSet ids, + CancellationToken ct = default) { - var entities = await Collection.Find(Filter.In(x => x.UniqueContentId, ids)).ToListAsync(); + var entities = await Collection.Find(Filter.In(x => x.UniqueContentId, ids)).ToListAsync(ct); return entities.ToDictionary(x => x.UniqueContentId); } - public Task SetAsync(List updates) + public Task SetAsync(List updates, + CancellationToken ct = default) { var writes = new List>(); @@ -80,7 +103,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.FullText return Task.CompletedTask; } - return Collection.BulkWriteAsync(writes, BulkUnordered); + return Collection.BulkWriteAsync(writes, BulkUnordered, ct); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs index c9db3c05d..fd7e63b28 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs @@ -11,6 +11,7 @@ using System.Threading; using System.Threading.Tasks; using MongoDB.Bson.Serialization; using MongoDB.Driver; +using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.History; using Squidex.Domain.Apps.Entities.History.Repositories; using Squidex.Infrastructure; @@ -18,7 +19,7 @@ using Squidex.Infrastructure.MongoDb; namespace Squidex.Domain.Apps.Entities.MongoDb.History { - public class MongoHistoryEventRepository : MongoRepositoryBase, IHistoryEventRepository + public sealed class MongoHistoryEventRepository : MongoRepositoryBase, IHistoryEventRepository, IDeleter { static MongoHistoryEventRepository() { @@ -42,7 +43,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.History } protected override Task SetupCollectionAsync(IMongoCollection collection, - CancellationToken ct = default) + CancellationToken ct) { return collection.Indexes.CreateManyAsync(new[] { @@ -60,19 +61,29 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.History }, ct); } - public async Task> QueryByChannelAsync(DomainId appId, string channelPrefix, int count) + async Task IDeleter.DeleteAppAsync(IAppEntity app, + CancellationToken ct) + { + await Collection.DeleteManyAsync(Filter.Eq(x => x.AppId, app.Id), ct); + } + + public async Task> QueryByChannelAsync(DomainId appId, string channelPrefix, int count, + CancellationToken ct = default) { if (!string.IsNullOrWhiteSpace(channelPrefix)) { - return await Collection.Find(x => x.AppId == appId && x.Channel == channelPrefix).SortByDescending(x => x.Created).ThenByDescending(x => x.Version).Limit(count).ToListAsync(); + return await Collection.Find(x => x.AppId == appId && x.Channel == channelPrefix) + .SortByDescending(x => x.Created).ThenByDescending(x => x.Version).Limit(count).ToListAsync(ct); } else { - return await Collection.Find(x => x.AppId == appId).SortByDescending(x => x.Created).ThenByDescending(x => x.Version).Limit(count).ToListAsync(); + return await Collection.Find(x => x.AppId == appId) + .SortByDescending(x => x.Created).ThenByDescending(x => x.Version).Limit(count).ToListAsync(ct); } } - public Task InsertManyAsync(IEnumerable historyEvents) + public Task InsertManyAsync(IEnumerable historyEvents, + CancellationToken ct = default) { var writes = historyEvents .Select(x => @@ -87,7 +98,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.History return Task.CompletedTask; } - return Collection.BulkWriteAsync(writes, BulkUnordered); + return Collection.BulkWriteAsync(writes, BulkUnordered, ct); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs new file mode 100644 index 000000000..d6cd8f1a5 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs @@ -0,0 +1,36 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using MongoDB.Bson.Serialization.Attributes; +using Squidex.Domain.Apps.Entities.Rules.DomainObject; +using Squidex.Infrastructure; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Rules +{ + public sealed class MongoRuleEntity : MongoState + { + [BsonRequired] + [BsonElement("_ai")] + public DomainId IndexedAppId { get; set; } + + [BsonRequired] + [BsonElement("_ri")] + public DomainId IndexedId { get; set; } + + [BsonRequired] + [BsonElement("_dl")] + public bool IndexedDeleted { get; set; } + + public override void Prepare() + { + IndexedAppId = Document.AppId.Id; + IndexedDeleted = Document.IsDeleted; + IndexedId = Document.Id; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventEntity.cs index 1dc815528..6daccd47c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventEntity.cs @@ -13,6 +13,7 @@ using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Entities.Rules; using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; +using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.MongoDb.Rules { @@ -21,7 +22,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules { [BsonId] [BsonElement] - public DomainId DocumentId { get; set; } + public DomainId JobId { get; set; } [BsonRequired] [BsonElement] @@ -72,12 +73,26 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules DomainId IEntity.Id { - get => DocumentId; + get => JobId; } DomainId IEntity.UniqueId { - get => DocumentId; + get => JobId; + } + + public static MongoRuleEventEntity FromJob(RuleJob job, Instant? nextAttempt) + { + var entity = new MongoRuleEventEntity + { + Job = job, + JobId = job.Id, + NextAttempt = nextAttempt + }; + + SimpleMapper.Map(job, entity); + + return entity; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs index a3157130a..b0975ba20 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEventRepository.cs @@ -13,15 +13,15 @@ using MongoDB.Driver; using NodaTime; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; +using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Rules.Repositories; using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; -using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.MongoDb.Rules { - public sealed class MongoRuleEventRepository : MongoRepositoryBase, IRuleEventRepository + public sealed class MongoRuleEventRepository : MongoRepositoryBase, IRuleEventRepository, IDeleter { private readonly MongoRuleStatisticsCollection statisticsCollection; @@ -37,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules } protected override async Task SetupCollectionAsync(IMongoCollection collection, - CancellationToken ct = default) + CancellationToken ct) { await statisticsCollection.InitializeAsync(ct); @@ -59,12 +59,22 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules }, ct); } - public Task QueryPendingAsync(Instant now, Func callback, CancellationToken ct = default) + async Task IDeleter.DeleteAppAsync(IAppEntity app, + CancellationToken ct) + { + await statisticsCollection.DeleteAppAsync(app.Id, ct); + + await Collection.DeleteManyAsync(Filter.Eq(x => x.AppId, app.Id), ct); + } + + public Task QueryPendingAsync(Instant now, Func callback, + CancellationToken ct = default) { return Collection.Find(x => x.NextAttempt < now).ForEachAsync(callback, ct); } - public async Task> QueryByAppAsync(DomainId appId, DomainId? ruleId = null, int skip = 0, int take = 20, CancellationToken ct = default) + public async Task> QueryByAppAsync(DomainId appId, DomainId? ruleId = null, int skip = 0, int take = 20, + CancellationToken ct = default) { var filter = Filter.Eq(x => x.AppId, appId); @@ -84,89 +94,99 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules return ResultList.Create(ruleEventTotal, ruleEventEntities); } - public async Task FindAsync(DomainId id, CancellationToken ct = default) + public async Task FindAsync(DomainId id, + CancellationToken ct = default) { var ruleEvent = - await Collection.Find(x => x.DocumentId == id) + await Collection.Find(x => x.JobId == id) .FirstOrDefaultAsync(ct); return ruleEvent; } - public Task EnqueueAsync(DomainId id, Instant nextAttempt) + public Task EnqueueAsync(DomainId id, Instant nextAttempt, + CancellationToken ct = default) { - return Collection.UpdateOneAsync(x => x.DocumentId == id, Update.Set(x => x.NextAttempt, nextAttempt)); + return Collection.UpdateOneAsync(x => x.JobId == id, Update.Set(x => x.NextAttempt, nextAttempt), cancellationToken: ct); } - public async Task EnqueueAsync(RuleJob job, Instant? nextAttempt) + public async Task EnqueueAsync(RuleJob job, Instant? nextAttempt, + CancellationToken ct = default) { - var entity = new MongoRuleEventEntity { Job = job, Created = job.Created, NextAttempt = nextAttempt }; + var entity = MongoRuleEventEntity.FromJob(job, nextAttempt); - SimpleMapper.Map(job, entity); - - entity.DocumentId = job.Id; - - await Collection.InsertOneIfNotExistsAsync(entity); + await Collection.InsertOneIfNotExistsAsync(entity, ct); } - public Task CancelByEventAsync(DomainId id) + public Task CancelByEventAsync(DomainId id, + CancellationToken ct = default) { - return Collection.UpdateOneAsync(x => x.DocumentId == id, + return Collection.UpdateOneAsync(x => x.JobId == id, Update .Set(x => x.NextAttempt, null) - .Set(x => x.JobResult, RuleJobResult.Cancelled)); + .Set(x => x.JobResult, RuleJobResult.Cancelled), + cancellationToken: ct); } - public Task CancelByRuleAsync(DomainId ruleId) + public Task CancelByRuleAsync(DomainId ruleId, + CancellationToken ct = default) { return Collection.UpdateManyAsync(x => x.RuleId == ruleId, Update .Set(x => x.NextAttempt, null) - .Set(x => x.JobResult, RuleJobResult.Cancelled)); + .Set(x => x.JobResult, RuleJobResult.Cancelled), + cancellationToken: ct); } - public Task CancelByAppAsync(DomainId appId) + public Task CancelByAppAsync(DomainId appId, + CancellationToken ct = default) { return Collection.UpdateManyAsync(x => x.AppId == appId, Update .Set(x => x.NextAttempt, null) - .Set(x => x.JobResult, RuleJobResult.Cancelled)); + .Set(x => x.JobResult, RuleJobResult.Cancelled), + cancellationToken: ct); } - public Task UpdateAsync(RuleJob job, RuleJobUpdate update) + public Task UpdateAsync(RuleJob job, RuleJobUpdate update, + CancellationToken ct = default) { Guard.NotNull(job, nameof(job)); Guard.NotNull(update, nameof(update)); return Task.WhenAll( - UpdateStatisticsAsync(job, update), - UpdateEventAsync(job, update)); + UpdateStatisticsAsync(job, update, ct), + UpdateEventAsync(job, update, ct)); } - private Task UpdateEventAsync(RuleJob job, RuleJobUpdate update) + private Task UpdateEventAsync(RuleJob job, RuleJobUpdate update, + CancellationToken ct = default) { - return Collection.UpdateOneAsync(x => x.DocumentId == job.Id, + return Collection.UpdateOneAsync(x => x.JobId == job.Id, Update .Set(x => x.Result, update.ExecutionResult) .Set(x => x.LastDump, update.ExecutionDump) .Set(x => x.JobResult, update.JobResult) .Set(x => x.NextAttempt, update.JobNext) - .Inc(x => x.NumCalls, 1)); + .Inc(x => x.NumCalls, 1), + cancellationToken: ct); } - private async Task UpdateStatisticsAsync(RuleJob job, RuleJobUpdate update) + private async Task UpdateStatisticsAsync(RuleJob job, RuleJobUpdate update, + CancellationToken ct = default) { if (update.ExecutionResult == RuleResult.Success) { - await statisticsCollection.IncrementSuccess(job.AppId, job.RuleId, update.Finished); + await statisticsCollection.IncrementSuccessAsync(job.AppId, job.RuleId, update.Finished, ct); } else { - await statisticsCollection.IncrementFailed(job.AppId, job.RuleId, update.Finished); + await statisticsCollection.IncrementFailedAsync(job.AppId, job.RuleId, update.Finished, ct); } } - public Task> QueryStatisticsByAppAsync(DomainId appId, CancellationToken ct = default) + public Task> QueryStatisticsByAppAsync(DomainId appId, + CancellationToken ct = default) { return statisticsCollection.QueryByAppAsync(appId, ct); } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs new file mode 100644 index 000000000..87670b87e --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs @@ -0,0 +1,58 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Driver; +using Newtonsoft.Json; +using Squidex.Domain.Apps.Entities.Apps; +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) + { + } + + protected override Task SetupCollectionAsync(IMongoCollection collection, + CancellationToken ct) + { + return collection.Indexes.CreateManyAsync(new[] + { + new CreateIndexModel( + Index + .Ascending(x => x.IndexedAppId)) + }, ct); + } + + Task IDeleter.DeleteAppAsync(IAppEntity app, + CancellationToken ct) + { + return Collection.DeleteManyAsync(Filter.Eq(x => x.IndexedAppId, app.Id), ct); + } + + public async Task> QueryIdsAsync(DomainId appId, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoSchemaRepository/QueryIdsAsync")) + { + var entities = await Collection.Find(x => x.IndexedAppId == appId && !x.IndexedDeleted).Only(x => x.IndexedId).ToListAsync(ct); + + return entities.Select(x => DomainId.Create(x["_ri"].AsString)).ToList(); + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleStatisticsCollection.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleStatisticsCollection.cs index 6ee7d76e9..0333faaab 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleStatisticsCollection.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleStatisticsCollection.cs @@ -50,6 +50,12 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules cancellationToken: ct); } + public async Task DeleteAppAsync(DomainId appId, + CancellationToken ct) + { + await Collection.DeleteManyAsync(Filter.Eq(x => x.AppId, appId), ct); + } + public async Task> QueryByAppAsync(DomainId appId, CancellationToken ct) { @@ -58,7 +64,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules return statistics; } - public Task IncrementSuccess(DomainId appId, DomainId ruleId, Instant now) + public Task IncrementSuccessAsync(DomainId appId, DomainId ruleId, Instant now, + CancellationToken ct) { return Collection.UpdateOneAsync( x => x.AppId == appId && x.RuleId == ruleId, @@ -67,10 +74,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules .Set(x => x.LastExecuted, now) .SetOnInsert(x => x.AppId, appId) .SetOnInsert(x => x.RuleId, ruleId), - Upsert); + Upsert, ct); } - public Task IncrementFailed(DomainId appId, DomainId ruleId, Instant now) + public Task IncrementFailedAsync(DomainId appId, DomainId ruleId, Instant now, + CancellationToken ct) { return Collection.UpdateOneAsync( x => x.AppId == appId && x.RuleId == ruleId, @@ -79,7 +87,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules .Set(x => x.LastExecuted, now) .SetOnInsert(x => x.AppId, appId) .SetOnInsert(x => x.RuleId, ruleId), - Upsert); + Upsert, ct); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs new file mode 100644 index 000000000..f1c754dcb --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs @@ -0,0 +1,41 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using MongoDB.Bson.Serialization.Attributes; +using Squidex.Domain.Apps.Entities.Schemas.DomainObject; +using Squidex.Infrastructure; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas +{ + public sealed class MongoSchemaEntity : MongoState + { + [BsonRequired] + [BsonElement("_ai")] + public DomainId IndexedAppId { get; set; } + + [BsonRequired] + [BsonElement("_si")] + public DomainId IndexedId { get; set; } + + [BsonRequired] + [BsonElement("_sn")] + public string IndexedName { get; set; } + + [BsonRequired] + [BsonElement("_dl")] + public bool IndexedDeleted { get; set; } + + public override void Prepare() + { + IndexedAppId = Document.AppId.Id; + IndexedDeleted = Document.IsDeleted; + IndexedId = Document.Id; + IndexedName = Document.SchemaDef.Name; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs new file mode 100644 index 000000000..0ddcebd38 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs @@ -0,0 +1,73 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Driver; +using Newtonsoft.Json; +using Squidex.Domain.Apps.Entities.Apps; +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) + { + } + + protected override Task SetupCollectionAsync(IMongoCollection collection, + CancellationToken ct) + { + return collection.Indexes.CreateManyAsync(new[] + { + new CreateIndexModel( + Index + .Ascending(x => x.IndexedAppId) + .Ascending(x => x.IndexedName)) + }, ct); + } + + Task IDeleter.DeleteAppAsync(IAppEntity app, + CancellationToken ct) + { + return Collection.DeleteManyAsync(Filter.Eq(x => x.IndexedAppId, app.Id), ct); + } + + public async Task> QueryIdsAsync(DomainId appId, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoSchemaRepository/QueryAsync")) + { + var find = Collection.Find(x => x.IndexedAppId == appId && !x.IndexedDeleted); + + return await QueryAsync(find, ct); + } + } + + private static async Task> QueryAsync(IFindFluent find, + CancellationToken ct) + { + var entities = await find.Only(x => x.IndexedId, x => x.IndexedName).ToListAsync(ct); + + return entities.Select(x => + { + var indexedId = DomainId.Create(x["_si"].AsString); + var indexedName = x["_sn"].AsString; + + return new { indexedName, indexedId }; + }).ToDictionary(x => x.indexedName, x => x.indexedId); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemasHash.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemasHash.cs index dd6f428b3..96668bdad 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemasHash.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemasHash.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using MongoDB.Driver; using NodaTime; @@ -20,7 +21,7 @@ using Squidex.Infrastructure.ObjectPool; namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas { - public sealed class MongoSchemasHash : MongoRepositoryBase, ISchemasHash, IEventConsumer + public sealed class MongoSchemasHash : MongoRepositoryBase, ISchemasHash, IEventConsumer, IDeleter { public int BatchSize { @@ -52,6 +53,12 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas return "SchemasHash"; } + async Task IDeleter.DeleteAppAsync(IAppEntity app, + CancellationToken ct) + { + await Collection.DeleteManyAsync(Filter.Eq(x => x.AppId, app.Id), ct); + } + public Task On(IEnumerable> events) { var writes = new List>(); @@ -67,7 +74,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas { writes.Add( new UpdateOneModel( - Filter.Eq(x => x.AppId, schemaEvent.AppId.Id.ToString()), + Filter.Eq(x => x.AppId, schemaEvent.AppId.Id), Update .Set($"s.{schemaEvent.SchemaId.Id}", @event.Headers.EventStreamNumber()) .Set(x => x.Updated, @event.Headers.Timestamp())) @@ -85,11 +92,12 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas return Collection.BulkWriteAsync(writes, BulkUnordered); } - public async Task<(Instant Create, string Hash)> GetCurrentHashAsync(IAppEntity app) + public async Task<(Instant Create, string Hash)> GetCurrentHashAsync(IAppEntity app, + CancellationToken ct = default) { Guard.NotNull(app, nameof(app)); - var entity = await Collection.Find(x => x.AppId == app.Id.ToString()).FirstOrDefaultAsync(); + var entity = await Collection.Find(x => x.AppId == app.Id).FirstOrDefaultAsync(ct); if (entity == null) { @@ -105,7 +113,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas return (entity.Updated, hash); } - public ValueTask ComputeHashAsync(IAppEntity app, IEnumerable schemas) + public ValueTask ComputeHashAsync(IAppEntity app, IEnumerable schemas, + CancellationToken ct = default) { var ids = schemas.Select(x => (x.Id.ToString(), x.Version)) diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemasHashEntity.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemasHashEntity.cs index d7f830797..862d40dd7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemasHashEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemasHashEntity.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using MongoDB.Bson.Serialization.Attributes; using NodaTime; +using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas { @@ -16,7 +17,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas { [BsonId] [BsonElement] - public string AppId { get; set; } + public DomainId AppId { get; set; } [BsonRequired] [BsonElement("s")] diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj index ce81d17ca..03e3a4eea 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj @@ -17,6 +17,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs b/backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs index 793a98e45..8470b919e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Squidex.Caching; using Squidex.Domain.Apps.Entities.Apps; @@ -24,27 +25,29 @@ namespace Squidex.Domain.Apps.Entities { private readonly ILocalCache localCache; private readonly IAppsIndex indexForApps; - private readonly IRulesIndex indexRules; - private readonly ISchemasIndex indexSchemas; + private readonly IRulesIndex indexForRules; + private readonly ISchemasIndex indexForSchemas; - public AppProvider(ILocalCache localCache, IAppsIndex indexForApps, IRulesIndex indexRules, ISchemasIndex indexSchemas) + public AppProvider(IAppsIndex indexForApps, IRulesIndex indexForRules, ISchemasIndex indexForSchemas, + ILocalCache localCache) { this.localCache = localCache; this.indexForApps = indexForApps; - this.indexRules = indexRules; - this.indexSchemas = indexSchemas; + this.indexForRules = indexForRules; + this.indexForSchemas = indexForSchemas; } - public async Task<(IAppEntity?, ISchemaEntity?)> GetAppWithSchemaAsync(DomainId appId, DomainId id, bool canCache = false) + public async Task<(IAppEntity?, ISchemaEntity?)> GetAppWithSchemaAsync(DomainId appId, DomainId id, bool canCache = false, + CancellationToken ct = default) { - var app = await GetAppAsync(appId, canCache); + var app = await GetAppAsync(appId, canCache, ct); if (app == null) { return (null, null); } - var schema = await GetSchemaAsync(appId, id, canCache); + var schema = await GetSchemaAsync(appId, id, canCache, ct); if (schema == null) { @@ -54,7 +57,8 @@ namespace Squidex.Domain.Apps.Entities return (app, schema); } - public async Task GetAppAsync(DomainId appId, bool canCache = false) + public async Task GetAppAsync(DomainId appId, bool canCache = false, + CancellationToken ct = default) { var cacheKey = AppCacheKey(appId); @@ -63,7 +67,7 @@ namespace Squidex.Domain.Apps.Entities return found; } - var app = await indexForApps.GetAppAsync(appId, canCache); + var app = await indexForApps.GetAppAsync(appId, canCache, ct); if (app != null) { @@ -74,7 +78,8 @@ namespace Squidex.Domain.Apps.Entities return app; } - public async Task GetAppAsync(string appName, bool canCache = false) + public async Task GetAppAsync(string appName, bool canCache = false, + CancellationToken ct = default) { var cacheKey = AppCacheKey(appName); @@ -83,7 +88,7 @@ namespace Squidex.Domain.Apps.Entities return found; } - var app = await indexForApps.GetAppByNameAsync(appName, canCache); + var app = await indexForApps.GetAppAsync(appName, canCache, ct); if (app != null) { @@ -94,7 +99,8 @@ namespace Squidex.Domain.Apps.Entities return app; } - public async Task GetSchemaAsync(DomainId appId, string name, bool canCache = false) + public async Task GetSchemaAsync(DomainId appId, string name, bool canCache = false, + CancellationToken ct = default) { var cacheKey = SchemaCacheKey(appId, name); @@ -103,7 +109,7 @@ namespace Squidex.Domain.Apps.Entities return found; } - var schema = await indexSchemas.GetSchemaByNameAsync(appId, name, canCache); + var schema = await indexForSchemas.GetSchemaAsync(appId, name, canCache, ct); if (schema != null) { @@ -114,7 +120,8 @@ namespace Squidex.Domain.Apps.Entities return schema; } - public async Task GetSchemaAsync(DomainId appId, DomainId id, bool canCache = false) + public async Task GetSchemaAsync(DomainId appId, DomainId id, bool canCache = false, + CancellationToken ct = default) { var cacheKey = SchemaCacheKey(appId, id); @@ -123,7 +130,7 @@ namespace Squidex.Domain.Apps.Entities return found; } - var schema = await indexSchemas.GetSchemaAsync(appId, id, canCache); + var schema = await indexForSchemas.GetSchemaAsync(appId, id, canCache, ct); if (schema != null) { @@ -134,21 +141,23 @@ namespace Squidex.Domain.Apps.Entities return schema; } - public async Task> GetUserAppsAsync(string userId, PermissionSet permissions) + public async Task> GetUserAppsAsync(string userId, PermissionSet permissions, + CancellationToken ct = default) { var apps = await localCache.GetOrCreateAsync($"GetUserApps({userId})", () => { - return indexForApps.GetAppsForUserAsync(userId, permissions); + return indexForApps.GetAppsForUserAsync(userId, permissions, ct); }); return apps; } - public async Task> GetSchemasAsync(DomainId appId) + public async Task> GetSchemasAsync(DomainId appId, + CancellationToken ct = default) { var schemas = await localCache.GetOrCreateAsync($"GetSchemasAsync({appId})", () => { - return indexSchemas.GetSchemasAsync(appId); + return indexForSchemas.GetSchemasAsync(appId, ct); }); foreach (var schema in schemas) @@ -160,19 +169,21 @@ namespace Squidex.Domain.Apps.Entities return schemas; } - public async Task> GetRulesAsync(DomainId appId) + public async Task> GetRulesAsync(DomainId appId, + CancellationToken ct = default) { var rules = await localCache.GetOrCreateAsync($"GetRulesAsync({appId})", () => { - return indexRules.GetRulesAsync(appId); + return indexForRules.GetRulesAsync(appId, ct); }); return rules.ToList(); } - public async Task GetRuleAsync(DomainId appId, DomainId id) + public async Task GetRuleAsync(DomainId appId, DomainId id, + CancellationToken ct = default) { - var rules = await GetRulesAsync(appId); + var rules = await GetRulesAsync(appId, ct); return rules.Find(x => x.Id == id); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/AppProviderExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/AppProviderExtensions.cs index e726584be..a5d8943be 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/AppProviderExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/AppProviderExtensions.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Entities.Schemas; @@ -16,7 +17,8 @@ namespace Squidex.Domain.Apps.Entities { public static class AppProviderExtensions { - public static async Task GetComponentsAsync(this IAppProvider appProvider, ISchemaEntity schema) + public static async Task GetComponentsAsync(this IAppProvider appProvider, ISchemaEntity schema, + CancellationToken ct = default) { Dictionary? result = null; @@ -35,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities } else if (result == null || !result.TryGetValue(schemaId, out _)) { - var resolvedEntity = await appProvider.GetSchemaAsync(appId, schemaId, false); + var resolvedEntity = await appProvider.GetSchemaAsync(appId, schemaId, false, ct); if (resolvedEntity != null) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppEventDeleter.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppEventDeleter.cs new file mode 100644 index 000000000..7cc36d7cb --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppEventDeleter.cs @@ -0,0 +1,31 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public sealed class AppEventDeleter : IDeleter + { + private readonly IEventStore eventStore; + + public int Order => int.MaxValue; + + public AppEventDeleter(IEventStore eventStore) + { + this.eventStore = eventStore; + } + + public Task DeleteAppAsync(IAppEntity app, + CancellationToken ct) + { + return eventStore.DeleteAsync($"^([a-zA-Z0-9]+)\\-{app.Id}", ct); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppPermanentDeleter.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppPermanentDeleter.cs new file mode 100644 index 000000000..a7a91ed42 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppPermanentDeleter.cs @@ -0,0 +1,114 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Entities.Apps.DomainObject; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public sealed class AppPermanentDeleter : IEventConsumer + { + private readonly IEnumerable deleters; + private readonly IGrainFactory grainFactory; + private readonly HashSet consumingTypes; + + public string Name + { + get => GetType().Name; + } + + public string EventsFilter + { + get => "^app-"; + } + + public AppPermanentDeleter(IEnumerable deleters, IGrainFactory grainFactory, TypeNameRegistry typeNameRegistry) + { + this.deleters = deleters.OrderBy(x => x.Order).ToList(); + + this.grainFactory = grainFactory; + + // Compute the event types names once for performance reasons and use hashset for extensibility. + consumingTypes = new HashSet + { + typeNameRegistry.GetName(), + typeNameRegistry.GetName() + }; + } + + public bool Handles(StoredEvent @event) + { + return consumingTypes.Contains(@event.Data.Type); + } + + public async Task On(Envelope @event) + { + if (@event.Headers.Restored()) + { + return; + } + + switch (@event.Payload) + { + case AppDeleted appArchived: + await OnArchiveAsync(appArchived); + break; + case AppContributorRemoved appContributorRemoved: + await OnAppContributorRemoved(appContributorRemoved); + break; + } + } + + private async Task OnAppContributorRemoved(AppContributorRemoved appContributorRemoved) + { + using (Telemetry.Activities.StartActivity("RemoveContributorFromSystem")) + { + var appId = appContributorRemoved.AppId.Id; + + foreach (var deleter in deleters) + { + using (Telemetry.Activities.StartActivity(deleter.GetType().Name)) + { + await deleter.DeleteContributorAsync(appId, appContributorRemoved.ContributorId, default); + } + } + } + } + + private async Task OnArchiveAsync(AppDeleted appArchived) + { + 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 = await appGrain.GetStateAsync(); + + // If the app does not exist, the version is lower than zero. + if (app.Value.Version < 0) + { + return; + } + + foreach (var deleter in deleters) + { + using (Telemetry.Activities.StartActivity(deleter.GetType().Name)) + { + await deleter.DeleteAppAsync(app.Value, default); + } + } + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettings.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettings.cs index 2a39b7427..83c01b316 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettings.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettings.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Threading; using System.Threading.Tasks; using Orleans; using Squidex.Infrastructure; @@ -13,7 +14,7 @@ using Squidex.Infrastructure.Orleans; namespace Squidex.Domain.Apps.Entities.Apps { - public sealed class AppUISettings : IAppUISettings + public sealed class AppUISettings : IAppUISettings, IDeleter { private readonly IGrainFactory grainFactory; @@ -22,6 +23,23 @@ namespace Squidex.Domain.Apps.Entities.Apps this.grainFactory = grainFactory; } + async Task IDeleter.DeleteContributorAsync(DomainId appId, string contributorId, + CancellationToken ct) + { + await GetGrain(appId, null).ClearAsync(); + } + + async Task IDeleter.DeleteAppAsync(IAppEntity app, + CancellationToken ct) + { + await GetGrain(app.Id, null).ClearAsync(); + + foreach (var userId in app.Contributors.Keys) + { + await GetGrain(app.Id, userId).ClearAsync(); + } + } + public async Task GetAsync(DomainId appId, string? userId) { var result = await GetGrain(appId, userId).GetAsync(); @@ -44,6 +62,11 @@ namespace Squidex.Domain.Apps.Entities.Apps return GetGrain(appId, userId).SetAsync(settings.AsJ()); } + public Task ClearAsync(DomainId appId, string? userId) + { + return GetGrain(appId, userId).ClearAsync(); + } + private IAppUISettingsGrain GetGrain(DomainId appId, string? userId) { return grainFactory.GetGrain(GetKey(appId, userId)); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettingsGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettingsGrain.cs index cd943b1ab..d383e9fa1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettingsGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettingsGrain.cs @@ -35,6 +35,13 @@ namespace Squidex.Domain.Apps.Entities.Apps return Task.FromResult(state.Value.Settings.AsJ()); } + public Task ClearAsync() + { + TryDeactivateOnIdle(); + + return state.ClearAsync(); + } + public Task SetAsync(J settings) { state.Value.Settings = settings; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUsageDeleter.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUsageDeleter.cs new file mode 100644 index 000000000..71eb6ff64 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUsageDeleter.cs @@ -0,0 +1,29 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using Squidex.Infrastructure.UsageTracking; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public sealed class AppUsageDeleter : IDeleter + { + private readonly IApiUsageTracker apiUsageTracker; + + public AppUsageDeleter(IApiUsageTracker apiUsageTracker) + { + this.apiUsageTracker = apiUsageTracker; + } + + public Task DeleteAppAsync(IAppEntity app, + CancellationToken ct) + { + return apiUsageTracker.DeleteAsync(app.Id.ToString(), ct); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs index 89325c8f3..52d7accb0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs @@ -7,12 +7,16 @@ using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Threading; using System.Threading.Tasks; using Squidex.Assets; +using Squidex.Domain.Apps.Entities.Apps.DomainObject; using Squidex.Domain.Apps.Entities.Apps.Indexes; using Squidex.Domain.Apps.Entities.Backup; using Squidex.Domain.Apps.Events.Apps; using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Json.Objects; @@ -22,6 +26,7 @@ namespace Squidex.Domain.Apps.Entities.Apps { private const string SettingsFile = "Settings.json"; private const string AvatarFile = "Avatar.image"; + private readonly Rebuilder rebuilder; private readonly IAppImageStore appImageStore; private readonly IAppsIndex appsIndex; private readonly IAppUISettings appUISettings; @@ -30,14 +35,20 @@ namespace Squidex.Domain.Apps.Entities.Apps public string Name { get; } = "Apps"; - public BackupApps(IAppImageStore appImageStore, IAppsIndex appsIndex, IAppUISettings appUISettings) + public BackupApps( + Rebuilder rebuilder, + IAppImageStore appImageStore, + IAppsIndex appsIndex, + IAppUISettings appUISettings) { this.appsIndex = appsIndex; + this.rebuilder = rebuilder; this.appImageStore = appImageStore; this.appUISettings = appUISettings; } - public async Task BackupEventAsync(Envelope @event, BackupContext context) + public async Task BackupEventAsync(Envelope @event, BackupContext context, + CancellationToken ct) { switch (@event.Payload) { @@ -45,32 +56,34 @@ namespace Squidex.Domain.Apps.Entities.Apps context.UserMapping.Backup(appContributorAssigned.ContributorId); break; case AppImageUploaded: - await WriteAssetAsync(context.AppId, context.Writer); + await WriteAssetAsync(context.AppId, context.Writer, ct); break; } } - public async Task BackupAsync(BackupContext context) + public async Task BackupAsync(BackupContext context, + CancellationToken ct) { var json = await appUISettings.GetAsync(context.AppId, null); - await context.Writer.WriteJsonAsync(SettingsFile, json); + await context.Writer.WriteJsonAsync(SettingsFile, json, ct); } - public async Task RestoreEventAsync(Envelope @event, RestoreContext context) + public async Task RestoreEventAsync(Envelope @event, RestoreContext context, + CancellationToken ct) { switch (@event.Payload) { case AppCreated appCreated: { - await ReserveAppAsync(context.AppId, appCreated.Name); + await ReserveAppAsync(context.AppId, appCreated.Name, ct); break; } case AppImageUploaded: { - await ReadAssetAsync(context.AppId, context.Reader); + await ReadAssetAsync(context.AppId, context.Reader, ct); break; } @@ -103,16 +116,18 @@ namespace Squidex.Domain.Apps.Entities.Apps return true; } - public async Task RestoreAsync(RestoreContext context) + public async Task RestoreAsync(RestoreContext context, + CancellationToken ct) { - var json = await context.Reader.ReadJsonAsync(SettingsFile); + var json = await context.Reader.ReadJsonAsync(SettingsFile, ct); await appUISettings.SetAsync(context.AppId, null, json); } - private async Task ReserveAppAsync(DomainId appId, string appName) + private async Task ReserveAppAsync(DomainId appId, string appName, + CancellationToken ct) { - appReservation = await appsIndex.ReserveAsync(appId, appName); + appReservation = await appsIndex.ReserveAsync(appId, appName, ct); if (appReservation == null) { @@ -122,46 +137,43 @@ namespace Squidex.Domain.Apps.Entities.Apps public async Task CleanupRestoreErrorAsync(DomainId appId) { - if (appReservation != null) - { - await appsIndex.RemoveReservationAsync(appReservation); - } + await appsIndex.RemoveReservationAsync(appReservation); } public async Task CompleteRestoreAsync(RestoreContext context) { - await appsIndex.AddAsync(appReservation); - await appsIndex.RebuildByContributorsAsync(context.AppId, contributors); + await rebuilder.InsertManyAsync(Enumerable.Repeat(context.AppId, 1), 1, default); + + await appsIndex.RemoveReservationAsync(appReservation); } - private Task WriteAssetAsync(DomainId appId, IBackupWriter writer) + private async Task WriteAssetAsync(DomainId appId, IBackupWriter writer, + CancellationToken ct) { - return writer.WriteBlobAsync(AvatarFile, async stream => + try { - try + await using (var stream = await writer.OpenBlobAsync(AvatarFile, ct)) { - await appImageStore.DownloadAsync(appId, stream); + await appImageStore.DownloadAsync(appId, stream, ct); } - catch (AssetNotFoundException) - { - } - }); + } + catch (AssetNotFoundException) + { + } } - private async Task ReadAssetAsync(DomainId appId, IBackupReader reader) + private async Task ReadAssetAsync(DomainId appId, IBackupReader reader, + CancellationToken ct) { try { - await reader.ReadBlobAsync(AvatarFile, async stream => + await using (var stream = await reader.OpenBlobAsync(AvatarFile, ct)) { - try - { - await appImageStore.UploadAsync(appId, stream); - } - catch (AssetAlreadyExistsException) - { - } - }); + await appImageStore.UploadAsync(appId, stream, ct); + } + } + catch (AssetAlreadyExistsException) + { } catch (FileNotFoundException) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/ArchiveApp.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteApp.cs similarity index 89% rename from backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/ArchiveApp.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteApp.cs index c4999db95..631e5d55e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/ArchiveApp.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteApp.cs @@ -7,7 +7,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands { - public sealed class ArchiveApp : AppUpdateCommand + public sealed class DeleteApp : AppUpdateCommand { } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/DefaultAppLogStore.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/DefaultAppLogStore.cs index b5650bb9b..0b12fafc0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/DefaultAppLogStore.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/DefaultAppLogStore.cs @@ -19,7 +19,7 @@ using Squidex.Infrastructure.Log; namespace Squidex.Domain.Apps.Entities.Apps { - public sealed class DefaultAppLogStore : IAppLogStore + public sealed class DefaultAppLogStore : IAppLogStore, IDeleter { private const string FieldAuthClientId = "AuthClientId"; private const string FieldAuthUserId = "AuthUserId"; @@ -49,7 +49,14 @@ namespace Squidex.Domain.Apps.Entities.Apps this.requestLogStore = requestLogStore; } - public Task LogAsync(DomainId appId, RequestLog request) + Task IDeleter.DeleteAppAsync(IAppEntity app, + CancellationToken ct) + { + return requestLogStore.DeleteAsync(app.Id.ToString(), ct); + } + + public Task LogAsync(DomainId appId, RequestLog request, + CancellationToken ct = default) { if (!requestLogStore.IsEnabled) { @@ -75,7 +82,7 @@ namespace Squidex.Domain.Apps.Entities.Apps Append(storedRequest, FieldRequestPath, request.RequestPath); Append(storedRequest, FieldStatusCode, request.StatusCode); - return requestLogStore.LogAsync(storedRequest); + return requestLogStore.LogAsync(storedRequest, ct); } public async Task ReadLogAsync(DomainId appId, DateTime fromDate, DateTime toDate, Stream stream, @@ -104,7 +111,7 @@ namespace Squidex.Domain.Apps.Entities.Apps await csv.NextRecordAsync(); - await requestLogStore.QueryAllAsync(async request => + await foreach (var request in requestLogStore.QueryAllAsync(appId.ToString(), fromDate, toDate, ct)) { csv.WriteField(request.Timestamp.ToString()); csv.WriteField(GetString(request, FieldRequestPath)); @@ -121,7 +128,7 @@ namespace Squidex.Domain.Apps.Entities.Apps csv.WriteField(GetLong(request, FieldStatusCode)); await csv.NextRecordAsync(); - }, appId.ToString(), fromDate, toDate, ct); + } } } finally diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs index 9a61bbcd7..360b3adce 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs @@ -23,16 +23,17 @@ namespace Squidex.Domain.Apps.Entities.Apps.Diagnostics this.grainFactory = grainFactory; } - public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + public async Task CheckHealthAsync(HealthCheckContext context, + CancellationToken cancellationToken = default) { - await GetGrain().CountAsync(); + await GetGrain().GetAppIdsAsync(new[] { "test" }); return HealthCheckResult.Healthy("Orleans must establish communication."); } - private IAppsByNameIndexGrain GetGrain() + private IAppsCacheGrain GetGrain() { - return grainFactory.GetGrain(SingleGrain.Id); + 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 b24400e06..8579a7a5f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppCommandMiddleware.cs @@ -57,7 +57,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject { var file = uploadImage.File; - using (var uploadStream = file.OpenRead()) + await using (var uploadStream = file.OpenRead()) { var image = await assetThumbnailGenerator.GetImageInfoAsync(uploadStream); @@ -67,7 +67,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject } } - using (var uploadStream = file.OpenRead()) + await using (var uploadStream = file.OpenRead()) { await appImageStore.UploadAsync(uploadImage.AppId.Id, uploadStream); } 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 10aed63df..f85209f44 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 @@ -48,7 +48,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject public Workflows Workflows { get; set; } = Workflows.Empty; - public bool IsArchived { get; set; } + public bool IsDeleted { get; set; } [IgnoreDataMember] public DomainId UniqueId @@ -146,11 +146,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject return l; }); - case AppArchived: + case AppDeleted: { Plan = null; - IsArchived = true; + IsDeleted = true; return true; } 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 840b3b4e3..39b9a5da6 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.cs @@ -46,7 +46,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject protected override bool IsDeleted() { - return Snapshot.IsArchived; + return Snapshot.IsDeleted; } protected override bool CanAcceptCreation(ICommand command) @@ -291,12 +291,12 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject } }); - case ArchiveApp archive: - return UpdateAsync(archive, async c => + case DeleteApp delete: + return UpdateAsync(delete, async c => { await appPlansBillingManager.ChangePlanAsync(c.Actor.Identifier, Snapshot.NamedId(), null, null); - ArchiveApp(c); + DeleteApp(c); }); default: @@ -335,7 +335,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject private void ChangePlan(ChangePlan command) { - if (string.Equals(appPlansProvider.GetFreePlan()?.Id, command.PlanId)) + if (string.Equals(appPlansProvider.GetFreePlan()?.Id, command.PlanId, StringComparison.Ordinal)) { Raise(command, new AppPlanReset()); } @@ -440,9 +440,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject Raise(command, new AppRoleUpdated()); } - private void ArchiveApp(ArchiveApp command) + private void DeleteApp(DeleteApp command) { - Raise(command, new AppArchived()); + Raise(command, new AppDeleted()); } private void Raise(T command, TEvent @event) where T : class where TEvent : AppEvent diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs index d546c71d3..fd2c599e2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs @@ -41,6 +41,6 @@ namespace Squidex.Domain.Apps.Entities.Apps Workflows Workflows { get; } - bool IsArchived { get; } + bool IsDeleted { get; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppImageStore.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppImageStore.cs index 689d8c963..d72bfd4ef 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppImageStore.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppImageStore.cs @@ -14,8 +14,10 @@ namespace Squidex.Domain.Apps.Entities.Apps { public interface IAppImageStore { - Task UploadAsync(DomainId appId, Stream stream, CancellationToken ct = default); + Task UploadAsync(DomainId appId, Stream stream, + CancellationToken ct = default); - Task DownloadAsync(DomainId appId, Stream stream, CancellationToken ct = default); + Task DownloadAsync(DomainId appId, Stream stream, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppLogStore.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppLogStore.cs index 871d9fb40..4659796ff 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppLogStore.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppLogStore.cs @@ -15,8 +15,10 @@ namespace Squidex.Domain.Apps.Entities.Apps { public interface IAppLogStore { - Task LogAsync(DomainId appId, RequestLog request); + Task LogAsync(DomainId appId, RequestLog request, + CancellationToken ct = default); - Task ReadLogAsync(DomainId appId, DateTime fromDate, DateTime toDate, Stream stream, CancellationToken ct = default); + Task ReadLogAsync(DomainId appId, DateTime fromDate, DateTime toDate, Stream stream, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppUISettingsGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppUISettingsGrain.cs index 4ce221196..d33381bc5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppUISettingsGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppUISettingsGrain.cs @@ -21,5 +21,7 @@ namespace Squidex.Domain.Apps.Entities.Apps Task SetAsync(J settings); Task RemoveAsync(string path); + + Task ClearAsync(); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs deleted file mode 100644 index 11200a848..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs +++ /dev/null @@ -1,27 +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; -using Squidex.Infrastructure.Orleans.Indexes; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.Apps.Indexes -{ - public sealed class AppsByNameIndexGrain : UniqueNameIndexGrain, IAppsByNameIndexGrain - { - public AppsByNameIndexGrain(IGrainState state) - : base(state) - { - } - } - - [CollectionName("Index_AppsByName")] - public sealed class AppsByNameIndexState : UniqueNameIndexState - { - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexGrain.cs deleted file mode 100644 index 4214a73f9..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexGrain.cs +++ /dev/null @@ -1,27 +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; -using Squidex.Infrastructure.Orleans.Indexes; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.Apps.Indexes -{ - public sealed class AppsByUserIndexGrain : IdsIndexGrain, IAppsByUserIndexGrain - { - public AppsByUserIndexGrain(IGrainState state) - : base(state) - { - } - } - - [CollectionName("Index_AppsByUser")] - public sealed class AppsByUserIndex : IdsIndexState - { - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsCacheGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsCacheGrain.cs new file mode 100644 index 000000000..001b2f7cc --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsCacheGrain.cs @@ -0,0 +1,89 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Orleans.Concurrency; +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(IAppRepository appRepository) + { + this.appRepository = appRepository; + } + + 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 5fe055b5e..c7883c5a4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs @@ -8,11 +8,13 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; 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.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Orleans; @@ -27,68 +29,31 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes public sealed class AppsIndex : IAppsIndex, ICommandMiddleware { private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5); + private readonly IAppRepository appRepository; private readonly IGrainFactory grainFactory; private readonly IReplicatedCache grainCache; - public AppsIndex(IGrainFactory grainFactory, IReplicatedCache grainCache) + public AppsIndex(IAppRepository appRepository, IGrainFactory grainFactory, IReplicatedCache grainCache) { + this.appRepository = appRepository; this.grainFactory = grainFactory; this.grainCache = grainCache; } - public async Task RebuildByContributorsAsync(DomainId appId, HashSet contributors) + public Task RemoveReservationAsync(string? token, + CancellationToken ct = default) { - foreach (var contributorId in contributors) - { - await Index(contributorId).AddAsync(appId); - } - } - - public Task RebuildByContributorsAsync(string contributorId, HashSet apps) - { - return Index(contributorId).RebuildAsync(apps); - } - - public Task RebuildAsync(Dictionary appsByName) - { - return Index().RebuildAsync(appsByName); + return Cache().RemoveReservationAsync(token); } - public Task RemoveReservationAsync(string? token) + public Task ReserveAsync(DomainId id, string name, + CancellationToken ct = default) { - return Index().RemoveReservationAsync(token); + return Cache().ReserveAsync(id, name); } - public Task> GetIdsAsync() - { - return Index().GetIdsAsync(); - } - - public Task AddAsync(string? token) - { - return Index().AddAsync(token); - } - - public Task ReserveAsync(DomainId id, string name) - { - return Index().ReserveAsync(id, name); - } - - public async Task> GetAppsAsync() - { - using (Telemetry.Activities.StartActivity("AppProvider/GetAppsAsync")) - { - var ids = await GetAppIdsAsync(); - - var apps = - await Task.WhenAll(ids - .Select(id => GetAppAsync(id, false))); - - return apps.NotNull().ToList(); - } - } - - public async Task> GetAppsForUserAsync(string userId, PermissionSet permissions) + public async Task> GetAppsForUserAsync(string userId, PermissionSet permissions, + CancellationToken ct = default) { using (Telemetry.Activities.StartActivity("AppProvider/GetAppsForUserAsync")) { @@ -100,13 +65,14 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes var apps = await Task.WhenAll(ids .SelectMany(x => x).Distinct() - .Select(id => GetAppAsync(id, false))); + .Select(id => GetAppAsync(id, false, ct))); return apps.NotNull().ToList(); } } - public async Task GetAppByNameAsync(string name, bool canCache = false) + public async Task GetAppAsync(string name, bool canCache = false, + CancellationToken ct = default) { using (Telemetry.Activities.StartActivity("AppProvider/GetAppByNameAsync")) { @@ -125,17 +91,18 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes return null; } - return await GetAppAsync(appId, canCache); + return await GetAppAsync(appId, canCache, ct); } } - public async Task GetAppAsync(DomainId appId, bool canCache) + public async Task GetAppAsync(DomainId appId, bool canCache = false, + CancellationToken ct = default) { using (Telemetry.Activities.StartActivity("AppProvider/GetAppAsync")) { if (canCache) { - if (grainCache.TryGetValue(GetCacheKey(appId), out var v) && v is IAppEntity cachedApp) + if (grainCache.TryGetValue(GetCacheKey(appId), out var cached) && cached is IAppEntity cachedApp) { return cachedApp; } @@ -152,27 +119,23 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes } } - private async Task> GetAppIdsByUserAsync(string userId) + private async Task> GetAppIdsByUserAsync(string userId) { using (Telemetry.Activities.StartActivity("AppProvider/GetAppIdsByUserAsync")) { - return await grainFactory.GetGrain(userId).GetIdsAsync(); - } - } + var result = await appRepository.QueryIdsAsync(userId); - private async Task> GetAppIdsAsync() - { - using (Telemetry.Activities.StartActivity("AppProvider/GetAllAppIdsAsync")) - { - return await grainFactory.GetGrain(SingleGrain.Id).GetIdsAsync(); + return result.Values; } } - private async Task> GetAppIdsAsync(string[] names) + private async Task> GetAppIdsAsync(string[] names) { using (Telemetry.Activities.StartActivity("AppProvider/GetAppIdsAsync")) { - return await grainFactory.GetGrain(SingleGrain.Id).GetIdsAsync(names); + var result = await Cache().GetAppIdsAsync(names); + + return result; } } @@ -180,134 +143,109 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes { using (Telemetry.Activities.StartActivity("AppProvider/GetAppIdAsync")) { - return await grainFactory.GetGrain(SingleGrain.Id).GetIdAsync(name); + var result = await Cache().GetAppIdsAsync(new[] { name }); + + return result.FirstOrDefault(); } } public async Task HandleAsync(CommandContext context, NextDelegate next) { - if (context.Command is CreateApp createApp) - { - var index = Index(); + var command = context.Command; - var token = await CheckAppAsync(index, createApp); + if (command is CreateApp createApp) + { + var cache = Cache(); + var token = await CheckAppAsync(cache, createApp); try { await next(context); } finally { - if (token != null) - { - if (context.IsCompleted) - { - await index.AddAsync(token); - - await Index(createApp.Actor.Identifier).AddAsync(createApp.AppId); - } - else - { - await index.RemoveReservationAsync(token); - } - } + await cache.RemoveReservationAsync(token); } } else { await next(context); + } - if (context.IsCompleted && context.Command is AppCommand appCommand) + if (context.IsCompleted) + { + switch (command) { - var app = context.PlainResult as IAppEntity; - - if (app == null) - { - app = await GetAppCoreAsync(appCommand.AggregateId, true); - } - - if (app != null) - { - await InvalidateItAsync(app); - - switch (context.Command) - { - case AssignContributor assignContributor: - await AssignContributorAsync(assignContributor); - break; - - case RemoveContributor removeContributor: - await RemoveContributorAsync(removeContributor); - break; - - case ArchiveApp: - await ArchiveAppAsync(app); - break; - } - } + case CreateApp create: + await OnCreateAsync(create); + break; + case DeleteApp delete: + await OnDeleteAsync(delete); + break; + case AppUpdateCommand update: + await OnUpdateAsync(update); + break; } } } - private static async Task CheckAppAsync(IAppsByNameIndexGrain index, CreateApp command) + private async Task CheckAppAsync(IAppsCacheGrain cache, CreateApp command) { - var name = command.Name; + var token = await cache.ReserveAsync(command.AppId, command.Name); + + if (token == null) + { + throw new ValidationException(T.Get("apps.nameAlreadyExists")); + } - if (name.IsSlug()) + try { - var token = await index.ReserveAsync(command.AppId, name); + var existingId = await GetAppIdAsync(command.Name); - if (token == null) + if (existingId != default) { throw new ValidationException(T.Get("apps.nameAlreadyExists")); } - - return token; + } + catch + { + // Catch our own exception, juist in case something went wrong before. + await cache.RemoveReservationAsync(token); + throw; } - return null; + return token; } - private async Task AssignContributorAsync(AssignContributor command) + private async Task OnCreateAsync(CreateApp create) { - await Index(command.ContributorId).AddAsync(command.AggregateId); - } + await InvalidateItAsync(create.AppId, create.Name); - private async Task RemoveContributorAsync(RemoveContributor command) - { - await Index(command.ContributorId).RemoveAsync(command.AggregateId); + await Cache().AddAsync(create.AppId, create.Name); } - private async Task ArchiveAppAsync(IAppEntity app) + private async Task OnDeleteAsync(DeleteApp delete) { - await Index().RemoveAsync(app.Id); + await InvalidateItAsync(delete.AppId.Id, delete.AppId.Name); - foreach (var contributorId in app.Contributors.Keys) - { - await Index(contributorId).RemoveAsync(app.Id); - } - - if (app.CreatedBy.IsClient || !app.Contributors.ContainsKey(app.CreatedBy.Identifier)) - { - await Index(app.CreatedBy.Identifier).RemoveAsync(app.Id); - } + await Cache().RemoveAsync(delete.AppId.Id); } - private IAppsByNameIndexGrain Index() + private async Task OnUpdateAsync(AppUpdateCommand update) { - return grainFactory.GetGrain(SingleGrain.Id); + await InvalidateItAsync(update.AppId.Id, update.AppId.Name); } - private IAppsByUserIndexGrain Index(string id) + private IAppsCacheGrain Cache() { - return grainFactory.GetGrain(id); + return grainFactory.GetGrain(SingleGrain.Id); } private async Task GetAppCoreAsync(DomainId id, bool allowArchived = false) { var app = (await grainFactory.GetGrain(id.ToString()).GetStateAsync()).Value; - if (app.Version <= EtagVersion.Empty || (app.IsArchived && !allowArchived)) + if (app.Version <= EtagVersion.Empty || (app.IsDeleted && !allowArchived)) { return null; } @@ -325,11 +263,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes return $"{typeof(AppsIndex)}_Apps_Name_{name}"; } - private Task InvalidateItAsync(IAppEntity app) + private Task InvalidateItAsync(DomainId id, string name) { return grainCache.RemoveAsync( - GetCacheKey(app.Id), - GetCacheKey(app.Name)); + GetCacheKey(id), + GetCacheKey(name)); } private Task CacheItAsync(IAppEntity app) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByUserIndexGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByUserIndexGrain.cs deleted file mode 100644 index 077679645..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByUserIndexGrain.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Orleans.Indexes; - -namespace Squidex.Domain.Apps.Entities.Apps.Indexes -{ - public interface IAppsByUserIndexGrain : IIdsIndexGrain, IGrainWithStringKey - { - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndexGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsCacheGrain.cs similarity index 63% rename from backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndexGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsCacheGrain.cs index 09154d325..c0a769d10 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndexGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsCacheGrain.cs @@ -5,13 +5,19 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Orleans; +using System.Collections.Generic; +using System.Threading.Tasks; using Squidex.Infrastructure; using Squidex.Infrastructure.Orleans.Indexes; namespace Squidex.Domain.Apps.Entities.Apps.Indexes { - public interface IAppsByNameIndexGrain : IUniqueNameIndexGrain, IGrainWithStringKey + 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 e72a0f4bb..309c6749e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsIndex.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsIndex.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Squidex.Infrastructure; using Squidex.Infrastructure.Security; @@ -14,26 +15,19 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes { public interface IAppsIndex { - Task> GetIdsAsync(); + Task> GetAppsForUserAsync(string userId, PermissionSet permissions, + CancellationToken ct = default); - Task> GetAppsAsync(); + Task GetAppAsync(string name, bool canCache = false, + CancellationToken ct = default); - Task> GetAppsForUserAsync(string userId, PermissionSet permissions); + Task GetAppAsync(DomainId appId, bool canCache = false, + CancellationToken ct = default); - Task GetAppByNameAsync(string name, bool canCache); + Task ReserveAsync(DomainId id, string name, + CancellationToken ct = default); - Task GetAppAsync(DomainId appId, bool canCache); - - Task ReserveAsync(DomainId id, string name); - - Task AddAsync(string? token); - - Task RemoveReservationAsync(string? token); - - Task RebuildByContributorsAsync(string contributorId, HashSet apps); - - Task RebuildAsync(Dictionary apps); - - Task RebuildByContributorsAsync(DomainId appId, HashSet contributors); + Task RemoveReservationAsync(string? token, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/ConfigAppPlansProvider.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/ConfigAppPlansProvider.cs index f0325e0dc..e55ade93c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/ConfigAppPlansProvider.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/ConfigAppPlansProvider.cs @@ -41,7 +41,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans } } - freePlan = plansList.FirstOrDefault(x => x.IsFree) ?? Infinite; + freePlan = plansList.Find(x => x.IsFree) ?? Infinite; } public IEnumerable GetAvailablePlans() 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 b96c0492f..5ee6eeb38 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/RestrictAppsCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/RestrictAppsCommandMiddleware.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Globalization; using System.Threading.Tasks; using Microsoft.Extensions.Options; using Squidex.Domain.Apps.Entities.Apps.Commands; @@ -54,8 +55,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans if (context.IsCompleted && user != null) { var newApps = totalApps + 1; + var newAppsValue = newApps.ToString(CultureInfo.InvariantCulture); - await userResolver.SetClaimAsync(user.Id, SquidexClaimTypes.TotalApps, newApps.ToString(), true); + await userResolver.SetClaimAsync(user.Id, SquidexClaimTypes.TotalApps, newAppsValue, true); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs new file mode 100644 index 000000000..5e0057f14 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Apps.Repositories +{ + public interface IAppRepository + { + Task> QueryIdsAsync(string contributorId, + CancellationToken ct = default); + + Task> QueryIdsAsync(IEnumerable names, + CancellationToken ct = default); + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs index 23b355719..39546cd39 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/RolePermissionsProvider.cs @@ -75,7 +75,7 @@ namespace Squidex.Domain.Apps.Entities.Apps foreach (var schema in schemaNames) { - var replaced = trimmed.Replace("{schema}", schema); + var replaced = trimmed.Replace("{schema}", schema, StringComparison.Ordinal); result.Add(replaced); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs index 200c49e09..e833e5ef5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs @@ -47,7 +47,7 @@ namespace Squidex.Domain.Apps.Entities.Assets } public async IAsyncEnumerable CreateSnapshotEventsAsync(RuleContext context, - [EnumeratorCancellation] CancellationToken ct = default) + [EnumeratorCancellation] CancellationToken ct) { await foreach (var asset in assetRepository.StreamAll(context.AppId.Id, ct)) { @@ -66,7 +66,7 @@ namespace Squidex.Domain.Apps.Entities.Assets } public async IAsyncEnumerable CreateEnrichedEventsAsync(Envelope @event, RuleContext context, - [EnumeratorCancellation] CancellationToken ct = default) + [EnumeratorCancellation] CancellationToken ct) { var assetEvent = (AssetEvent)@event.Payload; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetPermanentDeleter.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetPermanentDeleter.cs index 7d2b1ed7a..1071f13a9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetPermanentDeleter.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetPermanentDeleter.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Collections.Generic; using System.Threading.Tasks; using Squidex.Assets; using Squidex.Domain.Apps.Events.Assets; @@ -16,7 +17,7 @@ namespace Squidex.Domain.Apps.Entities.Assets public sealed class AssetPermanentDeleter : IEventConsumer { private readonly IAssetFileStore assetFileStore; - private readonly string? deletedType; + private readonly HashSet consumingTypes; public string Name { @@ -32,12 +33,16 @@ namespace Squidex.Domain.Apps.Entities.Assets { this.assetFileStore = assetFileStore; - deletedType = typeNameRegistry?.GetName(); + // Compute the event types names once for performance reasons and use hashset for extensibility. + consumingTypes = new HashSet + { + typeNameRegistry.GetName() + }; } public bool Handles(StoredEvent @event) { - return @event.Data.Type == deletedType; + return consumingTypes.Contains(@event.Data.Type); } public async Task On(Envelope @event) @@ -49,16 +54,13 @@ namespace Squidex.Domain.Apps.Entities.Assets if (@event.Payload is AssetDeleted assetDeleted) { - for (var version = 0; version < @event.Headers.EventStreamNumber(); version++) + try + { + await assetFileStore.DeleteAsync(assetDeleted.AppId.Id, assetDeleted.AssetId); + } + catch (AssetNotFoundException) { - try - { - await assetFileStore.DeleteAsync(assetDeleted.AppId.Id, assetDeleted.AssetId, version, null); - } - catch (AssetNotFoundException) - { - continue; - } + return; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetTagsDeleter.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetTagsDeleter.cs new file mode 100644 index 000000000..31e406a33 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetTagsDeleter.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Tags; +using Squidex.Domain.Apps.Entities.Apps; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public sealed class AssetTagsDeleter : IDeleter + { + private readonly ITagService tagService; + + public AssetTagsDeleter(ITagService tagService) + { + this.tagService = tagService; + } + + public Task DeleteAppAsync(IAppEntity app, + CancellationToken ct) + { + return tagService.ClearAsync(app.Id, TagGroups.Assets); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker.cs index 5d5f190d2..c2b43bec1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetUsageTracker.cs @@ -7,7 +7,9 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Apps; using Squidex.Infrastructure; using Squidex.Infrastructure.UsageTracking; @@ -15,7 +17,7 @@ using Squidex.Infrastructure.UsageTracking; namespace Squidex.Domain.Apps.Entities.Assets { - public partial class AssetUsageTracker : IAssetUsageTracker + public partial class AssetUsageTracker : IAssetUsageTracker, IDeleter { private const string CounterTotalCount = "TotalAssets"; private const string CounterTotalSize = "TotalSize"; @@ -27,6 +29,14 @@ namespace Squidex.Domain.Apps.Entities.Assets this.usageTracker = usageTracker; } + Task IDeleter.DeleteAppAsync(IAppEntity app, + CancellationToken ct) + { + var key = GetKey(app.Id); + + return usageTracker.DeleteAsync(key, ct); + } + public async Task GetTotalSizeAsync(DomainId appId) { var key = GetKey(appId); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs index ebabf7530..c67b1d394 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetsJintExtension.cs @@ -162,10 +162,9 @@ namespace Squidex.Domain.Apps.Entities.Assets .WithoutTotal()); var assetQuery = serviceProvider.GetRequiredService(); + var assetItems = await assetQuery.QueryAsync(requestContext, null, Q.Empty.WithIds(ids), context.CancellationToken); - var assets = await assetQuery.QueryAsync(requestContext, null, Q.Empty.WithIds(ids), context.CancellationToken); - - callback(JsValue.FromObject(context.Engine, assets.ToArray())); + callback(JsValue.FromObject(context.Engine, assetItems.ToArray())); } catch (Exception ex) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs index 1e5705af1..82775f5ae 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.IO; +using System.Threading; using System.Threading.Tasks; using Squidex.Assets; using Squidex.Domain.Apps.Core.Tags; @@ -38,12 +39,14 @@ namespace Squidex.Domain.Apps.Entities.Assets this.tagService = tagService; } - public Task BackupAsync(BackupContext context) + public Task BackupAsync(BackupContext context, + CancellationToken ct) { - return BackupTagsAsync(context); + return BackupTagsAsync(context, ct); } - public Task BackupEventAsync(Envelope @event, BackupContext context) + public Task BackupEventAsync(Envelope @event, BackupContext context, + CancellationToken ct) { switch (@event.Payload) { @@ -52,19 +55,22 @@ namespace Squidex.Domain.Apps.Entities.Assets assetCreated.AppId.Id, assetCreated.AssetId, assetCreated.FileVersion, - context.Writer); + context.Writer, + ct); case AssetUpdated assetUpdated: return WriteAssetAsync( assetUpdated.AppId.Id, assetUpdated.AssetId, assetUpdated.FileVersion, - context.Writer); + context.Writer, + ct); } return Task.CompletedTask; } - public async Task RestoreEventAsync(Envelope @event, RestoreContext context) + public async Task RestoreEventAsync(Envelope @event, RestoreContext context, + CancellationToken ct) { switch (@event.Payload) { @@ -78,57 +84,65 @@ namespace Squidex.Domain.Apps.Entities.Assets assetCreated.AppId.Id, assetCreated.AssetId, assetCreated.FileVersion, - context.Reader); + context.Reader, + ct); break; case AssetUpdated assetUpdated: await ReadAssetAsync( assetUpdated.AppId.Id, assetUpdated.AssetId, assetUpdated.FileVersion, - context.Reader); + context.Reader, + ct); break; } return true; } - public async Task RestoreAsync(RestoreContext context) + public async Task RestoreAsync(RestoreContext context, + CancellationToken ct) { - await RestoreTagsAsync(context); + await RestoreTagsAsync(context, ct); if (assetIds.Count > 0) { - await rebuilder.InsertManyAsync(assetIds, BatchSize); + await rebuilder.InsertManyAsync(assetIds, BatchSize, ct); } if (assetFolderIds.Count > 0) { - await rebuilder.InsertManyAsync(assetFolderIds, BatchSize); + await rebuilder.InsertManyAsync(assetFolderIds, BatchSize, ct); } } - private async Task RestoreTagsAsync(RestoreContext context) + private async Task RestoreTagsAsync(RestoreContext context, + CancellationToken ct) { - var tags = await context.Reader.ReadJsonAsync(TagsFile); + var tags = await context.Reader.ReadJsonAsync(TagsFile, ct); await tagService.RebuildTagsAsync(context.AppId, TagGroups.Assets, tags); } - private async Task BackupTagsAsync(BackupContext context) + private async Task BackupTagsAsync(BackupContext context, + CancellationToken ct) { var tags = await tagService.GetExportableTagsAsync(context.AppId, TagGroups.Assets); - await context.Writer.WriteJsonAsync(TagsFile, tags); + await context.Writer.WriteJsonAsync(TagsFile, tags, ct); } - private async Task WriteAssetAsync(DomainId appId, DomainId assetId, long fileVersion, IBackupWriter writer) + private async Task WriteAssetAsync(DomainId appId, DomainId assetId, long fileVersion, IBackupWriter writer, + CancellationToken ct) { try { - await writer.WriteBlobAsync(GetName(assetId, fileVersion), stream => + var fileName = GetName(assetId, fileVersion); + + await using (var stream = await writer.OpenBlobAsync(fileName, ct)) { - return assetFileStore.DownloadAsync(appId, assetId, fileVersion, null, stream); - }); + await assetFileStore.DownloadAsync(appId, assetId, fileVersion, null, stream, default, ct); + } } catch (AssetNotFoundException) { @@ -136,14 +150,17 @@ namespace Squidex.Domain.Apps.Entities.Assets } } - private async Task ReadAssetAsync(DomainId appId, DomainId assetId, long fileVersion, IBackupReader reader) + private async Task ReadAssetAsync(DomainId appId, DomainId assetId, long fileVersion, IBackupReader reader, + CancellationToken ct) { try { - await reader.ReadBlobAsync(GetName(assetId, fileVersion), stream => + var fileName = GetName(assetId, fileVersion); + + await using (var stream = await reader.OpenBlobAsync(fileName, ct)) { - return assetFileStore.UploadAsync(appId, assetId, fileVersion, null, stream); - }); + await assetFileStore.UploadAsync(appId, assetId, fileVersion, null, stream, true, ct); + } } catch (FileNotFoundException) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DefaultAssetFileStore.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DefaultAssetFileStore.cs index e34b8cbc1..ef023da30 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DefaultAssetFileStore.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DefaultAssetFileStore.cs @@ -6,27 +6,50 @@ // ========================================================================== using System.IO; +using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Options; using Squidex.Assets; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Assets { - public sealed class DefaultAssetFileStore : IAssetFileStore + public sealed class DefaultAssetFileStore : IAssetFileStore, IDeleter { private readonly IAssetStore assetStore; + private readonly IAssetRepository assetRepository; private readonly AssetOptions options; - public DefaultAssetFileStore(IAssetStore assetStore, + public DefaultAssetFileStore( + IAssetStore assetStore, + IAssetRepository assetRepository, IOptions options) { this.assetStore = assetStore; + this.assetRepository = assetRepository; this.options = options.Value; } + async Task IDeleter.DeleteAppAsync(IAppEntity app, + CancellationToken ct) + { + if (options.FolderPerApp) + { + await assetStore.DeleteByPrefixAsync($"{app.Id}/", ct); + } + else + { + await foreach (var asset in assetRepository.StreamAll(app.Id, ct)) + { + await DeleteAsync(app.Id, asset.Id, ct); + } + } + } + public string? GeneratePublicUrl(DomainId appId, DomainId id, long fileVersion, string? suffix) { var fileName = GetFileName(appId, id, fileVersion, suffix); @@ -90,64 +113,68 @@ namespace Squidex.Domain.Apps.Entities.Assets return assetStore.CopyAsync(tempFile, fileName, ct); } - public Task DeleteAsync(DomainId appId, DomainId id, long fileVersion, string? suffix) + public Task DeleteAsync(DomainId appId, DomainId id, + CancellationToken ct = default) { - var fileNameOld = GetFileName(id, fileVersion, suffix); - var fileNameNew = GetFileName(appId, id, fileVersion, suffix); - if (options.FolderPerApp) { - return assetStore.DeleteAsync(fileNameNew); + return assetStore.DeleteByPrefixAsync($"{appId}/", ct); } else { + var fileNameOld = GetFileName(id); + var fileNameNew = GetFileName(appId, id); + return Task.WhenAll( - assetStore.DeleteAsync(fileNameOld), - assetStore.DeleteAsync(fileNameNew)); + assetStore.DeleteByPrefixAsync(fileNameOld, ct), + assetStore.DeleteByPrefixAsync(fileNameNew, ct)); } } - public Task DeleteAsync(string tempFile) + public Task DeleteAsync(string tempFile, + CancellationToken ct = default) { - return assetStore.DeleteAsync(tempFile); + return assetStore.DeleteAsync(tempFile, ct); } - private static string GetFileName(DomainId id, long fileVersion, string? suffix) + private string GetFileName(DomainId id, long fileVersion = -1, string? suffix = null) { - if (!string.IsNullOrWhiteSpace(suffix)) - { - return $"{id}_{fileVersion}_{suffix}"; - } - else - { - return $"{id}_{fileVersion}"; - } + return GetFileName(default, id, fileVersion, suffix); } - private string GetFileName(DomainId appId, DomainId id, long fileVersion, string? suffix) + private string GetFileName(DomainId appId, DomainId id, long fileVersion = -1, string? suffix = null) { - if (options.FolderPerApp) + var sb = new StringBuilder(20); + + if (appId != default) { - if (!string.IsNullOrWhiteSpace(suffix)) + sb.Append(appId); + + if (options.FolderPerApp) { - return $"derived/{appId}/{id}_{fileVersion}_{suffix}"; + sb.Append('/'); } else { - return $"{appId}/{id}_{fileVersion}"; + sb.Append('_'); } } - else + + sb.Append(id); + + if (fileVersion >= 0) { - if (!string.IsNullOrWhiteSpace(suffix)) - { - return $"{appId}_{id}_{fileVersion}_{suffix}"; - } - else - { - return $"{appId}_{id}_{fileVersion}"; - } + sb.Append('_'); + sb.Append(fileVersion); } + + if (!string.IsNullOrWhiteSpace(suffix)) + { + sb.Append('_'); + sb.Append(suffix); + } + + return sb.ToString(); } } } 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 edc8985fc..12d9ea947 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetCommandMiddleware.cs @@ -161,7 +161,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject private async Task EnrichWithHashAndUploadAsync(UploadAssetCommand command, string tempFile) { - using (var uploadStream = command.File.OpenRead()) + await using (var uploadStream = command.File.OpenRead()) { using (var hashStream = new HasherStream(uploadStream, HashAlgorithmName.SHA256)) { 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 1ab46fd9e..47035675c 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 @@ -40,6 +40,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject public bool IsProtected { get; set; } + public bool IsDeleted { get; set; } + public HashSet Tags { get; set; } public AssetMetadata Metadata { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.State.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.State.cs index aa8694a97..186974063 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.State.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.State.cs @@ -24,6 +24,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject public DomainId ParentId { get; set; } + public bool IsDeleted { get; set; } + [IgnoreDataMember] public DomainId UniqueId { 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 20057aeaa..849a40811 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetsBulkUpdateCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetsBulkUpdateCommandMiddleware.cs @@ -104,7 +104,10 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject bulkUpdates, results); - await createCommandsBlock.SendAsync(task); + if (!await createCommandsBlock.SendAsync(task)) + { + break; + } } createCommandsBlock.Complete(); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetEnricher.cs index 2b2bf18cc..17a9ff463 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetEnricher.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetEnricher.cs @@ -13,8 +13,10 @@ namespace Squidex.Domain.Apps.Entities.Assets { public interface IAssetEnricher { - Task EnrichAsync(IAssetEntity asset, Context context, CancellationToken ct = default); + Task EnrichAsync(IAssetEntity asset, Context context, + CancellationToken ct); - Task> EnrichAsync(IEnumerable assets, Context context, CancellationToken ct = default); + Task> EnrichAsync(IEnumerable assets, Context context, + CancellationToken ct); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFileStore.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFileStore.cs index a3d1c69ae..ab287ab4b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFileStore.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFileStore.cs @@ -17,18 +17,25 @@ namespace Squidex.Domain.Apps.Entities.Assets { string? GeneratePublicUrl(DomainId appId, DomainId id, long fileVersion, string? suffix); - Task GetFileSizeAsync(DomainId appId, DomainId id, long fileVersion, string? suffix, CancellationToken ct = default); + Task GetFileSizeAsync(DomainId appId, DomainId id, long fileVersion, string? suffix, + CancellationToken ct = default); - Task CopyAsync(string tempFile, DomainId appId, DomainId id, long fileVersion, string? suffix, CancellationToken ct = default); + Task CopyAsync(string tempFile, DomainId appId, DomainId id, long fileVersion, string? suffix, + CancellationToken ct = default); - Task UploadAsync(string tempFile, Stream stream, CancellationToken ct = default); + Task UploadAsync(string tempFile, Stream stream, + CancellationToken ct = default); - Task UploadAsync(DomainId appId, DomainId id, long fileVersion, string? suffix, Stream stream, bool overwrite = true, CancellationToken ct = default); + Task UploadAsync(DomainId appId, DomainId id, long fileVersion, string? suffix, Stream stream, bool overwrite = true, + CancellationToken ct = default); - Task DownloadAsync(DomainId appId, DomainId id, long fileVersion, string? suffix, Stream stream, BytesRange range = default, CancellationToken ct = default); + Task DownloadAsync(DomainId appId, DomainId id, long fileVersion, string? suffix, Stream stream, BytesRange range = default, + CancellationToken ct = default); - Task DeleteAsync(string tempFile); + Task DeleteAsync(string tempFile, + CancellationToken ct = default); - Task DeleteAsync(DomainId appId, DomainId id, long fileVersion, string? suffix); + Task DeleteAsync(DomainId appId, DomainId id, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs index 07dcc4423..92190db7a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs @@ -14,20 +14,28 @@ namespace Squidex.Domain.Apps.Entities.Assets { public interface IAssetQueryService { - Task> QueryAsync(Context context, DomainId? parentId, Q q, CancellationToken ct = default); + Task> QueryAsync(Context context, DomainId? parentId, Q q, + CancellationToken ct = default); - Task> QueryAssetFoldersAsync(Context context, DomainId parentId, CancellationToken ct = default); + Task> QueryAssetFoldersAsync(Context context, DomainId parentId, + CancellationToken ct = default); - Task> QueryAssetFoldersAsync(DomainId appId, DomainId parentId, CancellationToken ct = default); + Task> QueryAssetFoldersAsync(DomainId appId, DomainId parentId, + CancellationToken ct = default); - Task> FindAssetFolderAsync(DomainId appId, DomainId id, CancellationToken ct = default); + Task> FindAssetFolderAsync(DomainId appId, DomainId id, + CancellationToken ct = default); - Task FindByHashAsync(Context context, string hash, string fileName, long fileSize, CancellationToken ct = default); + Task FindByHashAsync(Context context, string hash, string fileName, long fileSize, + CancellationToken ct = default); - Task FindAsync(Context context, DomainId id, long version = EtagVersion.Any, CancellationToken ct = default); + Task FindAsync(Context context, DomainId id, long version = EtagVersion.Any, + CancellationToken ct = default); - Task FindBySlugAsync(Context context, string slug, CancellationToken ct = default); + Task FindBySlugAsync(Context context, string slug, + CancellationToken ct = default); - Task FindGlobalAsync(Context context, DomainId id, CancellationToken ct = default); + Task FindGlobalAsync(Context context, DomainId id, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs index e2c0ecf28..0fa544680 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs @@ -61,7 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { ImageInfo? imageInfo = null; - using (var uploadStream = command.File.OpenRead()) + await using (var uploadStream = command.File.OpenRead()) { imageInfo = await assetThumbnailGenerator.GetImageInfoAsync(uploadStream); } @@ -74,7 +74,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { var tempFile = new TempAssetFile(command.File); - using (var uploadStream = command.File.OpenRead()) + await using (var uploadStream = command.File.OpenRead()) { imageInfo = await assetThumbnailGenerator.FixOrientationAsync(uploadStream, tempFile.Stream); } 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 db0f6eda2..d4ae955f5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetLoader.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetLoader.cs @@ -21,7 +21,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries this.grainFactory = grainFactory; } - public async Task GetAsync(DomainId appId, DomainId id, long version) + public async Task GetAsync(DomainId appId, DomainId id, long version = EtagVersion.Any) { using (Telemetry.Activities.StartActivity("AssetLoader/GetAsync")) { 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 4c1e06238..fd949b5b8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs @@ -256,7 +256,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries } } - private async Task FindFolderCoreAsync(DomainId appId, DomainId id, CancellationToken ct) + private async Task FindFolderCoreAsync(DomainId appId, DomainId id, + CancellationToken ct) { using (var timeout = new CancellationTokenSource(options.TimeoutFind)) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/RebuildFiles.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/RebuildFiles.cs index 1d6aaf181..d26e597d8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/RebuildFiles.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/RebuildFiles.cs @@ -33,7 +33,8 @@ namespace Squidex.Domain.Apps.Entities.Assets this.eventDataFormatter = eventDataFormatter; } - public async Task RepairAsync(CancellationToken ct = default) + public async Task RepairAsync( + CancellationToken ct = default) { await foreach (var storedEvent in eventStore.QueryAllAsync("^asset\\-", ct: ct)) { @@ -54,7 +55,8 @@ namespace Squidex.Domain.Apps.Entities.Assets } } - private async Task TryRepairAsync(NamedId appId, DomainId id, long fileVersion, CancellationToken ct) + private async Task TryRepairAsync(NamedId appId, DomainId id, long fileVersion, + CancellationToken ct) { try { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs index d865d4c0a..44f28e1ef 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Collections.Generic; using System.Threading.Tasks; using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.Repositories; @@ -23,7 +24,7 @@ namespace Squidex.Domain.Apps.Entities.Assets private readonly IAssetRepository assetRepository; private readonly IAssetFolderRepository assetFolderRepository; private readonly ISemanticLog log; - private readonly string? folderDeletedType; + private readonly HashSet consumingTypes; public string Name { @@ -47,12 +48,16 @@ namespace Squidex.Domain.Apps.Entities.Assets this.assetFolderRepository = assetFolderRepository; this.log = log; - folderDeletedType = typeNameRegistry?.GetName(); + // Compute the event types names once for performance reasons and use hashset for extensibility. + consumingTypes = new HashSet + { + typeNameRegistry.GetName() + }; } public bool Handles(StoredEvent @event) { - return @event.Data.Type == folderDeletedType; + return consumingTypes.Contains(@event.Data.Type); } public async Task On(Envelope @event) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetFolderRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetFolderRepository.cs index dac077fc7..629da0d65 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetFolderRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetFolderRepository.cs @@ -14,10 +14,13 @@ namespace Squidex.Domain.Apps.Entities.Assets.Repositories { public interface IAssetFolderRepository { - Task> QueryAsync(DomainId appId, DomainId parentId, CancellationToken ct = default); + Task> QueryAsync(DomainId appId, DomainId parentId, + CancellationToken ct = default); - Task> QueryChildIdsAsync(DomainId appId, DomainId parentId, CancellationToken ct = default); + Task> QueryChildIdsAsync(DomainId appId, DomainId parentId, + CancellationToken ct = default); - Task FindAssetFolderAsync(DomainId appId, DomainId id, CancellationToken ct = default); + Task FindAssetFolderAsync(DomainId appId, DomainId id, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs index 5ec3cfc90..0561799b7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs @@ -14,20 +14,28 @@ namespace Squidex.Domain.Apps.Entities.Assets.Repositories { public interface IAssetRepository { - IAsyncEnumerable StreamAll(DomainId appId, CancellationToken ct = default); + IAsyncEnumerable StreamAll(DomainId appId, + CancellationToken ct = default); - Task> QueryAsync(DomainId appId, DomainId? parentId, Q q, CancellationToken ct = default); + Task> QueryAsync(DomainId appId, DomainId? parentId, Q q, + CancellationToken ct = default); - Task> QueryIdsAsync(DomainId appId, HashSet ids, CancellationToken ct = default); + Task> QueryIdsAsync(DomainId appId, HashSet ids, + CancellationToken ct = default); - Task> QueryChildIdsAsync(DomainId appId, DomainId parentId, CancellationToken ct = default); + Task> QueryChildIdsAsync(DomainId appId, DomainId parentId, + CancellationToken ct = default); - Task FindAssetByHashAsync(DomainId appId, string hash, string fileName, long fileSize, CancellationToken ct = default); + Task FindAssetByHashAsync(DomainId appId, string hash, string fileName, long fileSize, + CancellationToken ct = default); - Task FindAssetBySlugAsync(DomainId appId, string slug, CancellationToken ct = default); + Task FindAssetBySlugAsync(DomainId appId, string slug, + CancellationToken ct = default); - Task FindAssetAsync(DomainId id, CancellationToken ct = default); + Task FindAssetAsync(DomainId id, + CancellationToken ct = default); - Task FindAssetAsync(DomainId appId, DomainId id, CancellationToken ct = default); + Task FindAssetAsync(DomainId appId, DomainId id, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/SvgAssetMetadataSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/SvgAssetMetadataSource.cs index 6fad2ff36..c088e2e74 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/SvgAssetMetadataSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/SvgAssetMetadataSource.cs @@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { private const int FileSizeLimit = 2 * 1024 * 1024; // 2MB - public Task EnhanceAsync(UploadAssetCommand command) + public async Task EnhanceAsync(UploadAssetCommand command) { var isSvg = command.File.MimeType == "image/svg+xml" || @@ -36,7 +36,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { using (var reader = new StreamReader(command.File.OpenRead())) { - var text = reader.ReadToEnd(); + var text = await reader.ReadToEndAsync(); if (!text.IsValidSvg()) { @@ -50,12 +50,10 @@ namespace Squidex.Domain.Apps.Entities.Assets } catch { - return Task.CompletedTask; + return; } } } - - return Task.CompletedTask; } public IEnumerable Format(IAssetEntity asset) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs index cc9b5204e..5fac90163 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs @@ -80,6 +80,20 @@ namespace Squidex.Domain.Apps.Entities.Backup await state.WriteAsync(); } + public async Task ClearAsync() + { + foreach (var backup in state.Value.Jobs) + { +#pragma warning disable MA0040 // Flow the cancellation token + await backupArchiveStore.DeleteAsync(backup.Id); +#pragma warning restore MA0040 // Flow the cancellation token + } + + TryDeactivateOnIdle(); + + await state.ClearAsync(); + } + public async Task BackupAsync(RefToken actor) { if (currentJobToken != null) @@ -106,7 +120,9 @@ namespace Squidex.Domain.Apps.Entities.Backup 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, @@ -126,7 +142,7 @@ namespace Squidex.Domain.Apps.Entities.Backup { var appId = DomainId.Create(Key); - using (var stream = backupArchiveLocation.OpenStream(job.Id)) + await using (var stream = backupArchiveLocation.OpenStream(job.Id)) { using (var writer = await backupArchiveLocation.OpenWriterAsync(stream)) { @@ -147,7 +163,7 @@ namespace Squidex.Domain.Apps.Entities.Backup foreach (var handler in handlers) { - await handler.BackupEventAsync(@event, context); + await handler.BackupEventAsync(@event, context, ct); } writer.WriteEvent(storedEvent); @@ -162,7 +178,7 @@ namespace Squidex.Domain.Apps.Entities.Backup { ct.ThrowIfCancellationRequested(); - await handler.BackupAsync(context); + await handler.BackupAsync(context, ct); } foreach (var handler in handlers) @@ -230,7 +246,7 @@ namespace Squidex.Domain.Apps.Entities.Backup public async Task DeleteAsync(DomainId id) { - var job = state.Value.Jobs.FirstOrDefault(x => x.Id == id); + var job = state.Value.Jobs.Find(x => x.Id == id); if (job == null) { @@ -258,7 +274,9 @@ namespace Squidex.Domain.Apps.Entities.Backup { 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) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs index 05ac223b3..95226cb3f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs @@ -6,8 +6,11 @@ // ========================================================================== using System; +using System.Collections.Generic; using System.IO; using System.IO.Compression; +using System.Runtime.CompilerServices; +using System.Threading; using System.Threading.Tasks; using Squidex.Domain.Apps.Entities.Backup.Helpers; using Squidex.Domain.Apps.Entities.Backup.Model; @@ -18,7 +21,7 @@ using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.Backup { - public class BackupReader : DisposableObjectBase, IBackupReader + public sealed class BackupReader : DisposableObjectBase, IBackupReader { private readonly ZipArchive archive; private readonly IJsonSerializer serializer; @@ -52,28 +55,26 @@ namespace Squidex.Domain.Apps.Entities.Backup } } - public Task ReadJsonAsync(string name) + public Task OpenBlobAsync(string name, + CancellationToken ct = default) { Guard.NotNullOrEmpty(name, nameof(name)); var entry = GetEntry(name); - using (var stream = entry.Open()) - { - return Task.FromResult(serializer.Deserialize(stream, null)); - } + return Task.FromResult(entry.Open()); } - public async Task ReadBlobAsync(string name, Func handler) + public async Task ReadJsonAsync(string name, + CancellationToken ct = default) { Guard.NotNullOrEmpty(name, nameof(name)); - Guard.NotNull(handler, nameof(handler)); var entry = GetEntry(name); - using (var stream = entry.Open()) + await using (var stream = entry.Open()) { - await handler(stream); + return serializer.Deserialize(stream, null); } } @@ -91,13 +92,13 @@ namespace Squidex.Domain.Apps.Entities.Backup return attachmentEntry; } - public async Task ReadEventsAsync(IStreamNameResolver streamNameResolver, IEventDataFormatter formatter, Func<(string Stream, Envelope Event), Task> handler) + public async IAsyncEnumerable<(string Stream, Envelope Event)> ReadEventsAsync(IStreamNameResolver streamNameResolver, IEventDataFormatter formatter, + [EnumeratorCancellation] CancellationToken ct = default) { - Guard.NotNull(handler, nameof(handler)); Guard.NotNull(formatter, nameof(formatter)); Guard.NotNull(streamNameResolver, nameof(streamNameResolver)); - while (true) + while (!ct.IsCancellationRequested) { var entry = archive.GetEntry(ArchiveHelper.GetEventPath(readEvents)); @@ -106,14 +107,14 @@ namespace Squidex.Domain.Apps.Entities.Backup break; } - using (var stream = entry.Open()) + await using (var stream = entry.Open()) { var storedEvent = serializer.Deserialize(stream).ToStoredEvent(); var eventStream = storedEvent.StreamName; var eventEnvelope = formatter.Parse(storedEvent); - await handler((eventStream, eventEnvelope)); + yield return (eventStream, eventEnvelope); } readEvents++; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupService.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupService.cs index 8c3ac5f5b..bbde84eb6 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupService.cs @@ -7,14 +7,16 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Orleans; +using Squidex.Domain.Apps.Entities.Apps; using Squidex.Infrastructure; using Squidex.Infrastructure.Orleans; namespace Squidex.Domain.Apps.Entities.Backup { - public sealed class BackupService : IBackupService + public sealed class BackupService : IBackupService, IDeleter { private readonly IGrainFactory grainFactory; @@ -23,52 +25,60 @@ namespace Squidex.Domain.Apps.Entities.Backup this.grainFactory = grainFactory; } - public Task StartBackupAsync(DomainId appId, RefToken actor) + Task IDeleter.DeleteAppAsync(IAppEntity app, + CancellationToken ct) { - var grain = grainFactory.GetGrain(appId.ToString()); + return BackupGrain(app.Id).ClearAsync(); + } - return grain.BackupAsync(actor); + public Task StartBackupAsync(DomainId appId, RefToken actor) + { + return BackupGrain(appId).BackupAsync(actor); } public Task StartRestoreAsync(RefToken actor, Uri url, string? newAppName) { - var grain = grainFactory.GetGrain(SingleGrain.Id); - - return grain.RestoreAsync(url, actor, newAppName); + return RestoreGrain().RestoreAsync(url, actor, newAppName); } - public async Task GetRestoreAsync() + public Task DeleteBackupAsync(DomainId appId, DomainId backupId, + CancellationToken ct = default) { - var grain = grainFactory.GetGrain(SingleGrain.Id); + return BackupGrain(appId).DeleteAsync(backupId); + } - var state = await grain.GetStateAsync(); + public async Task GetRestoreAsync( + CancellationToken ct = default) + { + var state = await RestoreGrain().GetStateAsync(); return state.Value; } - public async Task> GetBackupsAsync(DomainId appId) + public async Task> GetBackupsAsync(DomainId appId, + CancellationToken ct = default) { - var grain = grainFactory.GetGrain(appId.ToString()); - - var state = await grain.GetStateAsync(); + var state = await BackupGrain(appId).GetStateAsync(); return state.Value; } - public async Task GetBackupAsync(DomainId appId, DomainId backupId) + public async Task GetBackupAsync(DomainId appId, DomainId backupId, + CancellationToken ct = default) { - var grain = grainFactory.GetGrain(appId.ToString()); - - var state = await grain.GetStateAsync(); + var state = await BackupGrain(appId).GetStateAsync(); return state.Value.Find(x => x.Id == backupId); } - public Task DeleteBackupAsync(DomainId appId, DomainId backupId) + private IRestoreGrain RestoreGrain() { - var grain = grainFactory.GetGrain(appId.ToString()); + return grainFactory.GetGrain(SingleGrain.Id); + } - return grain.DeleteAsync(backupId); + private IBackupGrain BackupGrain(DomainId appId) + { + return grainFactory.GetGrain(appId.ToString()); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupWriter.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupWriter.cs index e4254e079..3b8d728a6 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupWriter.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupWriter.cs @@ -8,6 +8,7 @@ using System; using System.IO; using System.IO.Compression; +using System.Threading; using System.Threading.Tasks; using Squidex.Domain.Apps.Entities.Backup.Helpers; using Squidex.Domain.Apps.Entities.Backup.Model; @@ -57,38 +58,40 @@ namespace Squidex.Domain.Apps.Entities.Backup } } - public Task WriteJsonAsync(string name, object value) + public Task OpenBlobAsync(string name, + CancellationToken ct = default) { Guard.NotNullOrEmpty(name, nameof(name)); - var attachmentEntry = archive.CreateEntry(ArchiveHelper.GetAttachmentPath(name)); - - using (var stream = attachmentEntry.Open()) - { - serializer.Serialize(value, stream); - } - writtenAttachments++; - return Task.CompletedTask; + var entry = GetEntry(name); + + return Task.FromResult(entry.Open()); } - public async Task WriteBlobAsync(string name, Func handler) + public async Task WriteJsonAsync(string name, object value, + CancellationToken ct = default) { Guard.NotNullOrEmpty(name, nameof(name)); - Guard.NotNull(handler, nameof(handler)); - var attachmentEntry = archive.CreateEntry(ArchiveHelper.GetAttachmentPath(name)); + writtenAttachments++; + + var entry = GetEntry(name); - using (var stream = attachmentEntry.Open()) + await using (var stream = entry.Open()) { - await handler(stream); + serializer.Serialize(value, stream); } + } - writtenAttachments++; + private ZipArchiveEntry GetEntry(string name) + { + return archive.CreateEntry(ArchiveHelper.GetAttachmentPath(name)); } - public void WriteEvent(StoredEvent storedEvent) + public void WriteEvent(StoredEvent storedEvent, + CancellationToken ct = default) { Guard.NotNull(storedEvent, nameof(storedEvent)); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/CompatibilityExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/CompatibilityExtensions.cs index 34feff9f9..4248e331e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/CompatibilityExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/CompatibilityExtensions.cs @@ -15,7 +15,9 @@ namespace Squidex.Domain.Apps.Entities.Backup private static readonly FileVersion None = new FileVersion(); private static readonly FileVersion Expected = new FileVersion { Major = 5 }; +#pragma warning disable MA0077 // A class that provides Equals(T) should implement IEquatable public sealed class FileVersion +#pragma warning restore MA0077 // A class that provides Equals(T) should implement IEquatable { public int Major { get; set; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/DefaultBackupArchiveStore.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/DefaultBackupArchiveStore.cs index 0bed53d05..5f2f08d90 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/DefaultBackupArchiveStore.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/DefaultBackupArchiveStore.cs @@ -22,25 +22,28 @@ namespace Squidex.Domain.Apps.Entities.Backup this.assetStore = assetStore; } - public Task DownloadAsync(DomainId backupId, Stream stream, CancellationToken ct = default) + public Task DownloadAsync(DomainId backupId, Stream stream, + CancellationToken ct = default) { var fileName = GetFileName(backupId); return assetStore.DownloadAsync(fileName, stream, default, ct); } - public Task UploadAsync(DomainId backupId, Stream stream, CancellationToken ct = default) + public Task UploadAsync(DomainId backupId, Stream stream, + CancellationToken ct = default) { var fileName = GetFileName(backupId); return assetStore.UploadAsync(fileName, stream, true, ct); } - public Task DeleteAsync(DomainId backupId) + public Task DeleteAsync(DomainId backupId, + CancellationToken ct = default) { var fileName = GetFileName(backupId); - return assetStore.DeleteAsync(fileName); + return assetStore.DeleteAsync(fileName, ct); } private static string GetFileName(DomainId backupId) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupArchiveStore.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupArchiveStore.cs index bbe812178..495842ebc 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupArchiveStore.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupArchiveStore.cs @@ -14,10 +14,13 @@ namespace Squidex.Domain.Apps.Entities.Backup { public interface IBackupArchiveStore { - Task UploadAsync(DomainId backupId, Stream stream, CancellationToken ct = default); + Task UploadAsync(DomainId backupId, Stream stream, + CancellationToken ct = default); - Task DownloadAsync(DomainId backupId, Stream stream, CancellationToken ct = default); + Task DownloadAsync(DomainId backupId, Stream stream, + CancellationToken ct = default); - Task DeleteAsync(DomainId backupId); + Task DeleteAsync(DomainId backupId, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupGrain.cs index 803a7898d..3ec798e6f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupGrain.cs @@ -19,6 +19,8 @@ namespace Squidex.Domain.Apps.Entities.Backup Task DeleteAsync(DomainId id); + Task ClearAsync(); + Task>> GetStateAsync(); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupHandler.cs index 8c5d60ab0..6ae396a50 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupHandler.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupHandler.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Threading; using System.Threading.Tasks; using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; @@ -15,22 +16,26 @@ namespace Squidex.Domain.Apps.Entities.Backup { string Name { get; } - public Task RestoreEventAsync(Envelope @event, RestoreContext context) + public Task RestoreEventAsync(Envelope @event, RestoreContext context, + CancellationToken ct) { return Task.FromResult(true); } - public Task BackupEventAsync(Envelope @event, BackupContext context) + public Task BackupEventAsync(Envelope @event, BackupContext context, + CancellationToken ct) { return Task.CompletedTask; } - public Task RestoreAsync(RestoreContext context) + public Task RestoreAsync(RestoreContext context, + CancellationToken ct) { return Task.CompletedTask; } - public Task BackupAsync(BackupContext context) + public Task BackupAsync(BackupContext context, + CancellationToken ct) { return Task.CompletedTask; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupReader.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupReader.cs index fe252db22..08fc1b9e8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupReader.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupReader.cs @@ -6,7 +6,9 @@ // ========================================================================== using System; +using System.Collections.Generic; using System.IO; +using System.Threading; using System.Threading.Tasks; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.States; @@ -19,10 +21,13 @@ namespace Squidex.Domain.Apps.Entities.Backup int ReadEvents { get; } - Task ReadBlobAsync(string name, Func handler); + Task OpenBlobAsync(string name, + CancellationToken ct = default); - Task ReadEventsAsync(IStreamNameResolver streamNameResolver, IEventDataFormatter formatter, Func<(string Stream, Envelope Event), Task> handler); + Task ReadJsonAsync(string name, + CancellationToken ct = default); - Task ReadJsonAsync(string name); + IAsyncEnumerable<(string Stream, Envelope Event)> ReadEventsAsync(IStreamNameResolver streamNameResolver, IEventDataFormatter formatter, + CancellationToken ct = default); } -} \ No newline at end of file +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupService.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupService.cs index afb2f3db5..66e5f7b15 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupService.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Squidex.Infrastructure; @@ -18,12 +19,16 @@ namespace Squidex.Domain.Apps.Entities.Backup Task StartRestoreAsync(RefToken actor, Uri url, string? newAppName); - Task GetRestoreAsync(); + Task GetRestoreAsync( + CancellationToken ct = default); - Task> GetBackupsAsync(DomainId appId); + Task> GetBackupsAsync(DomainId appId, + CancellationToken ct = default); - Task GetBackupAsync(DomainId appId, DomainId backupId); + Task GetBackupAsync(DomainId appId, DomainId backupId, + CancellationToken ct = default); - Task DeleteBackupAsync(DomainId appId, DomainId backupId); + Task DeleteBackupAsync(DomainId appId, DomainId backupId, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupWriter.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupWriter.cs index 15b630b3b..3e26f2c47 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupWriter.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupWriter.cs @@ -7,6 +7,7 @@ using System; using System.IO; +using System.Threading; using System.Threading.Tasks; using Squidex.Infrastructure.EventSourcing; @@ -18,10 +19,13 @@ namespace Squidex.Domain.Apps.Entities.Backup int WrittenEvents { get; } - Task WriteBlobAsync(string name, Func handler); + Task OpenBlobAsync(string name, + CancellationToken ct = default); - void WriteEvent(StoredEvent storedEvent); + void WriteEvent(StoredEvent storedEvent, + CancellationToken ct = default); - Task WriteJsonAsync(string name, object value); + Task WriteJsonAsync(string name, object value, + CancellationToken ct = default); } -} \ No newline at end of file +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs index 291678386..848d4d50c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading; using System.Threading.Tasks; using System.Threading.Tasks.Dataflow; using Microsoft.Extensions.DependencyInjection; @@ -92,7 +93,7 @@ namespace Squidex.Domain.Apps.Entities.Backup } } - public async Task RestoreAsync(Uri url, RefToken actor, string? newAppName) + public async Task RestoreAsync(Uri url, RefToken actor, string? newAppName = null) { Guard.NotNull(url, nameof(url)); Guard.NotNull(actor, nameof(actor)); @@ -119,7 +120,9 @@ namespace Squidex.Domain.Apps.Entities.Backup 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() @@ -136,6 +139,8 @@ namespace Squidex.Domain.Apps.Entities.Backup jobUrl: CurrentJob.Url.ToString() ); + var ct = default(CancellationToken); + using (Telemetry.Activities.StartActivity("RestoreBackup")) { try @@ -165,7 +170,7 @@ namespace Squidex.Domain.Apps.Entities.Backup { using (Telemetry.Activities.StartActivity($"{handler.GetType().Name}/RestoreAsync")) { - await handler.RestoreAsync(runningContext); + await handler.RestoreAsync(runningContext, ct); } Log($"Restored {handler.Name}"); @@ -346,22 +351,26 @@ namespace Squidex.Domain.Apps.Entities.Backup batchBlock.BidirectionalLinkTo(writeBlock); - await reader.ReadEventsAsync(streamNameResolver, eventDataFormatter, async job => + await foreach (var job in reader.ReadEventsAsync(streamNameResolver, eventDataFormatter)) { var newStream = await HandleEventAsync(reader, handlers, job.Stream, job.Event); if (newStream != null) { - await batchBlock.SendAsync((newStream, job.Event)); + 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) + private async Task HandleEventAsync(IBackupReader reader, IEnumerable handlers, string stream, Envelope @event, + CancellationToken ct = default) { if (@event.Payload is AppCreated appCreated) { @@ -401,7 +410,7 @@ namespace Squidex.Domain.Apps.Entities.Backup foreach (var handler in handlers) { - if (!await handler.RestoreEventAsync(@event, runningContext)) + if (!await handler.RestoreEventAsync(@event, runningContext, ct)) { return null; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs index 0dbe14c36..5dd7cd98d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs @@ -29,7 +29,7 @@ namespace Squidex.Domain.Apps.Entities.Backup { Stream stream; - if (string.Equals(url.Scheme, "file")) + if (string.Equals(url.Scheme, "file", StringComparison.OrdinalIgnoreCase)) { stream = new FileStream(url.LocalPath, FileMode.Open, FileAccess.Read); } @@ -47,7 +47,7 @@ namespace Squidex.Domain.Apps.Entities.Backup response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); response.EnsureSuccessStatusCode(); - using (var sourceStream = await response.Content.ReadAsStreamAsync()) + await using (var sourceStream = await response.Content.ReadAsStreamAsync()) { await sourceStream.CopyToAsync(stream); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/UserMapping.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/UserMapping.cs index 42fb69dae..fe0ffe5a0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/UserMapping.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/UserMapping.cs @@ -7,13 +7,14 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Squidex.Infrastructure; using Squidex.Shared.Users; namespace Squidex.Domain.Apps.Entities.Backup { - public class UserMapping : IUserMapping + public sealed class UserMapping : IUserMapping { private const string UsersFile = "Users.json"; private readonly Dictionary userMap = new Dictionary(); @@ -53,28 +54,30 @@ namespace Squidex.Domain.Apps.Entities.Backup } } - public async Task StoreAsync(IBackupWriter writer, IUserResolver userResolver) + public async Task StoreAsync(IBackupWriter writer, IUserResolver userResolver, + CancellationToken ct = default) { Guard.NotNull(writer, nameof(writer)); Guard.NotNull(userResolver, nameof(userResolver)); - var users = await userResolver.QueryManyAsync(userMap.Keys.ToArray()); + var users = await userResolver.QueryManyAsync(userMap.Keys.ToArray(), ct); var json = users.ToDictionary(x => x.Key, x => x.Value.Email); - await writer.WriteJsonAsync(UsersFile, json); + await writer.WriteJsonAsync(UsersFile, json, ct); } - public async Task RestoreAsync(IBackupReader reader, IUserResolver userResolver) + public async Task RestoreAsync(IBackupReader reader, IUserResolver userResolver, + CancellationToken ct = default) { Guard.NotNull(reader, nameof(reader)); Guard.NotNull(userResolver, nameof(userResolver)); - var json = await reader.ReadJsonAsync>(UsersFile); + var json = await reader.ReadJsonAsync>(UsersFile, ct); foreach (var (userId, email) in json) { - var (user, _) = await userResolver.CreateUserIfNotExistsAsync(email, false); + var (user, _) = await userResolver.CreateUserIfNotExistsAsync(email, false, ct); if (user != null) { 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 019e161cc..429e82d12 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/CommentsCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/CommentsCommandMiddleware.cs @@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject { public sealed class CommentsCommandMiddleware : ICommandMiddleware { - 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, TimeSpan.FromMilliseconds(100)); + 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; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/Guards/GuardComments.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/Guards/GuardComments.cs index 5a7afcb38..8b5428c12 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/Guards/GuardComments.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/Guards/GuardComments.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; using Squidex.Domain.Apps.Entities.Comments.Commands; using Squidex.Domain.Apps.Events.Comments; @@ -36,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject.Guards var comment = FindComment(events, command.CommentId); - if (!string.Equals(commentsId, command.Actor.Identifier) && !comment.Payload.Actor.Equals(command.Actor)) + if (!string.Equals(commentsId, command.Actor.Identifier, StringComparison.Ordinal) && !comment.Payload.Actor.Equals(command.Actor)) { throw new DomainException(T.Get("comments.notUserComment")); } @@ -56,7 +57,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject.Guards var comment = FindComment(events, command.CommentId); - if (!string.Equals(commentsId, command.Actor.Identifier) && !comment.Payload.Actor.Equals(command.Actor)) + if (!string.Equals(commentsId, command.Actor.Identifier, StringComparison.Ordinal) && !comment.Payload.Actor.Equals(command.Actor)) { throw new DomainException(T.Get("comments.notUserComment")); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs index 705875606..ae720f3e6 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/BackupContents.cs @@ -5,8 +5,10 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Contents; @@ -61,23 +63,25 @@ namespace Squidex.Domain.Apps.Entities.Contents this.urlGenerator = urlGenerator; } - public async Task BackupEventAsync(Envelope @event, BackupContext context) + public async Task BackupEventAsync(Envelope @event, BackupContext context, + CancellationToken ct) { if (@event.Payload is AppCreated appCreated) { var urls = GetUrls(appCreated.Name); - await context.Writer.WriteJsonAsync(UrlsFile, urls); + await context.Writer.WriteJsonAsync(UrlsFile, urls, ct); } } - public async Task RestoreEventAsync(Envelope @event, RestoreContext context) + public async Task RestoreEventAsync(Envelope @event, RestoreContext context, + CancellationToken ct) { switch (@event.Payload) { case AppCreated appCreated: assetsUrlNew = GetUrls(appCreated.Name); - assetsUrlOld = await ReadUrlsAsync(context.Reader); + assetsUrlOld = await ReadUrlsAsync(context.Reader, ct); break; case SchemaDeleted schemaDeleted: contentIdsBySchemaId.Remove(schemaDeleted.SchemaId.Id); @@ -127,7 +131,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { var newValue = s.Value; - newValue = newValue.Replace(assetsUrlOld!.AssetsApp, assetsUrlNew!.AssetsApp); + newValue = newValue.Replace(assetsUrlOld!.AssetsApp, assetsUrlNew!.AssetsApp, StringComparison.Ordinal); if (!ReferenceEquals(newValue, s.Value)) { @@ -136,7 +140,7 @@ namespace Squidex.Domain.Apps.Entities.Contents break; } - newValue = newValue.Replace(assetsUrlOld!.Assets, assetsUrlNew!.Assets); + newValue = newValue.Replace(assetsUrlOld!.Assets, assetsUrlNew!.Assets, StringComparison.Ordinal); if (!ReferenceEquals(newValue, s.Value)) { @@ -179,7 +183,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { var newValue = s.Value; - newValue = newValue.Replace(assetsUrlOld!.AssetsApp, assetsUrlNew!.AssetsApp); + newValue = newValue.Replace(assetsUrlOld!.AssetsApp, assetsUrlNew!.AssetsApp, StringComparison.Ordinal); if (!ReferenceEquals(newValue, s.Value)) { @@ -187,7 +191,7 @@ namespace Squidex.Domain.Apps.Entities.Contents break; } - newValue = newValue.Replace(assetsUrlOld!.Assets, assetsUrlNew!.Assets); + newValue = newValue.Replace(assetsUrlOld!.Assets, assetsUrlNew!.Assets, StringComparison.Ordinal); if (!ReferenceEquals(newValue, s.Value)) { @@ -208,21 +212,23 @@ namespace Squidex.Domain.Apps.Entities.Contents } } - public async Task RestoreAsync(RestoreContext context) + public async Task RestoreAsync(RestoreContext context, + CancellationToken ct) { var ids = contentIdsBySchemaId.Values.SelectMany(x => x); if (ids.Any()) { - await rebuilder.InsertManyAsync(ids, BatchSize); + await rebuilder.InsertManyAsync(ids, BatchSize, ct); } } - private static async Task ReadUrlsAsync(IBackupReader reader) + private static async Task ReadUrlsAsync(IBackupReader reader, + CancellationToken ct) { try { - return await reader.ReadJsonAsync(UrlsFile); + return await reader.ReadJsonAsync(UrlsFile, ct); } catch { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs index 1e9f311e3..fd95ed2ef 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs @@ -51,7 +51,7 @@ namespace Squidex.Domain.Apps.Entities.Contents } public async IAsyncEnumerable CreateSnapshotEventsAsync(RuleContext context, - [EnumeratorCancellation] CancellationToken ct = default) + [EnumeratorCancellation] CancellationToken ct) { var trigger = (ContentChangedTriggerV2)context.Rule.Trigger; @@ -77,7 +77,7 @@ namespace Squidex.Domain.Apps.Entities.Contents } public async IAsyncEnumerable CreateEnrichedEventsAsync(Envelope @event, RuleContext context, - [EnumeratorCancellation] CancellationToken ct = default) + [EnumeratorCancellation] CancellationToken ct) { var contentEvent = (ContentEvent)@event.Payload; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs index bb20a5a69..f65e61986 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs @@ -25,7 +25,6 @@ namespace Squidex.Domain.Apps.Entities.Contents private readonly ICommandBus commandBus; private readonly IClock clock; private readonly ISemanticLog log; - private TaskScheduler scheduler; public ContentSchedulerGrain( IContentRepository contentRepository, @@ -43,8 +42,6 @@ namespace Squidex.Domain.Apps.Entities.Contents public override Task OnActivateAsync() { - scheduler = TaskScheduler.Current; - DelayDeactivation(TimeSpan.FromDays(1)); RegisterOrUpdateReminder("Default", TimeSpan.Zero, TimeSpan.FromMinutes(10)); @@ -58,58 +55,55 @@ namespace Squidex.Domain.Apps.Entities.Contents return Task.CompletedTask; } - public Task PublishAsync() + public async Task PublishAsync() { var now = clock.GetCurrentInstant(); - return contentRepository.QueryScheduledWithoutDataAsync(now, content => + await foreach (var content in contentRepository.QueryScheduledWithoutDataAsync(now)) { - return Dispatch(async () => - { - var id = content.Id; + await TryPublishAsync(content); + } + } - try - { - var job = content.ScheduleJob; - - if (job != null) - { - var command = new ChangeContentStatus - { - Actor = job.ScheduledBy, - AppId = content.AppId, - ContentId = id, - SchemaId = content.SchemaId, - Status = job.Status, - StatusJobId = job.Id - }; - - await commandBus.PublishAsync(command); - } - } - catch (DomainObjectNotFoundException) - { - await contentRepository.ResetScheduledAsync(content.UniqueId, default); - } - catch (Exception ex) + private async Task TryPublishAsync(IContentEntity content) + { + var id = content.Id; + + try + { + var job = content.ScheduleJob; + + if (job != null) + { + var command = new ChangeContentStatus { - log.LogError(ex, content.Id.ToString(), (logContentId, w) => w - .WriteProperty("action", "ChangeStatusScheduled") - .WriteProperty("status", "Failed") - .WriteProperty("contentId", logContentId)); - } - }); - }, default); + Actor = job.ScheduledBy, + AppId = content.AppId, + ContentId = id, + SchemaId = content.SchemaId, + Status = job.Status, + StatusJobId = job.Id + }; + + await commandBus.PublishAsync(command); + } + } + catch (DomainObjectNotFoundException) + { + await contentRepository.ResetScheduledAsync(content.UniqueId, default); + } + catch (Exception ex) + { + log.LogError(ex, content.Id.ToString(), (logContentId, w) => w + .WriteProperty("action", "ChangeStatusScheduled") + .WriteProperty("status", "Failed") + .WriteProperty("contentId", logContentId)); + } } public Task ReceiveReminder(string reminderName, TickStatus status) { return Task.CompletedTask; } - - private Task Dispatch(Func task) - { - return Task.Factory.StartNew(task, CancellationToken.None, TaskCreationOptions.None, scheduler ?? TaskScheduler.Default).Unwrap(); - } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentsSearchSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentsSearchSource.cs index 764a63449..acb7c1ba6 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentsSearchSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentsSearchSource.cs @@ -46,7 +46,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { var result = new SearchResults(); - var searchFilter = await CreateSearchFilterAsync(context); + var searchFilter = await CreateSearchFilterAsync(context, ct); if (searchFilter == null) { @@ -55,7 +55,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var textQuery = new TextQuery($"{query}~", searchFilter); - var ids = await contentTextIndexer.SearchAsync(context.App, textQuery, context.Scope()); + var ids = await contentTextIndexer.SearchAsync(context.App, textQuery, context.Scope(), ct); if (ids == null || ids.Count == 0) { @@ -78,11 +78,12 @@ namespace Squidex.Domain.Apps.Entities.Contents return result; } - private async Task CreateSearchFilterAsync(Context context) + private async Task CreateSearchFilterAsync(Context context, + CancellationToken ct) { var allowedSchemas = new List(); - var schemas = await appProvider.GetSchemasAsync(context.App.Id); + var schemas = await appProvider.GetSchemasAsync(context.App.Id, ct); foreach (var schema in schemas) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterDeleter.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterDeleter.cs new file mode 100644 index 000000000..68bae84e7 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterDeleter.cs @@ -0,0 +1,32 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using Orleans; +using 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 index 71ba48291..ec4d4ac40 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterGrain.cs @@ -27,6 +27,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.Counter this.state = state; } + public Task ClearAsync() + { + TryDeactivateOnIdle(); + + return state.ClearAsync(); + } + public Task IncrementAsync(string name) { state.Value.Counters.TryGetValue(name, out var value); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/ICounterGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/ICounterGrain.cs index 39551a7bc..fa814073c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/ICounterGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/ICounterGrain.cs @@ -15,5 +15,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Counter Task IncrementAsync(string name); Task ResetAsync(string name, long value); + + Task ClearAsync(); } } 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 7458c39e8..4180dd66a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentCommandMiddleware.cs @@ -31,7 +31,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject if (payload is IContentEntity content && payload is not IEnrichedContentEntity) { - payload = await contentEnricher.EnrichAsync(content, true, contextProvider.Context); + payload = await contentEnricher.EnrichAsync(content, true, contextProvider.Context, default); } 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 5ce52beb5..4c93c19e0 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 @@ -28,6 +28,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject public ScheduleJob? ScheduleJob { get; set; } + public bool IsDeleted { get; set; } + [IgnoreDataMember] public DomainId UniqueId { 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 449d64fad..097b8f28e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentsBulkUpdateCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentsBulkUpdateCommandMiddleware.cs @@ -74,7 +74,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject // Dataflow swallows operation cancelled exception. throw new AggregateException(ex); } - }, executionOptions); + }, executionOptions); var executeCommandBlock = new ActionBlock(async command => { @@ -111,7 +111,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject bulkUpdates, results); - await createCommandsBlock.SendAsync(task); + if (!await createCommandsBlock.SendAsync(task)) + { + break; + } } createCommandsBlock.Complete(); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/SecurityExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/SecurityExtensions.cs index c99ac2065..2dc019756 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/SecurityExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/SecurityExtensions.cs @@ -32,7 +32,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards var permission = Permissions.ForApp(permissionId, context.App.Name, context.Schema.SchemaDef.Name); - if (permissions.Allows(permission) != true) + if (!permissions.Allows(permission)) { throw new DomainForbiddenException(T.Get("common.errorNoPermission")); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs index 463330f73..7c75c9ca7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Globalization; using System.Threading.Tasks; using GraphQL; using Microsoft.Extensions.DependencyInjection; @@ -69,7 +70,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL return CreateModelAsync(app); } - var cacheKey = CreateCacheKey(app.Id, app.Version.ToString()); + var cacheKey = CreateCacheKey(app.Id, app.Version.ToString(CultureInfo.InvariantCulture)); return cache.GetOrCreateAsync(cacheKey, CacheDuration, async entry => { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/DefaultDocumentWriter.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/DefaultDocumentWriter.cs index 9b15fcd32..b3f442300 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/DefaultDocumentWriter.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/DefaultDocumentWriter.cs @@ -23,7 +23,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL this.jsonSerializer = jsonSerializer; } - public async Task WriteAsync(Stream stream, T value, CancellationToken cancellationToken = default) + public async Task WriteAsync(Stream stream, T value, + CancellationToken cancellationToken = default) { await using (var buffer = new FileBufferingWriteStream()) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs index 611ee15e5..6f60fb39d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using GraphQL.DataLoader; using Squidex.Domain.Apps.Core; @@ -56,7 +57,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL Log = log; } - public async Task FindUserAsync(RefToken refToken) + public async Task FindUserAsync(RefToken refToken, + CancellationToken ct) { if (refToken.IsClient) { @@ -66,25 +68,28 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL { var dataLoader = GetUserLoader(); - return await dataLoader.LoadAsync(refToken.Identifier).GetResultAsync(); + return await dataLoader.LoadAsync(refToken.Identifier).GetResultAsync(ct); } } - public async Task FindAssetAsync(DomainId id) + public async Task FindAssetAsync(DomainId id, + CancellationToken ct) { var dataLoader = GetAssetsLoader(); - return await dataLoader.LoadAsync(id).GetResultAsync(); + return await dataLoader.LoadAsync(id).GetResultAsync(ct); } - public async Task FindContentAsync(DomainId id) + public async Task FindContentAsync(DomainId id, + CancellationToken ct) { var dataLoader = GetContentsLoader(); - return await dataLoader.LoadAsync(id).GetResultAsync(); + return await dataLoader.LoadAsync(id).GetResultAsync(ct); } - public Task> GetReferencedAssetsAsync(IJsonValue value) + public Task> GetReferencedAssetsAsync(IJsonValue value, + CancellationToken ct) { var ids = ParseIds(value); @@ -95,10 +100,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var dataLoader = GetAssetsLoader(); - return LoadManyAsync(dataLoader, ids); + return LoadManyAsync(dataLoader, ids, ct); } - public Task> GetReferencedContentsAsync(IJsonValue value) + public Task> GetReferencedContentsAsync(IJsonValue value, + CancellationToken ct) { var ids = ParseIds(value); @@ -109,15 +115,15 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var dataLoader = GetContentsLoader(); - return LoadManyAsync(dataLoader, ids); + return LoadManyAsync(dataLoader, ids, ct); } private IDataLoader GetAssetsLoader() { return dataLoaders.Context.GetOrAddBatchLoader(nameof(GetAssetsLoader), - async batch => + async (batch, ct) => { - var result = await GetReferencedAssetsAsync(new List(batch)); + var result = await GetReferencedAssetsAsync(new List(batch), ct); return result.ToDictionary(x => x.Id); }); @@ -126,9 +132,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL private IDataLoader GetContentsLoader() { return dataLoaders.Context.GetOrAddBatchLoader(nameof(GetContentsLoader), - async batch => + async (batch, ct) => { - var result = await GetReferencedContentsAsync(new List(batch)); + var result = await GetReferencedContentsAsync(new List(batch), ct); return result.ToDictionary(x => x.Id); }); @@ -137,17 +143,18 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL private IDataLoader GetUserLoader() { return dataLoaders.Context.GetOrAddBatchLoader(nameof(GetUserLoader), - async batch => + async (batch, ct) => { - var result = await userResolver.QueryManyAsync(batch.ToArray()); + var result = await userResolver.QueryManyAsync(batch.ToArray(), ct); return result; }); } - private static async Task> LoadManyAsync(IDataLoader dataLoader, ICollection keys) where T : class + private static async Task> LoadManyAsync(IDataLoader dataLoader, ICollection keys, + CancellationToken ct) where T : class { - var contents = await Task.WhenAll(keys.Select(x => dataLoader.LoadAsync(x).GetResultAsync())); + var contents = await Task.WhenAll(keys.Select(x => dataLoader.LoadAsync(x).GetResultAsync(ct))); return contents.NotNull().ToList(); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs index a0e9cd796..1328cb44d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs @@ -57,7 +57,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets { var assetId = fieldContext.GetArgument("id"); - return await context.FindAssetAsync(assetId); + return await context.FindAssetAsync(assetId, + fieldContext.CancellationToken); }); } @@ -97,7 +98,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets var q = Q.Empty.WithODataQuery(query).WithoutTotal(); - return await context.QueryAssetsAsync(q); + return await context.QueryAssetsAsync(q, + fieldContext.CancellationToken); }); public static readonly IFieldResolver ResolverWithTotal = Resolvers.Async(async (_, fieldContext, context) => @@ -106,7 +108,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets var q = Q.Empty.WithODataQuery(query); - return await context.QueryAssetsAsync(q); + return await context.QueryAssetsAsync(q, + fieldContext.CancellationToken); }); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ComponentGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ComponentGraphType.cs index 4ced5c670..78cc72c6b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ComponentGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ComponentGraphType.cs @@ -65,7 +65,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents { return value => { - return Component.IsValid(value as IJsonValue, out var discrimiator) && discrimiator == schemaId.ToString(); + return Component.IsValid(value as IJsonValue, out var discrimiator) && discrimiator == schemaId; }; } } 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 f62974a34..f1c45f3b6 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 @@ -83,11 +83,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents if (version >= 0) { - return await context.FindContentAsync(fieldContext.FieldDefinition.SchemaId(), contentId, version.Value); + return await context.FindContentAsync(fieldContext.FieldDefinition.SchemaId(), contentId, version.Value, + fieldContext.CancellationToken); } else { - return await context.FindContentAsync(contentId); + return await context.FindContentAsync(contentId, + fieldContext.CancellationToken); } }); } @@ -134,7 +136,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents var q = Q.Empty.WithODataQuery(query).WithoutTotal(); - return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId(), q); + return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId(), q, + fieldContext.CancellationToken); }); public static readonly IFieldResolver QueryWithTotal = Resolvers.Async(async (_, fieldContext, context) => @@ -143,7 +146,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents var q = Q.Empty.WithODataQuery(query); - return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId(), q); + return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId(), q, + fieldContext.CancellationToken); }); public static readonly IFieldResolver Referencing = Resolvers.Async(async (source, fieldContext, context) => @@ -152,7 +156,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents var q = Q.Empty.WithODataQuery(query).WithReference(source.Id).WithoutTotal(); - return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId(), q); + return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId(), q, + fieldContext.CancellationToken); }); public static readonly IFieldResolver ReferencingWithTotal = Resolvers.Async(async (source, fieldContext, context) => @@ -161,7 +166,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents var q = Q.Empty.WithODataQuery(query).WithReference(source.Id); - return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId(), q); + return await context.QueryContentsAsync(fieldContext.FieldDefinition.SchemaId(), q, + fieldContext.CancellationToken); }); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldVisitor.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldVisitor.cs index aa8f2b087..464b88ca1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldVisitor.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldVisitor.cs @@ -80,14 +80,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents } }); - private static readonly IFieldResolver Assets = CreateValueResolver((value, _, context) => + private static readonly IFieldResolver Assets = CreateValueResolver((value, fieldContext, context) => { - return context.GetReferencedAssetsAsync(value); + return context.GetReferencedAssetsAsync(value, fieldContext.CancellationToken); }); - private static readonly IFieldResolver References = CreateValueResolver((value, _, context) => + private static readonly IFieldResolver References = CreateValueResolver((value, fieldContext, context) => { - return context.GetReferencedContentsAsync(value); + return context.GetReferencedContentsAsync(value, fieldContext.CancellationToken); }); private readonly Builder builder; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/EntityResolvers.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/EntityResolvers.cs index ae2aecf95..c33f7a9c6 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/EntityResolvers.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/EntityResolvers.cs @@ -30,7 +30,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives private static IFieldResolver ResolveUser(Func resolver) { - return Resolvers.Async((source, _, context) => context.FindUserAsync(resolver(source))); + return Resolvers.Async((source, fieldContext, context) => context.FindUserAsync(resolver(source), fieldContext.CancellationToken)); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs index 167204054..7d2542c99 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs @@ -14,14 +14,19 @@ namespace Squidex.Domain.Apps.Entities.Contents { public interface IContentQueryService { - Task> QueryAsync(Context context, Q q, CancellationToken ct = default); + Task> QueryAsync(Context context, Q q, + CancellationToken ct = default); - Task> QueryAsync(Context context, string schemaIdOrName, Q query, CancellationToken ct = default); + Task> QueryAsync(Context context, string schemaIdOrName, Q query, + CancellationToken ct = default); - Task FindAsync(Context context, string schemaIdOrName, DomainId id, long version = EtagVersion.Any, CancellationToken ct = default); + Task FindAsync(Context context, string schemaIdOrName, DomainId id, long version = EtagVersion.Any, + CancellationToken ct = default); - Task GetSchemaOrThrowAsync(Context context, string schemaIdOrName); + Task GetSchemaOrThrowAsync(Context context, string schemaIdOrName, + CancellationToken ct = default); - Task GetSchemaAsync(Context context, string schemaIdOrNama); + Task GetSchemaAsync(Context context, string schemaIdOrNama, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs index 337de08a9..6b4ed43b9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs @@ -84,7 +84,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { return schemaCache.GetOrAdd(id, async x => { - var schema = await appProvider.GetSchemaAsync(context.App.Id, x, false); + var schema = await appProvider.GetSchemaAsync(context.App.Id, x, false, ct); if (schema == null) { 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 a9082d753..7fa920a93 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs @@ -21,7 +21,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries this.grainFactory = grainFactory; } - public async Task GetAsync(DomainId appId, DomainId id, long version) + public async Task GetAsync(DomainId appId, DomainId id, long version = EtagVersion.Any) { using (Telemetry.Activities.StartActivity("ContentLoader/GetAsync")) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs index 8a4cc3a58..161c82eaa 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs @@ -54,11 +54,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries using (Telemetry.Activities.StartActivity("ContentQueryService/FindAsync")) { - var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName); + var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName, ct); IContentEntity? content; - if (id.ToString().Equals(SingletonId)) + if (id.ToString().Equals(SingletonId, StringComparison.Ordinal)) { id = schema.Id; } @@ -93,7 +93,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries return EmptyContents; } - var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName); + var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName, ct); if (!HasPermission(context, schema, Permissions.AppContentsRead)) { @@ -125,7 +125,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries return EmptyContents; } - var schemas = await GetSchemasAsync(context); + var schemas = await GetSchemasAsync(context, ct); if (schemas.Count == 0) { @@ -170,9 +170,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries } } - public async Task GetSchemaOrThrowAsync(Context context, string schemaIdOrName) + public async Task GetSchemaOrThrowAsync(Context context, string schemaIdOrName, + CancellationToken ct = default) { - var schema = await GetSchemaAsync(context, schemaIdOrName); + var schema = await GetSchemaAsync(context, schemaIdOrName, ct); if (schema == null) { @@ -182,7 +183,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries return schema; } - public async Task GetSchemaAsync(Context context, string schemaIdOrName) + public async Task GetSchemaAsync(Context context, string schemaIdOrName, + CancellationToken ct = default) { Guard.NotNull(context, nameof(context)); Guard.NotNullOrEmpty(schemaIdOrName, nameof(schemaIdOrName)); @@ -195,12 +197,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { var schemaId = DomainId.Create(guid); - schema = await appProvider.GetSchemaAsync(context.App.Id, schemaId, canCache); + schema = await appProvider.GetSchemaAsync(context.App.Id, schemaId, canCache, ct); } if (schema == null) { - schema = await appProvider.GetSchemaAsync(context.App.Id, schemaIdOrName, canCache); + schema = await appProvider.GetSchemaAsync(context.App.Id, schemaIdOrName, canCache, ct); } if (schema != null && !HasPermission(context, schema, Permissions.AppContentsReadOwn)) @@ -211,9 +213,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries return schema; } - private async Task> GetSchemasAsync(Context context) + private async Task> GetSchemasAsync(Context context, + CancellationToken ct) { - var schemas = await appProvider.GetSchemasAsync(context.App.Id); + var schemas = await appProvider.GetSchemasAsync(context.App.Id, ct); return schemas.Where(x => IsAccessible(x) && HasPermission(context, x, Permissions.AppContentsReadOwn)).ToList(); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/IContentEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/IContentEnricher.cs index 74c775f4d..8b6c18528 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/IContentEnricher.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/IContentEnricher.cs @@ -13,8 +13,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { public interface IContentEnricher { - Task EnrichAsync(IContentEntity content, bool cloneData, Context context, CancellationToken ct = default); + Task EnrichAsync(IContentEntity content, bool cloneData, Context context, + CancellationToken ct); - Task> EnrichAsync(IEnumerable contents, Context context, CancellationToken ct = default); + Task> EnrichAsync(IEnumerable contents, Context context, + CancellationToken ct); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/IContentEnricherStep.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/IContentEnricherStep.cs index b658fcfe5..d15bcead6 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/IContentEnricherStep.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/IContentEnricherStep.cs @@ -18,9 +18,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries public interface IContentEnricherStep { - Task EnrichAsync(Context context, IEnumerable contents, ProvideSchema schemas, CancellationToken ct); + Task EnrichAsync(Context context, IEnumerable contents, ProvideSchema schemas, + CancellationToken ct); - Task EnrichAsync(Context context, CancellationToken ct) + Task EnrichAsync(Context context, + CancellationToken ct) { return Task.CompletedTask; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs index df22094a0..233158edb 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs @@ -34,19 +34,21 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries this.contentQuery = contentQuery; } - public virtual Task FindContentAsync(string schemaIdOrName, DomainId id, long version) + public virtual Task FindContentAsync(string schemaIdOrName, DomainId id, long version, + CancellationToken ct) { - return contentQuery.FindAsync(Context, schemaIdOrName, id, version); + return contentQuery.FindAsync(Context, schemaIdOrName, id, version, ct); } - public virtual async Task> QueryAssetsAsync(Q q) + public virtual async Task> QueryAssetsAsync(Q q, + CancellationToken ct) { IResultList assets; - await maxRequests.WaitAsync(); + await maxRequests.WaitAsync(ct); try { - assets = await assetQuery.QueryAsync(Context, null, q); + assets = await assetQuery.QueryAsync(Context, null, q, ct); } finally { @@ -61,14 +63,15 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries return assets; } - public virtual async Task> QueryContentsAsync(string schemaIdOrName, Q q) + public virtual async Task> QueryContentsAsync(string schemaIdOrName, Q q, + CancellationToken ct) { IResultList contents; - await maxRequests.WaitAsync(); + await maxRequests.WaitAsync(ct); try { - contents = await contentQuery.QueryAsync(Context, schemaIdOrName, q); + contents = await contentQuery.QueryAsync(Context, schemaIdOrName, q, ct); } finally { @@ -83,7 +86,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries return contents; } - public virtual async Task> GetReferencedAssetsAsync(ICollection ids) + public virtual async Task> GetReferencedAssetsAsync(ICollection ids, + CancellationToken ct) { Guard.NotNull(ids, nameof(ids)); @@ -93,10 +97,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { IResultList assets; - await maxRequests.WaitAsync(); + await maxRequests.WaitAsync(ct); try { - assets = await assetQuery.QueryAsync(Context, null, Q.Empty.WithIds(notLoadedAssets).WithoutTotal()); + assets = await assetQuery.QueryAsync(Context, null, Q.Empty.WithIds(notLoadedAssets).WithoutTotal(), ct); } finally { @@ -112,7 +116,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries return ids.Select(cachedAssets.GetOrDefault).NotNull().ToList(); } - public virtual async Task> GetReferencedContentsAsync(ICollection ids) + public virtual async Task> GetReferencedContentsAsync(ICollection ids, + CancellationToken ct) { Guard.NotNull(ids, nameof(ids)); @@ -122,10 +127,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { IResultList contents; - await maxRequests.WaitAsync(); + await maxRequests.WaitAsync(ct); try { - contents = await contentQuery.QueryAsync(Context, Q.Empty.WithIds(notLoadedContents).WithoutTotal()); + contents = await contentQuery.QueryAsync(Context, Q.Empty.WithIds(notLoadedContents).WithoutTotal(), ct); } finally { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs index c42b83d8f..2d1f28f7a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs @@ -20,22 +20,31 @@ namespace Squidex.Domain.Apps.Entities.Contents.Repositories { public interface IContentRepository { - IAsyncEnumerable StreamAll(DomainId appId, HashSet? schemaIds, CancellationToken ct = default); + IAsyncEnumerable StreamAll(DomainId appId, HashSet? schemaIds, + CancellationToken ct = default); - Task> QueryAsync(IAppEntity app, List schemas, Q q, SearchScope scope, CancellationToken ct = default); + Task> QueryAsync(IAppEntity app, List schemas, Q q, SearchScope scope, + CancellationToken ct = default); - Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Q q, SearchScope scope, CancellationToken ct = default); + Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Q q, SearchScope scope, + CancellationToken ct = default); - Task> QueryIdsAsync(DomainId appId, DomainId schemaId, FilterNode filterNode, CancellationToken ct = default); + Task> QueryIdsAsync(DomainId appId, DomainId schemaId, FilterNode filterNode, + CancellationToken ct = default); - Task> QueryIdsAsync(DomainId appId, HashSet ids, SearchScope scope, CancellationToken ct = default); + Task> QueryIdsAsync(DomainId appId, HashSet ids, SearchScope scope, + CancellationToken ct = default); - Task FindContentAsync(IAppEntity app, ISchemaEntity schema, DomainId id, SearchScope scope, CancellationToken ct = default); + Task FindContentAsync(IAppEntity app, ISchemaEntity schema, DomainId id, SearchScope scope, + CancellationToken ct = default); - Task HasReferrersAsync(DomainId appId, DomainId contentId, SearchScope scope, CancellationToken ct = default); + Task HasReferrersAsync(DomainId appId, DomainId contentId, SearchScope scope, + CancellationToken ct = default); - Task ResetScheduledAsync(DomainId documentId, CancellationToken ct = default); + Task ResetScheduledAsync(DomainId documentId, + CancellationToken ct = default); - Task QueryScheduledWithoutDataAsync(Instant now, Func callback, CancellationToken ct = default); + IAsyncEnumerable QueryScheduledWithoutDataAsync(Instant now, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Elastic/ElasticSearchMapping.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Elastic/ElasticSearchMapping.cs index f0fabc326..626ff6bc1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Elastic/ElasticSearchMapping.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Elastic/ElasticSearchMapping.cs @@ -15,7 +15,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text.Elastic { public static class ElasticSearchMapping { - public static async Task ApplyAsync(IElasticLowLevelClient elastic, string indexName, CancellationToken ct = default) + public static async Task ApplyAsync(IElasticLowLevelClient elastic, string indexName, + CancellationToken ct = default) { var query = new { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Elastic/ElasticSearchTextIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Elastic/ElasticSearchTextIndex.cs index 05a9ac38b..2e6efc7ed 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Elastic/ElasticSearchTextIndex.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Elastic/ElasticSearchTextIndex.cs @@ -36,17 +36,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text.Elastic this.waitForTesting = waitForTesting; } - public Task InitializeAsync(CancellationToken ct = default) + public Task InitializeAsync( + CancellationToken ct) { return ElasticSearchMapping.ApplyAsync(client, indexName, ct); } - public Task ClearAsync() + public Task ClearAsync( + CancellationToken ct = default) { return Task.CompletedTask; } - public async Task ExecuteAsync(params IndexCommand[] commands) + public async Task ExecuteAsync(IndexCommand[] commands, + CancellationToken ct = default) { var args = new List(); @@ -57,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text.Elastic if (args.Count > 0) { - var result = await client.BulkAsync(PostData.MultiJson(args)); + var result = await client.BulkAsync(PostData.MultiJson(args), ctx: ct); if (!result.Success) { @@ -67,16 +70,18 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text.Elastic if (waitForTesting) { - await Task.Delay(1000); + await Task.Delay(1000, ct); } } - public Task?> SearchAsync(IAppEntity app, GeoQuery query, SearchScope scope) + public Task?> SearchAsync(IAppEntity app, GeoQuery query, SearchScope scope, + CancellationToken ct = default) { return Task.FromResult?>(null); } - public async Task?> SearchAsync(IAppEntity app, TextQuery query, SearchScope scope) + public async Task?> SearchAsync(IAppEntity app, TextQuery query, SearchScope scope, + CancellationToken ct = default) { Guard.NotNull(app, nameof(app)); Guard.NotNull(query, nameof(query)); @@ -176,7 +181,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text.Elastic } } - var result = await client.SearchAsync(indexName, CreatePost(elasticQuery)); + var result = await client.SearchAsync(indexName, CreatePost(elasticQuery), ctx: ct); if (!result.Success) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndex.cs index 507b5988e..b22561b02 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndex.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/ITextIndex.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Infrastructure; @@ -14,12 +15,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text { public interface ITextIndex { - Task?> SearchAsync(IAppEntity app, TextQuery query, SearchScope scope); + Task?> SearchAsync(IAppEntity app, TextQuery query, SearchScope scope, + CancellationToken ct = default); - Task?> SearchAsync(IAppEntity app, GeoQuery query, SearchScope scope); + Task?> SearchAsync(IAppEntity app, GeoQuery query, SearchScope scope, + CancellationToken ct = default); - Task ClearAsync(); + Task ClearAsync( + CancellationToken ct = default); - Task ExecuteAsync(params IndexCommand[] commands); + Task ExecuteAsync(IndexCommand[] commands, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/CachingTextIndexerState.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/CachingTextIndexerState.cs index 6d5f953bb..698fa184d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/CachingTextIndexerState.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/CachingTextIndexerState.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Squidex.Caching; using Squidex.Infrastructure; @@ -25,14 +26,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text.State this.inner = inner; } - public async Task ClearAsync() + public async Task ClearAsync( + CancellationToken ct = default) { - await inner.ClearAsync(); + await inner.ClearAsync(ct); cache.Clear(); } - public async Task> GetAsync(HashSet ids) + public async Task> GetAsync(HashSet ids, + CancellationToken ct = default) { Guard.NotNull(ids, nameof(ids)); @@ -57,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text.State if (missingIds.Count > 0) { - var fromInner = await inner.GetAsync(missingIds); + var fromInner = await inner.GetAsync(missingIds, ct); foreach (var (id, state) in fromInner) { @@ -75,7 +78,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text.State return result; } - public Task SetAsync(List updates) + public Task SetAsync(List updates, + CancellationToken ct = default) { Guard.NotNull(updates, nameof(updates)); @@ -91,7 +95,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text.State } } - return inner.SetAsync(updates); + return inner.SetAsync(updates, ct); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/ITextIndexerState.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/ITextIndexerState.cs index 9552b5055..f828f9fe7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/ITextIndexerState.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/ITextIndexerState.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Squidex.Infrastructure; @@ -13,10 +14,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text.State { public interface ITextIndexerState { - Task> GetAsync(HashSet ids); + Task> GetAsync(HashSet ids, + CancellationToken ct = default); - Task SetAsync(List updates); + Task SetAsync(List updates, + CancellationToken ct = default); - Task ClearAsync(); + Task ClearAsync( + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/InMemoryTextIndexerState.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/InMemoryTextIndexerState.cs index cb59532e3..2b6214793 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/InMemoryTextIndexerState.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/InMemoryTextIndexerState.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Squidex.Infrastructure; @@ -15,14 +16,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text.State { private readonly Dictionary states = new Dictionary(); - public Task ClearAsync() + public Task ClearAsync( + CancellationToken ct = default) { states.Clear(); return Task.CompletedTask; } - public Task> GetAsync(HashSet ids) + public Task> GetAsync(HashSet ids, + CancellationToken ct = default) { Guard.NotNull(ids, nameof(ids)); @@ -39,7 +42,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text.State return Task.FromResult(result); } - public Task SetAsync(List updates) + public Task SetAsync(List updates, + CancellationToken ct = default) { Guard.NotNull(updates, nameof(updates)); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/TextContentState.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/TextContentState.cs index 59737259b..fb103a862 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/TextContentState.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/TextContentState.cs @@ -5,12 +5,15 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Contents.Text.State { public sealed class TextContentState { + public DomainId AppId { get; set; } + public DomainId UniqueContentId { get; set; } public string DocIdCurrent { get; set; } @@ -23,7 +26,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text.State public void GenerateDocIdNew() { - if (DocIdCurrent?.EndsWith("_2") != false) + if (DocIdCurrent?.EndsWith("_2", StringComparison.Ordinal) != false) { DocIdNew = $"{UniqueContentId}_1"; } @@ -35,7 +38,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text.State public void GenerateDocIdCurrent() { - if (DocIdNew?.EndsWith("_2") != false) + if (DocIdNew?.EndsWith("_2", StringComparison.Ordinal) != false) { DocIdCurrent = $"{UniqueContentId}_1"; } 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 38a5822a7..4d659608d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexingProcess.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexingProcess.cs @@ -118,6 +118,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text var state = new TextContentState { + AppId = @event.AppId.Id, UniqueContentId = uniqueId }; diff --git a/backend/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs b/backend/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs index a91aeb994..21bd41d84 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/DomainObjectState.cs @@ -25,8 +25,6 @@ namespace Squidex.Domain.Apps.Entities public Instant LastModified { get; set; } - public bool IsDeleted { get; set; } - public long Version { get; set; } protected DomainObjectState() diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs b/backend/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs index a746ef0a9..1e32e91bd 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/History/HistoryService.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Squidex.Domain.Apps.Entities.History.Repositories; using Squidex.Domain.Apps.Events; @@ -98,9 +99,10 @@ namespace Squidex.Domain.Apps.Entities.History } } - public async Task> QueryByChannelAsync(DomainId appId, string channelPrefix, int count) + public async Task> QueryByChannelAsync(DomainId appId, string channelPrefix, int count, + CancellationToken ct = default) { - var items = await repository.QueryByChannelAsync(appId, channelPrefix, count); + var items = await repository.QueryByChannelAsync(appId, channelPrefix, count, ct); return items.Select(x => new ParsedHistoryEvent(x, texts)).ToList(); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/IHistoryService.cs b/backend/src/Squidex.Domain.Apps.Entities/History/IHistoryService.cs index a5a2d7e4e..ddd0e7f3e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/History/IHistoryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/History/IHistoryService.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Squidex.Infrastructure; @@ -13,6 +14,7 @@ namespace Squidex.Domain.Apps.Entities.History { public interface IHistoryService { - Task> QueryByChannelAsync(DomainId appId, string channelPrefix, int count); + Task> QueryByChannelAsync(DomainId appId, string channelPrefix, int count, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/ParsedHistoryEvent.cs b/backend/src/Squidex.Domain.Apps.Entities/History/ParsedHistoryEvent.cs index 6dd90f3a2..1e785828b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/History/ParsedHistoryEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/History/ParsedHistoryEvent.cs @@ -65,7 +65,7 @@ namespace Squidex.Domain.Apps.Entities.History foreach (var (key, value) in item.Parameters) { - result = result.Replace("[" + key + "]", value); + result = result.Replace("[" + key + "]", value, StringComparison.Ordinal); } return result; diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/Repositories/IHistoryEventRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/History/Repositories/IHistoryEventRepository.cs index d146eb114..2673f0828 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/History/Repositories/IHistoryEventRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/History/Repositories/IHistoryEventRepository.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Squidex.Infrastructure; @@ -13,10 +14,13 @@ namespace Squidex.Domain.Apps.Entities.History.Repositories { public interface IHistoryEventRepository { - Task> QueryByChannelAsync(DomainId appId, string channelPrefix, int count); + Task> QueryByChannelAsync(DomainId appId, string channelPrefix, int count, + CancellationToken ct = default); - Task InsertManyAsync(IEnumerable historyEvents); + Task InsertManyAsync(IEnumerable historyEvents, + CancellationToken ct = default); - Task ClearAsync(); + Task ClearAsync( + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/IAppProvider.cs b/backend/src/Squidex.Domain.Apps.Entities/IAppProvider.cs index 5a30918c7..22e079446 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/IAppProvider.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/IAppProvider.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Rules; @@ -17,22 +18,31 @@ namespace Squidex.Domain.Apps.Entities { public interface IAppProvider { - Task<(IAppEntity?, ISchemaEntity?)> GetAppWithSchemaAsync(DomainId appId, DomainId id, bool canCache = false); + Task<(IAppEntity?, ISchemaEntity?)> GetAppWithSchemaAsync(DomainId appId, DomainId id, bool canCache = false, + CancellationToken ct = default); - Task GetAppAsync(DomainId appId, bool canCache = false); + Task GetAppAsync(DomainId appId, bool canCache = false, + CancellationToken ct = default); - Task GetAppAsync(string appName, bool canCache = false); + Task GetAppAsync(string appName, bool canCache = false, + CancellationToken ct = default); - Task> GetUserAppsAsync(string userId, PermissionSet permissions); + Task> GetUserAppsAsync(string userId, PermissionSet permissions, + CancellationToken ct = default); - Task GetSchemaAsync(DomainId appId, DomainId id, bool canCache = false); + Task GetSchemaAsync(DomainId appId, DomainId id, bool canCache = false, + CancellationToken ct = default); - Task GetSchemaAsync(DomainId appId, string name, bool canCache = false); + Task GetSchemaAsync(DomainId appId, string name, bool canCache = false, + CancellationToken ct = default); - Task> GetSchemasAsync(DomainId appId); + Task> GetSchemasAsync(DomainId appId, + CancellationToken ct = default); - Task> GetRulesAsync(DomainId appId); + Task> GetRulesAsync(DomainId appId, + CancellationToken ct = default); - Task GetRuleAsync(DomainId appId, DomainId id); + Task GetRuleAsync(DomainId appId, DomainId id, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/IDeleter.cs b/backend/src/Squidex.Domain.Apps.Entities/IDeleter.cs new file mode 100644 index 000000000..da139bf3f --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/IDeleter.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities +{ + public interface IDeleter + { + int Order => 0; + + Task DeleteAppAsync(IAppEntity app, + CancellationToken ct); + + Task DeleteContributorAsync(DomainId appId, string contributorId, + CancellationToken ct) + { + return Task.CompletedTask; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Notifications/NotificationEmailSender.cs b/backend/src/Squidex.Domain.Apps.Entities/Notifications/NotificationEmailSender.cs index 626e53ea0..824249d4b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Notifications/NotificationEmailSender.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Notifications/NotificationEmailSender.cs @@ -144,31 +144,31 @@ namespace Squidex.Domain.Apps.Entities.Notifications private static string Format(string text, TemplatesVars vars) { - text = text.Replace("$APP_NAME", vars.AppName); + text = text.Replace("$APP_NAME", vars.AppName, StringComparison.Ordinal); if (vars.Assigner != null) { - text = text.Replace("$ASSIGNER_EMAIL", vars.Assigner.Email); - text = text.Replace("$ASSIGNER_NAME", vars.Assigner.Claims.DisplayName()); + text = text.Replace("$ASSIGNER_EMAIL", vars.Assigner.Email, StringComparison.Ordinal); + text = text.Replace("$ASSIGNER_NAME", vars.Assigner.Claims.DisplayName(), StringComparison.Ordinal); } if (vars.User != null) { - text = text.Replace("$USER_EMAIL", vars.User.Email); - text = text.Replace("$USER_NAME", vars.User.Claims.DisplayName()); + text = text.Replace("$USER_EMAIL", vars.User.Email, StringComparison.Ordinal); + text = text.Replace("$USER_NAME", vars.User.Claims.DisplayName(), StringComparison.Ordinal); } if (vars.ApiCallsLimit != null) { - text = text.Replace("$API_CALLS_LIMIT", vars.ApiCallsLimit.ToString()); + text = text.Replace("$API_CALLS_LIMIT", vars.ApiCallsLimit.ToString(), StringComparison.Ordinal); } if (vars.ApiCalls != null) { - text = text.Replace("$API_CALLS", vars.ApiCalls.ToString()); + text = text.Replace("$API_CALLS", vars.ApiCalls.ToString(), StringComparison.Ordinal); } - text = text.Replace("$UI_URL", vars.URL); + text = text.Replace("$UI_URL", vars.URL, StringComparison.Ordinal); return text; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/BackupRules.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/BackupRules.cs index da9d4a37d..f492111aa 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/BackupRules.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/BackupRules.cs @@ -6,45 +6,53 @@ // ========================================================================== using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Squidex.Domain.Apps.Entities.Backup; -using Squidex.Domain.Apps.Entities.Rules.Indexes; +using Squidex.Domain.Apps.Entities.Rules.DomainObject; using Squidex.Domain.Apps.Events.Rules; using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; namespace Squidex.Domain.Apps.Entities.Rules { public sealed class BackupRules : IBackupHandler { + private const int BatchSize = 100; private readonly HashSet ruleIds = new HashSet(); - private readonly IRulesIndex indexForRules; + private readonly Rebuilder rebuilder; public string Name { get; } = "Rules"; - public BackupRules(IRulesIndex indexForRules) + public BackupRules(Rebuilder rebuilder) { - this.indexForRules = indexForRules; + this.rebuilder = rebuilder; } - public Task RestoreEventAsync(Envelope @event, RestoreContext context) + public Task RestoreEventAsync(Envelope @event, RestoreContext context, + CancellationToken ct) { switch (@event.Payload) { - case RuleCreated ruleCreated: - ruleIds.Add(ruleCreated.RuleId); + case RuleCreated: + ruleIds.Add(@event.Headers.AggregateId()); break; - case RuleDeleted ruleDeleted: - ruleIds.Remove(ruleDeleted.RuleId); + case RuleDeleted: + ruleIds.Remove(@event.Headers.AggregateId()); break; } return Task.FromResult(true); } - public Task RestoreAsync(RestoreContext context) + public async Task RestoreAsync(RestoreContext context, + CancellationToken ct) { - return indexForRules.RebuildAsync(context.AppId, ruleIds); + if (ruleIds.Count > 0) + { + await rebuilder.InsertManyAsync(ruleIds, BatchSize, ct); + } } } } 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 432d9a2bd..646724f6d 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 @@ -23,6 +23,8 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject public Rule RuleDef { get; set; } + public bool IsDeleted { get; set; } + [IgnoreDataMember] public DomainId UniqueId { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnricher.cs index bbf1f6e45..af9b2f33f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnricher.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnricher.cs @@ -6,14 +6,17 @@ // ========================================================================== using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; namespace Squidex.Domain.Apps.Entities.Rules { public interface IRuleEnricher { - Task EnrichAsync(IRuleEntity rule, Context context); + Task EnrichAsync(IRuleEntity rule, Context context, + CancellationToken ct); - Task> EnrichAsync(IEnumerable rules, Context context); + Task> EnrichAsync(IEnumerable rules, Context context, + CancellationToken ct); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleQueryService.cs index f3b4d501f..aa41cc398 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleQueryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleQueryService.cs @@ -6,12 +6,14 @@ // ========================================================================== using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; namespace Squidex.Domain.Apps.Entities.Rules { public interface IRuleQueryService { - Task> QueryAsync(Context context); + Task> QueryAsync(Context context, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesByAppIndexGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesCacheGrain.cs similarity index 64% rename from backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesByAppIndexGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesCacheGrain.cs index 0d3825cbf..3bb2baaf5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesByAppIndexGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesCacheGrain.cs @@ -5,13 +5,19 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Collections.Generic; +using System.Threading.Tasks; using Orleans; using Squidex.Infrastructure; -using Squidex.Infrastructure.Orleans.Indexes; namespace Squidex.Domain.Apps.Entities.Rules.Indexes { - public interface IRulesByAppIndexGrain : IIdsIndexGrain, IGrainWithStringKey + public interface IRulesCacheGrain : IGrainWithStringKey { + Task> GetRuleIdsAsync(); + + Task AddAsync(DomainId id); + + Task RemoveAsync(DomainId id); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesIndex.cs index 8a1995a3c..8e870a01d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesIndex.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesIndex.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Squidex.Infrastructure; @@ -13,8 +14,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Indexes { public interface IRulesIndex { - Task> GetRulesAsync(DomainId appId); - - Task RebuildAsync(DomainId appId, HashSet rules); + Task> GetRulesAsync(DomainId appId, + CancellationToken ct = default); } -} \ No newline at end of file +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexGrain.cs deleted file mode 100644 index 04633ae69..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexGrain.cs +++ /dev/null @@ -1,27 +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; -using Squidex.Infrastructure.Orleans.Indexes; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.Rules.Indexes -{ - public sealed class RulesByAppIndexGrain : IdsIndexGrain, IRulesByAppIndexGrain - { - public RulesByAppIndexGrain(IGrainState state) - : base(state) - { - } - } - - [CollectionName("Index_RulesByApp")] - public sealed class RulesByAppIndexState : IdsIndexState - { - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesCacheGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesCacheGrain.cs new file mode 100644 index 000000000..694e38ec7 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesCacheGrain.cs @@ -0,0 +1,58 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; +using Orleans.Concurrency; +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 : GrainOfString, IRulesCacheGrain + { + private readonly IRuleRepository ruleRepository; + private List? ruleIds; + + private DomainId AppId => DomainId.Create(Key); + + public RulesCacheGrain(IRuleRepository ruleRepository) + { + this.ruleRepository = ruleRepository; + } + + public async Task> GetRuleIdsAsync() + { + var ids = ruleIds; + + if (ids == null) + { + ids = await ruleRepository.QueryIdsAsync(AppId); + + 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 d03bd0f23..d051f9225 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesIndex.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesIndex.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Orleans; using Squidex.Domain.Apps.Entities.Rules.Commands; @@ -25,12 +26,8 @@ namespace Squidex.Domain.Apps.Entities.Rules.Indexes this.grainFactory = grainFactory; } - public Task RebuildAsync(DomainId appId, HashSet rules) - { - return Index(appId).RebuildAsync(rules); - } - - public async Task> GetRulesAsync(DomainId appId) + public async Task> GetRulesAsync(DomainId appId, + CancellationToken ct = default) { using (Telemetry.Activities.StartActivity("RulesIndex/GetRulesAsync")) { @@ -44,11 +41,11 @@ namespace Squidex.Domain.Apps.Entities.Rules.Indexes } } - private async Task> GetRuleIdsAsync(DomainId appId) + private async Task> GetRuleIdsAsync(DomainId appId) { using (Telemetry.Activities.StartActivity("RulesIndex/GetRuleIdsAsync")) { - return await Index(appId).GetIdsAsync(); + return await Cache(appId).GetRuleIdsAsync(); } } @@ -60,34 +57,29 @@ namespace Squidex.Domain.Apps.Entities.Rules.Indexes { switch (context.Command) { - case CreateRule createRule: - await CreateRuleAsync(createRule); + case CreateRule create: + await OnCreateAsync(create); break; - case DeleteRule deleteRule: - await DeleteRuleAsync(deleteRule); + case DeleteRule delete: + await OnDeleteAsync(delete); break; } } } - private async Task CreateRuleAsync(CreateRule command) + private async Task OnCreateAsync(CreateRule create) { - await Index(command.AppId.Id).AddAsync(command.RuleId); + await Cache(create.AppId.Id).AddAsync(create.RuleId); } - private async Task DeleteRuleAsync(DeleteRule command) + private async Task OnDeleteAsync(DeleteRule delete) { - var rule = await GetRuleCoreAsync(command.AggregateId); - - if (rule != null) - { - await Index(rule.AppId.Id).RemoveAsync(rule.Id); - } + await Cache(delete.AppId.Id).RemoveAsync(delete.RuleId); } - private IRulesByAppIndexGrain Index(DomainId appId) + private IRulesCacheGrain Cache(DomainId appId) { - return grainFactory.GetGrain(appId.ToString()); + return grainFactory.GetGrain(appId.ToString()); } private async Task GetRuleCoreAsync(DomainId id) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleEnricher.cs index 1565524f6..65636948a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleEnricher.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleEnricher.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Squidex.Domain.Apps.Entities.Rules.Repositories; using Squidex.Infrastructure; @@ -27,16 +28,18 @@ namespace Squidex.Domain.Apps.Entities.Rules.Queries this.requestCache = requestCache; } - public async Task EnrichAsync(IRuleEntity rule, Context context) + public async Task EnrichAsync(IRuleEntity rule, Context context, + CancellationToken ct) { Guard.NotNull(rule, nameof(rule)); - var enriched = await EnrichAsync(Enumerable.Repeat(rule, 1), context); + var enriched = await EnrichAsync(Enumerable.Repeat(rule, 1), context, ct); return enriched[0]; } - public async Task> EnrichAsync(IEnumerable rules, Context context) + public async Task> EnrichAsync(IEnumerable rules, Context context, + CancellationToken ct) { Guard.NotNull(rules, nameof(rules)); Guard.NotNull(context, nameof(context)); @@ -54,7 +57,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Queries foreach (var group in results.GroupBy(x => x.AppId.Id)) { - var statistics = await ruleEventRepository.QueryStatisticsByAppAsync(group.Key); + var statistics = await ruleEventRepository.QueryStatisticsByAppAsync(group.Key, ct); foreach (var rule in group) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleQueryService.cs index 91d63dfd9..90fa97219 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleQueryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Queries/RuleQueryService.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Squidex.Domain.Apps.Entities.Rules.Indexes; @@ -23,13 +24,14 @@ namespace Squidex.Domain.Apps.Entities.Rules.Queries this.ruleEnricher = ruleEnricher; } - public async Task> QueryAsync(Context context) + public async Task> QueryAsync(Context context, + CancellationToken ct = default) { - var rules = await rulesIndex.GetRulesAsync(context.App.Id); + var rules = await rulesIndex.GetRulesAsync(context.App.Id, ct); if (rules.Count > 0) { - var enriched = await ruleEnricher.EnrichAsync(rules, context); + var enriched = await ruleEnricher.EnrichAsync(rules, context, ct); return enriched; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.cs index 5a8b30077..309a315ff 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleEventRepository.cs @@ -18,11 +18,12 @@ namespace Squidex.Domain.Apps.Entities.Rules.Repositories { public interface IRuleEventRepository { - async Task EnqueueAsync(RuleJob job, Exception? ex) + async Task EnqueueAsync(RuleJob job, Exception? ex, + CancellationToken ct = default) { if (ex != null) { - await EnqueueAsync(job, (Instant?)null); + await EnqueueAsync(job, (Instant?)null, ct); await UpdateAsync(job, new RuleJobUpdate { @@ -30,32 +31,42 @@ namespace Squidex.Domain.Apps.Entities.Rules.Repositories ExecutionResult = RuleResult.Failed, ExecutionDump = ex.ToString(), Finished = job.Created - }); + }, ct); } else { - await EnqueueAsync(job, job.Created); + await EnqueueAsync(job, job.Created, ct); } } - Task UpdateAsync(RuleJob job, RuleJobUpdate update); + Task UpdateAsync(RuleJob job, RuleJobUpdate update, + CancellationToken ct = default); - Task EnqueueAsync(RuleJob job, Instant? nextAttempt); + Task EnqueueAsync(RuleJob job, Instant? nextAttempt, + CancellationToken ct = default); - Task EnqueueAsync(DomainId id, Instant nextAttempt); + Task EnqueueAsync(DomainId id, Instant nextAttempt, + CancellationToken ct = default); - Task CancelByEventAsync(DomainId eventId); + Task CancelByEventAsync(DomainId eventId, + CancellationToken ct = default); - Task CancelByRuleAsync(DomainId ruleId); + Task CancelByRuleAsync(DomainId ruleId, + CancellationToken ct = default); - Task CancelByAppAsync(DomainId appId); + Task CancelByAppAsync(DomainId appId, + CancellationToken ct = default); - Task QueryPendingAsync(Instant now, Func callback, CancellationToken ct = default); + Task QueryPendingAsync(Instant now, Func callback, + CancellationToken ct = default); - Task> QueryStatisticsByAppAsync(DomainId appId, CancellationToken ct = default); + Task> QueryStatisticsByAppAsync(DomainId appId, + CancellationToken ct = default); - Task> QueryByAppAsync(DomainId appId, DomainId? ruleId = null, int skip = 0, int take = 20, CancellationToken ct = default); + Task> QueryByAppAsync(DomainId appId, DomainId? ruleId = null, int skip = 0, int take = 20, + CancellationToken ct = default); - Task FindAsync(DomainId id, CancellationToken ct = default); + Task FindAsync(DomainId id, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Infrastructure/Orleans/Indexes/IIdsIndexGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs similarity index 60% rename from backend/src/Squidex.Infrastructure/Orleans/Indexes/IIdsIndexGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs index 4dd1427a3..681c1ca90 100644 --- a/backend/src/Squidex.Infrastructure/Orleans/Indexes/IIdsIndexGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs @@ -6,22 +6,15 @@ // ========================================================================== using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; +using Squidex.Infrastructure; -namespace Squidex.Infrastructure.Orleans.Indexes +namespace Squidex.Domain.Apps.Entities.Rules.Repositories { - public interface IIdsIndexGrain + public interface IRuleRepository { - Task CountAsync(); - - Task RebuildAsync(HashSet ids); - - Task AddAsync(T id); - - Task RemoveAsync(T id); - - Task ClearAsync(); - - Task> GetIdsAsync(); + Task> QueryIdsAsync(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 624363c14..8118b2eda 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs @@ -32,7 +32,7 @@ namespace Squidex.Domain.Apps.Entities.Rules if (payload is IRuleEntity rule && payload is not IEnrichedRuleEntity) { - payload = await ruleEnricher.EnrichAsync(rule, contextProvider.Context); + payload = await ruleEnricher.EnrichAsync(rule, contextProvider.Context, default); } return payload; 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 34ddd615b..b86832325 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/DefaultRuleRunnerService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/DefaultRuleRunnerService.cs @@ -37,7 +37,8 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner this.ruleService = ruleService; } - public async Task> SimulateAsync(IRuleEntity rule, CancellationToken ct) + public async Task> SimulateAsync(IRuleEntity rule, + CancellationToken ct = default) { Guard.NotNull(rule, nameof(rule)); @@ -54,7 +55,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner var fromNow = SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromDays(7)); - await foreach (var storedEvent in eventStore.QueryAllReverseAsync($"^([a-z]+)\\-{rule.AppId.Id}", fromNow, MaxSimulatedEvents, ct)) + await foreach (var storedEvent in eventStore.QueryAllReverseAsync($"^([a-zA-Z0-9]+)\\-{rule.AppId.Id}", fromNow, MaxSimulatedEvents, ct)) { var @event = eventDataFormatter.ParseIfKnown(storedEvent); @@ -87,13 +88,6 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner return simulatedEvents; } - public Task CancelAsync(DomainId appId) - { - var grain = grainFactory.GetGrain(appId.ToString()); - - return grain.CancelAsync(); - } - public bool CanRunRule(IRuleEntity rule) { var context = GetContext(rule); @@ -108,14 +102,24 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner return CanRunRule(rule) && ruleService.CanCreateSnapshotEvents(context); } - public Task GetRunningRuleIdAsync(DomainId appId) + public Task CancelAsync(DomainId appId, + CancellationToken ct = default) + { + var grain = grainFactory.GetGrain(appId.ToString()); + + return grain.CancelAsync(); + } + + public Task GetRunningRuleIdAsync(DomainId appId, + CancellationToken ct = default) { var grain = grainFactory.GetGrain(appId.ToString()); return grain.GetRunningRuleIdAsync(); } - public Task RunAsync(DomainId appId, DomainId ruleId, bool fromSnapshots = false) + public Task RunAsync(DomainId appId, DomainId ruleId, bool fromSnapshots = false, + CancellationToken ct = default) { var grain = grainFactory.GetGrain(appId.ToString()); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/IRuleRunnerService.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/IRuleRunnerService.cs index 5cb073914..23008f265 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/IRuleRunnerService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/IRuleRunnerService.cs @@ -14,16 +14,20 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner { public interface IRuleRunnerService { - Task> SimulateAsync(IRuleEntity rule, CancellationToken ct); + Task> SimulateAsync(IRuleEntity rule, + CancellationToken ct = default); - Task RunAsync(DomainId appId, DomainId ruleId, bool fromSnapshots = false); + Task RunAsync(DomainId appId, DomainId ruleId, bool fromSnapshots = false, + CancellationToken ct = default); - Task CancelAsync(DomainId appId); + Task CancelAsync(DomainId appId, + CancellationToken ct = default); + + Task GetRunningRuleIdAsync(DomainId appId, + CancellationToken ct = default); bool CanRunRule(IRuleEntity rule); bool CanRunFromSnapshots(IRuleEntity rule); - - Task GetRunningRuleIdAsync(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 index 6d750a48d..a5a73f96d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerGrain.cs @@ -137,23 +137,27 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner { 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) + private void Process(State job, + CancellationToken ct) { TaskExtensions.Forget(ProcessAsync(job, ct)); } - private async Task ProcessAsync(State currentState, CancellationToken ct) + private async Task ProcessAsync(State currentState, + CancellationToken ct) { try { currentReminder = await RegisterOrUpdateReminder("KeepAlive", TimeSpan.Zero, TimeSpan.FromMinutes(2)); - var rule = await appProvider.GetRuleAsync(DomainId.Create(Key), currentState.RuleId!.Value); + var rule = await appProvider.GetRuleAsync(DomainId.Create(Key), currentState.RuleId!.Value, ct); if (rule == null) { @@ -213,7 +217,8 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner } } - private async Task EnqueueFromSnapshotsAsync(RuleContext context, CancellationToken ct) + private async Task EnqueueFromSnapshotsAsync(RuleContext context, + CancellationToken ct) { var errors = 0; @@ -221,7 +226,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner { if (job.Job != null && job.SkipReason == SkipReason.None) { - await ruleEventRepository.EnqueueAsync(job.Job, job.EnrichmentError); + await ruleEventRepository.EnqueueAsync(job.Job, job.EnrichmentError, ct); } else if (job.EnrichmentError != null) { @@ -239,7 +244,8 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner } } - private async Task EnqueueFromEventsAsync(State currentState, RuleContext context, CancellationToken ct) + private async Task EnqueueFromEventsAsync(State currentState, RuleContext context, + CancellationToken ct) { var errors = 0; @@ -255,11 +261,11 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner { var jobs = ruleService.CreateJobsAsync(@event, context, ct); - await foreach (var job in jobs) + await foreach (var job in jobs.WithCancellation(ct)) { if (job.Job != null && job.SkipReason == SkipReason.None) { - await ruleEventRepository.EnqueueAsync(job.Job, job.EnrichmentError); + await ruleEventRepository.EnqueueAsync(job.Job, job.EnrichmentError, ct); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/BackupSchemas.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/BackupSchemas.cs index 4f69a8d29..25626e34f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/BackupSchemas.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/BackupSchemas.cs @@ -6,45 +6,53 @@ // ========================================================================== using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Squidex.Domain.Apps.Entities.Backup; -using Squidex.Domain.Apps.Entities.Schemas.Indexes; +using Squidex.Domain.Apps.Entities.Schemas.DomainObject; using Squidex.Domain.Apps.Events.Schemas; using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; namespace Squidex.Domain.Apps.Entities.Schemas { public sealed class BackupSchemas : IBackupHandler { - private readonly Dictionary schemasByName = new Dictionary(); - private readonly ISchemasIndex indexSchemas; + private const int BatchSize = 100; + private readonly HashSet schemaIds = new HashSet(); + private readonly Rebuilder rebuilder; public string Name { get; } = "Schemas"; - public BackupSchemas(ISchemasIndex indexSchemas) + public BackupSchemas(Rebuilder rebuilder) { - this.indexSchemas = indexSchemas; + this.rebuilder = rebuilder; } - public Task RestoreEventAsync(Envelope @event, RestoreContext context) + public Task RestoreEventAsync(Envelope @event, RestoreContext context, + CancellationToken ct) { switch (@event.Payload) { - case SchemaCreated schemaCreated: - schemasByName[schemaCreated.SchemaId.Name] = schemaCreated.SchemaId.Id; + case SchemaCreated: + schemaIds.Add(@event.Headers.AggregateId()); break; - case SchemaDeleted schemaDeleted: - schemasByName.Remove(schemaDeleted.SchemaId.Name); + case SchemaDeleted: + schemaIds.Remove(@event.Headers.AggregateId()); break; } return Task.FromResult(true); } - public Task RestoreAsync(RestoreContext context) + public async Task RestoreAsync(RestoreContext context, + CancellationToken ct) { - return indexSchemas.RebuildAsync(context.AppId, schemasByName); + if (schemaIds.Count > 0) + { + await rebuilder.InsertManyAsync(schemaIds, BatchSize, ct); + } } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/GuardHelper.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/GuardHelper.cs index be8abf16a..c9e9630bd 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/GuardHelper.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/GuardHelper.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Globalization; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.Translations; @@ -17,7 +18,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards { if (!schema.FieldsById.TryGetValue(parentId, out var rootField) || rootField is not IArrayField arrayField) { - throw new DomainObjectNotFoundException(parentId.ToString()); + throw new DomainObjectNotFoundException(parentId.ToString(CultureInfo.InvariantCulture)); } if (!allowLocked) @@ -36,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards if (!arrayField.FieldsById.TryGetValue(fieldId, out var nestedField)) { - throw new DomainObjectNotFoundException(fieldId.ToString()); + throw new DomainObjectNotFoundException(fieldId.ToString(CultureInfo.InvariantCulture)); } return nestedField; @@ -44,7 +45,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards if (!schema.FieldsById.TryGetValue(fieldId, out var field)) { - throw new DomainObjectNotFoundException(fieldId.ToString()); + throw new DomainObjectNotFoundException(fieldId.ToString(CultureInfo.InvariantCulture)); } if (!allowLocked) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.State.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.State.cs index e1c75bdb4..7281d6d6b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.State.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.State.cs @@ -28,6 +28,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject public long SchemaFieldsTotal { get; set; } + public bool IsDeleted { get; set; } + [IgnoreDataMember] public DomainId UniqueId { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/ISchemasHash.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/ISchemasHash.cs index 3ddeef9f1..485d18ca3 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/ISchemasHash.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/ISchemasHash.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using NodaTime; using Squidex.Domain.Apps.Entities.Apps; @@ -14,8 +15,10 @@ namespace Squidex.Domain.Apps.Entities.Schemas { public interface ISchemasHash { - Task<(Instant Create, string Hash)> GetCurrentHashAsync(IAppEntity app); + Task<(Instant Create, string Hash)> GetCurrentHashAsync(IAppEntity app, + CancellationToken ct = default); - ValueTask ComputeHashAsync(IAppEntity app, IEnumerable schemas); + ValueTask ComputeHashAsync(IAppEntity app, IEnumerable schemas, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasByAppIndexGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasCacheGrain.cs similarity index 60% rename from backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasByAppIndexGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasCacheGrain.cs index bc4ec46c8..8b7278c2a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasByAppIndexGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasCacheGrain.cs @@ -5,13 +5,21 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Orleans; +using System.Collections.Generic; +using System.Threading.Tasks; using Squidex.Infrastructure; using Squidex.Infrastructure.Orleans.Indexes; namespace Squidex.Domain.Apps.Entities.Schemas.Indexes { - public interface ISchemasByAppIndexGrain : IUniqueNameIndexGrain, IGrainWithStringKey + 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/ISchemasIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasIndex.cs index 88be8162b..a6fee1ba4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasIndex.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasIndex.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Squidex.Infrastructure; @@ -13,12 +14,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes { public interface ISchemasIndex { - Task GetSchemaAsync(DomainId appId, DomainId id, bool canCache); + Task GetSchemaAsync(DomainId appId, DomainId id, bool canCache, + CancellationToken ct = default); - Task GetSchemaByNameAsync(DomainId appId, string name, bool canCache); + Task GetSchemaAsync(DomainId appId, string name, bool canCache, + CancellationToken ct = default); - Task> GetSchemasAsync(DomainId appId); - - Task RebuildAsync(DomainId appId, Dictionary schemas); + Task> GetSchemasAsync(DomainId appId, + CancellationToken ct = default); } -} \ No newline at end of file +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexGrain.cs deleted file mode 100644 index d59cb07a3..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexGrain.cs +++ /dev/null @@ -1,27 +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; -using Squidex.Infrastructure.Orleans.Indexes; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.Schemas.Indexes -{ - public sealed class SchemasByAppIndexGrain : UniqueNameIndexGrain, ISchemasByAppIndexGrain - { - public SchemasByAppIndexGrain(IGrainState state) - : base(state) - { - } - } - - [CollectionName("Index_SchemasByApp")] - public sealed class SchemasByAppIndexGrainState : UniqueNameIndexState - { - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasCacheGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasCacheGrain.cs new file mode 100644 index 000000000..3052fd0e9 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasCacheGrain.cs @@ -0,0 +1,84 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Orleans.Concurrency; +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; + + private DomainId AppId => DomainId.Create(Key); + + public SchemasCacheGrain(ISchemaRepository schemaRepository) + { + this.schemaRepository = schemaRepository; + } + + 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(AppId); + + 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 efd855651..7bfd7aba6 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Orleans; using Squidex.Caching; @@ -33,12 +34,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes this.grainCache = grainCache; } - public Task RebuildAsync(DomainId appId, Dictionary schemas) - { - return Index(appId).RebuildAsync(schemas); - } - - public async Task> GetSchemasAsync(DomainId appId) + public async Task> GetSchemasAsync(DomainId appId, + CancellationToken ct = default) { using (Telemetry.Activities.StartActivity("SchemasIndex/GetSchemasAsync")) { @@ -46,13 +43,14 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes var schemas = await Task.WhenAll( - ids.Select(id => GetSchemaAsync(appId, id, false))); + ids.Select(id => GetSchemaAsync(appId, id, false, ct))); return schemas.NotNull().ToList(); } } - public async Task GetSchemaByNameAsync(DomainId appId, string name, bool canCache) + public async Task GetSchemaAsync(DomainId appId, string name, bool canCache, + CancellationToken ct = default) { using (Telemetry.Activities.StartActivity("SchemasIndex/GetSchemaByNameAsync")) { @@ -60,7 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes if (canCache) { - if (grainCache.TryGetValue(cacheKey, out var v) && v is ISchemaEntity cachedSchema) + if (grainCache.TryGetValue(cacheKey, out var value) && value is ISchemaEntity cachedSchema) { return cachedSchema; } @@ -73,11 +71,12 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes return null; } - return await GetSchemaAsync(appId, id, canCache); + return await GetSchemaAsync(appId, id, canCache, ct); } } - public async Task GetSchemaAsync(DomainId appId, DomainId id, bool canCache) + public async Task GetSchemaAsync(DomainId appId, DomainId id, bool canCache, + CancellationToken ct = default) { using (Telemetry.Activities.StartActivity("SchemasIndex/GetSchemaAsync")) { @@ -106,98 +105,108 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes { using (Telemetry.Activities.StartActivity("SchemasIndex/GetSchemaIdAsync")) { - return await Index(appId).GetIdAsync(name); + return await Cache(appId).GetSchemaIdAsync(name); } } - private async Task> GetSchemaIdsAsync(DomainId appId) + private async Task> GetSchemaIdsAsync(DomainId appId) { using (Telemetry.Activities.StartActivity("SchemasIndex/GetSchemaIdsAsync")) { - return await Index(appId).GetIdsAsync(); + return await Cache(appId).GetSchemaIdsAsync(); } } public async Task HandleAsync(CommandContext context, NextDelegate next) { - if (context.Command is CreateSchema createSchema) - { - var index = Index(createSchema.AppId.Id); + var command = context.Command; - var token = await CheckSchemaAsync(index, createSchema); + if (command is CreateSchema createSchema) + { + var cache = Cache(createSchema.AppId.Id); + var token = await CheckSchemaAsync(cache, createSchema); try { await next(context); } finally { - if (token != null) - { - if (context.IsCompleted) - { - await index.AddAsync(token); - } - else - { - await index.RemoveReservationAsync(token); - } - } + await cache.RemoveReservationAsync(token); } } else { await next(context); + } - if (context.IsCompleted && context.Command is SchemaCommand schemaCommand) + if (context.IsCompleted) + { + switch (command) { - var schema = context.PlainResult as ISchemaEntity; + case CreateSchema create: + await OnCreateAsync(create); + break; + case DeleteSchema delete: + await OnDeleteAsync(delete); + break; + case SchemaUpdateCommand update: + await OnUpdateAsync(update); + break; + } + } + } - if (schema == null) - { - schema = await GetSchemaCoreAsync(schemaCommand.AggregateId, true); - } + private async Task OnCreateAsync(CreateSchema create) + { + await InvalidateItAsync(create.AppId.Id, create.SchemaId, create.Name); - if (schema != null) - { - await InvalidateItAsync(schema); + await Cache(create.AppId.Id).AddAsync(create.SchemaId, create.Name); + } - if (context.Command is DeleteSchema) - { - await DeleteSchemaAsync(schema); - } - } - } - } + private async Task OnDeleteAsync(DeleteSchema delete) + { + await InvalidateItAsync(delete.AppId.Id, delete.SchemaId.Id, delete.SchemaId.Name); + + await Cache(delete.AppId.Id).RemoveAsync(delete.SchemaId.Id); + } + + private async Task OnUpdateAsync(SchemaUpdateCommand update) + { + await InvalidateItAsync(update.AppId.Id, update.SchemaId.Id, update.SchemaId.Name); } - private static async Task CheckSchemaAsync(ISchemasByAppIndexGrain index, CreateSchema command) + private async Task CheckSchemaAsync(ISchemasCacheGrain cache, CreateSchema command) { - var name = command.Name; + var token = await cache.ReserveAsync(command.SchemaId, command.Name); - if (name.IsSlug()) + if (token == null) { - var token = await index.ReserveAsync(command.SchemaId, name); + throw new ValidationException(T.Get("schemas.nameAlreadyExists")); + } - if (token == null) + try + { + var existingId = await GetSchemaIdAsync(command.AppId.Id, command.Name); + + if (existingId != default) { - throw new ValidationException(T.Get("schemas.nameAlreadyExists")); + throw new ValidationException(T.Get("apps.nameAlreadyExists")); } - - return token; + } + catch + { + // Catch our own exception, juist in case something went wrong before. + await cache.RemoveReservationAsync(token); + throw; } - return null; - } - - private Task DeleteSchemaAsync(ISchemaEntity schema) - { - return Index(schema.AppId.Id).RemoveAsync(schema.Id); + return token; } - private ISchemasByAppIndexGrain Index(DomainId appId) + private ISchemasCacheGrain Cache(DomainId appId) { - return grainFactory.GetGrain(appId.ToString()); + return grainFactory.GetGrain(appId.ToString()); } private async Task GetSchemaCoreAsync(DomainId id, bool allowDeleted = false) @@ -212,6 +221,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes return schema; } + private Task InvalidateItAsync(DomainId appId, DomainId id, string name) + { + return grainCache.RemoveAsync( + GetCacheKey(appId, id), + GetCacheKey(appId, name)); + } + private static string GetCacheKey(DomainId appId, string name) { return $"{typeof(SchemasIndex)}_Schemas_Name_{appId}_{name}"; @@ -222,13 +238,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes return $"{typeof(SchemasIndex)}_Schemas_Id_{appId}_{id}"; } - private Task InvalidateItAsync(ISchemaEntity schema) - { - return grainCache.RemoveAsync( - GetCacheKey(schema.AppId.Id, schema.Id), - GetCacheKey(schema.AppId.Id, schema.SchemaDef.Name)); - } - private Task CacheItAsync(ISchemaEntity schema) { return Task.WhenAll( diff --git a/backend/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexState.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Repositories/ISchemaRepository.cs similarity index 58% rename from backend/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexState.cs rename to backend/src/Squidex.Domain.Apps.Entities/Schemas/Repositories/ISchemaRepository.cs index 66ea6c939..92d0df477 100644 --- a/backend/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexState.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Repositories/ISchemaRepository.cs @@ -6,11 +6,15 @@ // ========================================================================== using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Squidex.Infrastructure; -namespace Squidex.Infrastructure.Orleans.Indexes +namespace Squidex.Domain.Apps.Entities.Schemas.Repositories { - public class UniqueNameIndexState + public interface ISchemaRepository { - public Dictionary Names { get; set; } = new Dictionary(); + Task> QueryIdsAsync(DomainId appId, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemasSearchSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemasSearchSource.cs index 855e4d8af..9c5f429be 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemasSearchSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemasSearchSource.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Threading; using System.Threading.Tasks; using Squidex.Domain.Apps.Core; @@ -34,7 +35,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas { var result = new SearchResults(); - var schemas = await appProvider.GetSchemasAsync(context.App.Id); + var schemas = await appProvider.GetSchemasAsync(context.App.Id, ct); if (schemas.Count > 0) { @@ -46,7 +47,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var name = schema.SchemaDef.DisplayNameUnchanged(); - if (name.Contains(query)) + if (name.Contains(query, StringComparison.OrdinalIgnoreCase)) { AddSchemaUrl(result, appId, schemaId, name); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Search/ISearchManager.cs b/backend/src/Squidex.Domain.Apps.Entities/Search/ISearchManager.cs index 7bb95ddd8..8a93d9bf4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Search/ISearchManager.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Search/ISearchManager.cs @@ -12,6 +12,7 @@ namespace Squidex.Domain.Apps.Entities.Search { public interface ISearchManager { - Task SearchAsync(string? query, Context context, CancellationToken ct = default); + Task SearchAsync(string? query, Context context, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Search/ISearchSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Search/ISearchSource.cs index 60c82eac7..38e52f0dd 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Search/ISearchSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Search/ISearchSource.cs @@ -12,6 +12,7 @@ namespace Squidex.Domain.Apps.Entities.Search { public interface ISearchSource { - Task SearchAsync(string query, Context context, CancellationToken ct); + Task SearchAsync(string query, Context context, + CancellationToken ct); } } 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 199b24c30..b49f957fc 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 @@ -26,6 +26,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + all diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppArchived.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppDeleted.cs similarity index 84% rename from backend/src/Squidex.Domain.Apps.Events/Apps/AppArchived.cs rename to backend/src/Squidex.Domain.Apps.Events/Apps/AppDeleted.cs index 02032eb9d..4963f8d89 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Apps/AppArchived.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppDeleted.cs @@ -9,8 +9,8 @@ using Squidex.Infrastructure.EventSourcing; namespace Squidex.Domain.Apps.Events.Apps { - [EventType(nameof(AppArchived))] - public sealed class AppArchived : AppEvent + [EventType(nameof(AppDeleted))] + public sealed class AppDeleted : AppEvent { } } diff --git a/backend/src/Squidex.Domain.Apps.Events/Rules/RuleUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Rules/RuleUpdated.cs index fa7f031e7..1f3c5d72c 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Rules/RuleUpdated.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Rules/RuleUpdated.cs @@ -26,7 +26,11 @@ namespace Squidex.Domain.Apps.Events.Rules { if (Trigger is IMigrated migrated) { - Trigger = migrated.Migrate(); + var clone = (RuleUpdated)MemberwiseClone(); + + clone.Trigger = migrated.Migrate(); + + return clone; } return this; diff --git a/backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj b/backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj index c1abda4a1..7da75c85f 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj +++ b/backend/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj @@ -13,6 +13,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/backend/src/Squidex.Domain.Users.MongoDb/MongoRoleStore.cs b/backend/src/Squidex.Domain.Users.MongoDb/MongoRoleStore.cs index 76cca2f06..c1d63fa5f 100644 --- a/backend/src/Squidex.Domain.Users.MongoDb/MongoRoleStore.cs +++ b/backend/src/Squidex.Domain.Users.MongoDb/MongoRoleStore.cs @@ -64,60 +64,70 @@ namespace Squidex.Domain.Users.MongoDb { } - public async Task FindByIdAsync(string roleId, CancellationToken cancellationToken) + public async Task FindByIdAsync(string roleId, + CancellationToken cancellationToken) { return await Collection.Find(x => x.Id == roleId).FirstOrDefaultAsync(cancellationToken); } - public async Task FindByNameAsync(string normalizedRoleName, CancellationToken cancellationToken) + public async Task FindByNameAsync(string normalizedRoleName, + CancellationToken cancellationToken) { return await Collection.Find(x => x.NormalizedName == normalizedRoleName).FirstOrDefaultAsync(cancellationToken); } - public async Task CreateAsync(IdentityRole role, CancellationToken cancellationToken) + public async Task CreateAsync(IdentityRole role, + CancellationToken cancellationToken) { await Collection.InsertOneAsync(role, null, cancellationToken); return IdentityResult.Success; } - public async Task UpdateAsync(IdentityRole role, CancellationToken cancellationToken) + public async Task UpdateAsync(IdentityRole role, + CancellationToken cancellationToken) { await Collection.ReplaceOneAsync(x => x.Id == role.Id, role, cancellationToken: cancellationToken); return IdentityResult.Success; } - public async Task DeleteAsync(IdentityRole role, CancellationToken cancellationToken) + public async Task DeleteAsync(IdentityRole role, + CancellationToken cancellationToken) { await Collection.DeleteOneAsync(x => x.Id == role.Id, null, cancellationToken); return IdentityResult.Success; } - public Task GetRoleIdAsync(IdentityRole role, CancellationToken cancellationToken) + public Task GetRoleIdAsync(IdentityRole role, + CancellationToken cancellationToken) { return Task.FromResult(role.Id); } - public Task GetRoleNameAsync(IdentityRole role, CancellationToken cancellationToken) + public Task GetRoleNameAsync(IdentityRole role, + CancellationToken cancellationToken) { return Task.FromResult(role.Name); } - public Task GetNormalizedRoleNameAsync(IdentityRole role, CancellationToken cancellationToken) + public Task GetNormalizedRoleNameAsync(IdentityRole role, + CancellationToken cancellationToken) { return Task.FromResult(role.NormalizedName); } - public Task SetRoleNameAsync(IdentityRole role, string roleName, CancellationToken cancellationToken) + public Task SetRoleNameAsync(IdentityRole role, string roleName, + CancellationToken cancellationToken) { role.Name = roleName; return Task.CompletedTask; } - public Task SetNormalizedRoleNameAsync(IdentityRole role, string normalizedName, CancellationToken cancellationToken) + public Task SetNormalizedRoleNameAsync(IdentityRole role, string normalizedName, + CancellationToken cancellationToken) { role.NormalizedName = normalizedName; diff --git a/backend/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs b/backend/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs index d348d3d25..55e4f4607 100644 --- a/backend/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs +++ b/backend/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs @@ -188,7 +188,8 @@ namespace Squidex.Domain.Users.MongoDb return new MongoUser { Email = email, UserName = email }; } - public async Task FindByIdAsync(string userId, CancellationToken cancellationToken) + public async Task FindByIdAsync(string userId, + CancellationToken cancellationToken) { if (!IsId(userId)) { @@ -198,42 +199,48 @@ namespace Squidex.Domain.Users.MongoDb return await Collection.Find(x => x.Id == userId).FirstOrDefaultAsync(cancellationToken); } - public async Task FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken) + public async Task FindByEmailAsync(string normalizedEmail, + CancellationToken cancellationToken) { var result = await Collection.Find(x => x.NormalizedEmail == normalizedEmail).FirstOrDefaultAsync(cancellationToken); return result; } - public async Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken) + public async Task FindByNameAsync(string normalizedUserName, + CancellationToken cancellationToken) { var result = await Collection.Find(x => x.NormalizedEmail == normalizedUserName).FirstOrDefaultAsync(cancellationToken); return result; } - public async Task FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken) + public async Task FindByLoginAsync(string loginProvider, string providerKey, + CancellationToken cancellationToken) { var result = await Collection.Find(x => x.Logins.Any(y => y.LoginProvider == loginProvider && y.ProviderKey == providerKey)).FirstOrDefaultAsync(cancellationToken); return result; } - public async Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken) + public async Task> GetUsersForClaimAsync(Claim claim, + CancellationToken cancellationToken) { var result = await Collection.Find(x => x.Claims.Any(y => y.Type == claim.Type && y.Value == claim.Value)).ToListAsync(cancellationToken); return result.OfType().ToList(); } - public async Task> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken) + public async Task> GetUsersInRoleAsync(string roleName, + CancellationToken cancellationToken) { var result = await Collection.Find(x => x.Roles.Contains(roleName)).ToListAsync(cancellationToken); return result.OfType().ToList(); } - public async Task CreateAsync(IdentityUser user, CancellationToken cancellationToken) + public async Task CreateAsync(IdentityUser user, + CancellationToken cancellationToken) { user.Id = ObjectId.GenerateNewId().ToString(); @@ -242,350 +249,400 @@ namespace Squidex.Domain.Users.MongoDb return IdentityResult.Success; } - public async Task UpdateAsync(IdentityUser user, CancellationToken cancellationToken) + public async Task UpdateAsync(IdentityUser user, + CancellationToken cancellationToken) { await Collection.ReplaceOneAsync(x => x.Id == user.Id, (MongoUser)user, cancellationToken: cancellationToken); return IdentityResult.Success; } - public async Task DeleteAsync(IdentityUser user, CancellationToken cancellationToken) + public async Task DeleteAsync(IdentityUser user, + CancellationToken cancellationToken) { await Collection.DeleteOneAsync(x => x.Id == user.Id, null, cancellationToken); return IdentityResult.Success; } - public Task GetUserIdAsync(IdentityUser user, CancellationToken cancellationToken) + public Task GetUserIdAsync(IdentityUser user, + CancellationToken cancellationToken) { var result = user.Id; return Task.FromResult(result); } - public Task GetUserNameAsync(IdentityUser user, CancellationToken cancellationToken) + public Task GetUserNameAsync(IdentityUser user, + CancellationToken cancellationToken) { var result = user.UserName; return Task.FromResult(result); } - public Task GetNormalizedUserNameAsync(IdentityUser user, CancellationToken cancellationToken) + public Task GetNormalizedUserNameAsync(IdentityUser user, + CancellationToken cancellationToken) { var result = user.NormalizedUserName; return Task.FromResult(result); } - public Task GetPasswordHashAsync(IdentityUser user, CancellationToken cancellationToken) + public Task GetPasswordHashAsync(IdentityUser user, + CancellationToken cancellationToken) { var result = user.PasswordHash; return Task.FromResult(result); } - public Task> GetRolesAsync(IdentityUser user, CancellationToken cancellationToken) + public Task> GetRolesAsync(IdentityUser user, + CancellationToken cancellationToken) { var result = ((MongoUser)user).Roles.ToList(); return Task.FromResult>(result); } - public Task IsInRoleAsync(IdentityUser user, string roleName, CancellationToken cancellationToken) + public Task IsInRoleAsync(IdentityUser user, string roleName, + CancellationToken cancellationToken) { var result = ((MongoUser)user).Roles.Contains(roleName); return Task.FromResult(result); } - public Task> GetLoginsAsync(IdentityUser user, CancellationToken cancellationToken) + public Task> GetLoginsAsync(IdentityUser user, + CancellationToken cancellationToken) { var result = ((MongoUser)user).Logins.Select(x => new UserLoginInfo(x.LoginProvider, x.ProviderKey, x.ProviderDisplayName)).ToList(); return Task.FromResult>(result); } - public Task GetSecurityStampAsync(IdentityUser user, CancellationToken cancellationToken) + public Task GetSecurityStampAsync(IdentityUser user, + CancellationToken cancellationToken) { var result = user.SecurityStamp; return Task.FromResult(result); } - public Task GetEmailAsync(IdentityUser user, CancellationToken cancellationToken) + public Task GetEmailAsync(IdentityUser user, + CancellationToken cancellationToken) { var result = user.Email; return Task.FromResult(result); } - public Task GetEmailConfirmedAsync(IdentityUser user, CancellationToken cancellationToken) + public Task GetEmailConfirmedAsync(IdentityUser user, + CancellationToken cancellationToken) { var result = user.EmailConfirmed; return Task.FromResult(result); } - public Task GetNormalizedEmailAsync(IdentityUser user, CancellationToken cancellationToken) + public Task GetNormalizedEmailAsync(IdentityUser user, + CancellationToken cancellationToken) { var result = user.NormalizedEmail; return Task.FromResult(result); } - public Task> GetClaimsAsync(IdentityUser user, CancellationToken cancellationToken) + public Task> GetClaimsAsync(IdentityUser user, + CancellationToken cancellationToken) { var result = ((MongoUser)user).Claims; return Task.FromResult>(result); } - public Task GetPhoneNumberAsync(IdentityUser user, CancellationToken cancellationToken) + public Task GetPhoneNumberAsync(IdentityUser user, + CancellationToken cancellationToken) { var result = user.PhoneNumber; return Task.FromResult(result); } - public Task GetPhoneNumberConfirmedAsync(IdentityUser user, CancellationToken cancellationToken) + public Task GetPhoneNumberConfirmedAsync(IdentityUser user, + CancellationToken cancellationToken) { var result = user.PhoneNumberConfirmed; return Task.FromResult(result); } - public Task GetTwoFactorEnabledAsync(IdentityUser user, CancellationToken cancellationToken) + public Task GetTwoFactorEnabledAsync(IdentityUser user, + CancellationToken cancellationToken) { var result = user.TwoFactorEnabled; return Task.FromResult(result); } - public Task GetLockoutEndDateAsync(IdentityUser user, CancellationToken cancellationToken) + public Task GetLockoutEndDateAsync(IdentityUser user, + CancellationToken cancellationToken) { var result = user.LockoutEnd; return Task.FromResult(result); } - public Task GetAccessFailedCountAsync(IdentityUser user, CancellationToken cancellationToken) + public Task GetAccessFailedCountAsync(IdentityUser user, + CancellationToken cancellationToken) { var result = user.AccessFailedCount; return Task.FromResult(result); } - public Task GetLockoutEnabledAsync(IdentityUser user, CancellationToken cancellationToken) + public Task GetLockoutEnabledAsync(IdentityUser user, + CancellationToken cancellationToken) { var result = user.LockoutEnabled; return Task.FromResult(result); } - public Task GetTokenAsync(IdentityUser user, string loginProvider, string name, CancellationToken cancellationToken) + public Task GetTokenAsync(IdentityUser user, string loginProvider, string name, + CancellationToken cancellationToken) { var result = ((MongoUser)user).GetToken(loginProvider, name)!; return Task.FromResult(result); } - public Task GetAuthenticatorKeyAsync(IdentityUser user, CancellationToken cancellationToken) + public Task GetAuthenticatorKeyAsync(IdentityUser user, + CancellationToken cancellationToken) { var result = ((MongoUser)user).GetToken(InternalLoginProvider, AuthenticatorKeyTokenName)!; return Task.FromResult(result); } - public Task HasPasswordAsync(IdentityUser user, CancellationToken cancellationToken) + public Task HasPasswordAsync(IdentityUser user, + CancellationToken cancellationToken) { var result = !string.IsNullOrWhiteSpace(user.PasswordHash); return Task.FromResult(result); } - public Task CountCodesAsync(IdentityUser user, CancellationToken cancellationToken) + public Task CountCodesAsync(IdentityUser user, + CancellationToken cancellationToken) { var result = ((MongoUser)user).GetToken(InternalLoginProvider, RecoveryCodeTokenName)?.Split(';').Length ?? 0; return Task.FromResult(result); } - public Task SetUserNameAsync(IdentityUser user, string userName, CancellationToken cancellationToken) + public Task SetUserNameAsync(IdentityUser user, string userName, + CancellationToken cancellationToken) { ((MongoUser)user).UserName = userName; return Task.CompletedTask; } - public Task SetNormalizedUserNameAsync(IdentityUser user, string normalizedName, CancellationToken cancellationToken) + public Task SetNormalizedUserNameAsync(IdentityUser user, string normalizedName, + CancellationToken cancellationToken) { ((MongoUser)user).NormalizedUserName = normalizedName; return Task.CompletedTask; } - public Task SetPasswordHashAsync(IdentityUser user, string passwordHash, CancellationToken cancellationToken) + public Task SetPasswordHashAsync(IdentityUser user, string passwordHash, + CancellationToken cancellationToken) { ((MongoUser)user).PasswordHash = passwordHash; return Task.CompletedTask; } - public Task AddToRoleAsync(IdentityUser user, string roleName, CancellationToken cancellationToken) + public Task AddToRoleAsync(IdentityUser user, string roleName, + CancellationToken cancellationToken) { ((MongoUser)user).AddRole(roleName); return Task.CompletedTask; } - public Task RemoveFromRoleAsync(IdentityUser user, string roleName, CancellationToken cancellationToken) + public Task RemoveFromRoleAsync(IdentityUser user, string roleName, + CancellationToken cancellationToken) { ((MongoUser)user).RemoveRole(roleName); return Task.CompletedTask; } - public Task AddLoginAsync(IdentityUser user, UserLoginInfo login, CancellationToken cancellationToken) + public Task AddLoginAsync(IdentityUser user, UserLoginInfo login, + CancellationToken cancellationToken) { ((MongoUser)user).AddLogin(login); return Task.CompletedTask; } - public Task RemoveLoginAsync(IdentityUser user, string loginProvider, string providerKey, CancellationToken cancellationToken) + public Task RemoveLoginAsync(IdentityUser user, string loginProvider, string providerKey, + CancellationToken cancellationToken) { ((MongoUser)user).RemoveLogin(loginProvider, providerKey); return Task.CompletedTask; } - public Task SetSecurityStampAsync(IdentityUser user, string stamp, CancellationToken cancellationToken) + public Task SetSecurityStampAsync(IdentityUser user, string stamp, + CancellationToken cancellationToken) { ((MongoUser)user).SecurityStamp = stamp; return Task.CompletedTask; } - public Task SetEmailAsync(IdentityUser user, string email, CancellationToken cancellationToken) + public Task SetEmailAsync(IdentityUser user, string email, + CancellationToken cancellationToken) { ((MongoUser)user).Email = email; return Task.CompletedTask; } - public Task SetEmailConfirmedAsync(IdentityUser user, bool confirmed, CancellationToken cancellationToken) + public Task SetEmailConfirmedAsync(IdentityUser user, bool confirmed, + CancellationToken cancellationToken) { ((MongoUser)user).EmailConfirmed = confirmed; return Task.CompletedTask; } - public Task SetNormalizedEmailAsync(IdentityUser user, string normalizedEmail, CancellationToken cancellationToken) + public Task SetNormalizedEmailAsync(IdentityUser user, string normalizedEmail, + CancellationToken cancellationToken) { ((MongoUser)user).NormalizedEmail = normalizedEmail; return Task.CompletedTask; } - public Task AddClaimsAsync(IdentityUser user, IEnumerable claims, CancellationToken cancellationToken) + public Task AddClaimsAsync(IdentityUser user, IEnumerable claims, + CancellationToken cancellationToken) { ((MongoUser)user).AddClaims(claims); return Task.CompletedTask; } - public Task ReplaceClaimAsync(IdentityUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken) + public Task ReplaceClaimAsync(IdentityUser user, Claim claim, Claim newClaim, + CancellationToken cancellationToken) { ((MongoUser)user).ReplaceClaim(claim, newClaim); return Task.CompletedTask; } - public Task RemoveClaimsAsync(IdentityUser user, IEnumerable claims, CancellationToken cancellationToken) + public Task RemoveClaimsAsync(IdentityUser user, IEnumerable claims, + CancellationToken cancellationToken) { ((MongoUser)user).RemoveClaims(claims); return Task.CompletedTask; } - public Task SetPhoneNumberAsync(IdentityUser user, string phoneNumber, CancellationToken cancellationToken) + public Task SetPhoneNumberAsync(IdentityUser user, string phoneNumber, + CancellationToken cancellationToken) { ((MongoUser)user).PhoneNumber = phoneNumber; return Task.CompletedTask; } - public Task SetPhoneNumberConfirmedAsync(IdentityUser user, bool confirmed, CancellationToken cancellationToken) + public Task SetPhoneNumberConfirmedAsync(IdentityUser user, bool confirmed, + CancellationToken cancellationToken) { ((MongoUser)user).PhoneNumberConfirmed = confirmed; return Task.CompletedTask; } - public Task SetTwoFactorEnabledAsync(IdentityUser user, bool enabled, CancellationToken cancellationToken) + public Task SetTwoFactorEnabledAsync(IdentityUser user, bool enabled, + CancellationToken cancellationToken) { ((MongoUser)user).TwoFactorEnabled = enabled; return Task.CompletedTask; } - public Task SetLockoutEndDateAsync(IdentityUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken) + public Task SetLockoutEndDateAsync(IdentityUser user, DateTimeOffset? lockoutEnd, + CancellationToken cancellationToken) { ((MongoUser)user).LockoutEnd = lockoutEnd?.UtcDateTime; return Task.CompletedTask; } - public Task IncrementAccessFailedCountAsync(IdentityUser user, CancellationToken cancellationToken) + public Task IncrementAccessFailedCountAsync(IdentityUser user, + CancellationToken cancellationToken) { ((MongoUser)user).AccessFailedCount++; return Task.FromResult(((MongoUser)user).AccessFailedCount); } - public Task ResetAccessFailedCountAsync(IdentityUser user, CancellationToken cancellationToken) + public Task ResetAccessFailedCountAsync(IdentityUser user, + CancellationToken cancellationToken) { ((MongoUser)user).AccessFailedCount = 0; return Task.CompletedTask; } - public Task SetLockoutEnabledAsync(IdentityUser user, bool enabled, CancellationToken cancellationToken) + public Task SetLockoutEnabledAsync(IdentityUser user, bool enabled, + CancellationToken cancellationToken) { ((MongoUser)user).LockoutEnabled = enabled; return Task.CompletedTask; } - public Task SetTokenAsync(IdentityUser user, string loginProvider, string name, string value, CancellationToken cancellationToken) + public Task SetTokenAsync(IdentityUser user, string loginProvider, string name, string value, + CancellationToken cancellationToken) { ((MongoUser)user).ReplaceToken(loginProvider, name, value); return Task.CompletedTask; } - public Task RemoveTokenAsync(IdentityUser user, string loginProvider, string name, CancellationToken cancellationToken) + public Task RemoveTokenAsync(IdentityUser user, string loginProvider, string name, + CancellationToken cancellationToken) { ((MongoUser)user).RemoveToken(loginProvider, name); return Task.CompletedTask; } - public Task SetAuthenticatorKeyAsync(IdentityUser user, string key, CancellationToken cancellationToken) + public Task SetAuthenticatorKeyAsync(IdentityUser user, string key, + CancellationToken cancellationToken) { ((MongoUser)user).ReplaceToken(InternalLoginProvider, AuthenticatorKeyTokenName, key); return Task.CompletedTask; } - public Task ReplaceCodesAsync(IdentityUser user, IEnumerable recoveryCodes, CancellationToken cancellationToken) + public Task ReplaceCodesAsync(IdentityUser user, IEnumerable recoveryCodes, + CancellationToken cancellationToken) { ((MongoUser)user).ReplaceToken(InternalLoginProvider, RecoveryCodeTokenName, string.Join(";", recoveryCodes)); return Task.CompletedTask; } - public Task RedeemCodeAsync(IdentityUser user, string code, CancellationToken cancellationToken) + public Task RedeemCodeAsync(IdentityUser user, string code, + CancellationToken cancellationToken) { var mergedCodes = ((MongoUser)user).GetToken(InternalLoginProvider, RecoveryCodeTokenName) ?? string.Empty; diff --git a/backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj b/backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj index ecf15442e..002e3b63c 100644 --- a/backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj +++ b/backend/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj @@ -18,6 +18,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/backend/src/Squidex.Domain.Users/DefaultKeyStore.cs b/backend/src/Squidex.Domain.Users/DefaultKeyStore.cs index eb4164005..1cee64f26 100644 --- a/backend/src/Squidex.Domain.Users/DefaultKeyStore.cs +++ b/backend/src/Squidex.Domain.Users/DefaultKeyStore.cs @@ -65,7 +65,7 @@ namespace Squidex.Domain.Users if (securityKey.Rsa != null) { - var parameters = securityKey.Rsa.ExportParameters(includePrivateParameters: true); + var parameters = securityKey.Rsa.ExportParameters(true); state.Parameters = parameters; } diff --git a/backend/src/Squidex.Domain.Users/DefaultUserPictureStore.cs b/backend/src/Squidex.Domain.Users/DefaultUserPictureStore.cs index 51d7648ea..9b388387b 100644 --- a/backend/src/Squidex.Domain.Users/DefaultUserPictureStore.cs +++ b/backend/src/Squidex.Domain.Users/DefaultUserPictureStore.cs @@ -21,14 +21,16 @@ namespace Squidex.Domain.Users this.assetStore = assetStore; } - public Task UploadAsync(string userId, Stream stream, CancellationToken ct = default) + public Task UploadAsync(string userId, Stream stream, + CancellationToken ct = default) { var fileName = GetFileName(userId); return assetStore.UploadAsync(fileName, stream, true, ct); } - public Task DownloadAsync(string userId, Stream stream, CancellationToken ct = default) + public Task DownloadAsync(string userId, Stream stream, + CancellationToken ct = default) { var fileName = GetFileName(userId); diff --git a/backend/src/Squidex.Domain.Users/DefaultUserResolver.cs b/backend/src/Squidex.Domain.Users/DefaultUserResolver.cs index 2b571ddcb..ded9310f9 100644 --- a/backend/src/Squidex.Domain.Users/DefaultUserResolver.cs +++ b/backend/src/Squidex.Domain.Users/DefaultUserResolver.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Squidex.Infrastructure; @@ -27,7 +28,8 @@ namespace Squidex.Domain.Users this.serviceProvider = serviceProvider; } - public async Task<(IUser? User, bool Created)> CreateUserIfNotExistsAsync(string email, bool invited) + public async Task<(IUser? User, bool Created)> CreateUserIfNotExistsAsync(string email, bool invited = false, + CancellationToken ct = default) { Guard.NotNullOrEmpty(email, nameof(email)); @@ -40,7 +42,7 @@ namespace Squidex.Domain.Users var user = await userService.CreateAsync(email, new UserValues { Invited = invited - }); + }, ct: ct); return (user, true); } @@ -48,13 +50,14 @@ namespace Squidex.Domain.Users { } - var found = await FindByIdOrEmailAsync(email); + var found = await FindByIdOrEmailAsync(email, ct); return (found, false); } } - public async Task SetClaimAsync(string id, string type, string value, bool silent) + public async Task SetClaimAsync(string id, string type, string value, bool silent = false, + CancellationToken ct = default) { Guard.NotNullOrEmpty(id, nameof(id)); Guard.NotNullOrEmpty(type, nameof(type)); @@ -72,11 +75,12 @@ namespace Squidex.Domain.Users } }; - await userService.UpdateAsync(id, values, silent); + await userService.UpdateAsync(id, values, silent, ct); } } - public async Task FindByIdAsync(string id) + public async Task FindByIdAsync(string id, + CancellationToken ct = default) { Guard.NotNullOrEmpty(id, nameof(id)); @@ -84,11 +88,12 @@ namespace Squidex.Domain.Users { var userService = scope.ServiceProvider.GetRequiredService(); - return await userService.FindByIdAsync(id); + return await userService.FindByIdAsync(id, ct); } } - public async Task FindByIdOrEmailAsync(string idOrEmail) + public async Task FindByIdOrEmailAsync(string idOrEmail, + CancellationToken ct = default) { Guard.NotNullOrEmpty(idOrEmail, nameof(idOrEmail)); @@ -96,30 +101,32 @@ namespace Squidex.Domain.Users { var userService = scope.ServiceProvider.GetRequiredService(); - if (idOrEmail.Contains("@")) + if (idOrEmail.Contains("@", StringComparison.Ordinal)) { - return await userService.FindByEmailAsync(idOrEmail); + return await userService.FindByEmailAsync(idOrEmail, ct); } else { - return await userService.FindByIdAsync(idOrEmail); + return await userService.FindByIdAsync(idOrEmail, ct); } } } - public async Task> QueryAllAsync() + public async Task> QueryAllAsync( + CancellationToken ct = default) { using (var scope = serviceProvider.CreateScope()) { var userService = scope.ServiceProvider.GetRequiredService(); - var result = await userService.QueryAsync(take: int.MaxValue); + var result = await userService.QueryAsync(take: int.MaxValue, ct: ct); return result.ToList(); } } - public async Task> QueryByEmailAsync(string email) + public async Task> QueryByEmailAsync(string email, + CancellationToken ct = default) { Guard.NotNullOrEmpty(email, nameof(email)); @@ -127,13 +134,14 @@ namespace Squidex.Domain.Users { var userService = scope.ServiceProvider.GetRequiredService(); - var result = await userService.QueryAsync(email); + var result = await userService.QueryAsync(email, ct: ct); return result.ToList(); } } - public async Task> QueryManyAsync(string[] ids) + public async Task> QueryManyAsync(string[] ids, + CancellationToken ct = default) { Guard.NotNull(ids, nameof(ids)); @@ -141,7 +149,7 @@ namespace Squidex.Domain.Users { var userService = scope.ServiceProvider.GetRequiredService(); - var result = await userService.QueryAsync(ids); + var result = await userService.QueryAsync(ids, ct); return result.OfType().ToDictionary(x => x.Id); } diff --git a/backend/src/Squidex.Domain.Users/DefaultUserService.cs b/backend/src/Squidex.Domain.Users/DefaultUserService.cs index 6b0e23422..d3f2fb26f 100644 --- a/backend/src/Squidex.Domain.Users/DefaultUserService.cs +++ b/backend/src/Squidex.Domain.Users/DefaultUserService.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Squidex.Infrastructure; @@ -37,21 +38,24 @@ namespace Squidex.Domain.Users this.log = log; } - public async Task IsEmptyAsync() + public async Task IsEmptyAsync( + CancellationToken ct = default) { - var result = await QueryAsync(null, 1, 0); + var result = await QueryAsync(null, 1, 0, ct); return result.Total == 0; } - public string GetUserId(ClaimsPrincipal user) + public string GetUserId(ClaimsPrincipal user, + CancellationToken ct = default) { Guard.NotNull(user, nameof(user)); return userManager.GetUserId(user); } - public async Task> QueryAsync(IEnumerable ids) + public async Task> QueryAsync(IEnumerable ids, + CancellationToken ct = default) { Guard.NotNull(ids, nameof(ids)); @@ -69,7 +73,8 @@ namespace Squidex.Domain.Users return ResultList.Create(users.Count, resolved); } - public async Task> QueryAsync(string? query, int take, int skip) + public async Task> QueryAsync(string? query = null, int take = 10, int skip = 0, + CancellationToken ct = default) { Guard.GreaterThan(take, 0, nameof(take)); Guard.GreaterEquals(skip, 0, nameof(skip)); @@ -96,21 +101,24 @@ namespace Squidex.Domain.Users return ResultList.Create(userTotal, resolved); } - public Task> GetLoginsAsync(IUser user) + public Task> GetLoginsAsync(IUser user, + CancellationToken ct = default) { Guard.NotNull(user, nameof(user)); return userManager.GetLoginsAsync((IdentityUser)user.Identity); } - public Task HasPasswordAsync(IUser user) + public Task HasPasswordAsync(IUser user, + CancellationToken ct = default) { Guard.NotNull(user, nameof(user)); return userManager.HasPasswordAsync((IdentityUser)user.Identity); } - public async Task FindByLoginAsync(string provider, string key) + public async Task FindByLoginAsync(string provider, string key, + CancellationToken ct = default) { Guard.NotNullOrEmpty(provider, nameof(provider)); @@ -119,7 +127,8 @@ namespace Squidex.Domain.Users return await ResolveOptionalAsync(user); } - public async Task FindByEmailAsync(string email) + public async Task FindByEmailAsync(string email, + CancellationToken ct = default) { Guard.NotNullOrEmpty(email, nameof(email)); @@ -128,7 +137,8 @@ namespace Squidex.Domain.Users return await ResolveOptionalAsync(user); } - public async Task GetAsync(ClaimsPrincipal principal) + public async Task GetAsync(ClaimsPrincipal principal, + CancellationToken ct = default) { Guard.NotNull(principal, nameof(principal)); @@ -137,7 +147,8 @@ namespace Squidex.Domain.Users return await ResolveOptionalAsync(user); } - public async Task FindByIdAsync(string id) + public async Task FindByIdAsync(string id, + CancellationToken ct = default) { if (!userFactory.IsId(id)) { @@ -149,7 +160,8 @@ namespace Squidex.Domain.Users return await ResolveOptionalAsync(user); } - public async Task CreateAsync(string email, UserValues? values = null, bool lockAutomatically = false) + public async Task CreateAsync(string email, UserValues? values = null, bool lockAutomatically = false, + CancellationToken ct = default) { Guard.NotNullOrEmpty(email, nameof(email)); @@ -226,7 +238,8 @@ namespace Squidex.Domain.Users return resolved; } - public Task SetPasswordAsync(string id, string password, string? oldPassword) + public Task SetPasswordAsync(string id, string password, string? oldPassword = null, + CancellationToken ct = default) { Guard.NotNullOrEmpty(id, nameof(id)); @@ -243,7 +256,8 @@ namespace Squidex.Domain.Users }); } - public async Task UpdateAsync(string id, UserValues values, bool silent = false) + public async Task UpdateAsync(string id, UserValues values, bool silent = false, + CancellationToken ct = default) { Guard.NotNullOrEmpty(id, nameof(id)); Guard.NotNull(values, nameof(values)); @@ -291,35 +305,40 @@ namespace Squidex.Domain.Users return resolved; } - public Task LockAsync(string id) + public Task LockAsync(string id, + CancellationToken ct = default) { Guard.NotNullOrEmpty(id, nameof(id)); return ForUserAsync(id, user => userManager.SetLockoutEndDateAsync(user, LockoutDate()).Throw(log)); } - public Task UnlockAsync(string id) + public Task UnlockAsync(string id, + CancellationToken ct = default) { Guard.NotNullOrEmpty(id, nameof(id)); return ForUserAsync(id, user => userManager.SetLockoutEndDateAsync(user, null).Throw(log)); } - public Task AddLoginAsync(string id, ExternalLoginInfo externalLogin) + public Task AddLoginAsync(string id, ExternalLoginInfo externalLogin, + CancellationToken ct = default) { Guard.NotNullOrEmpty(id, nameof(id)); return ForUserAsync(id, user => userManager.AddLoginAsync(user, externalLogin).Throw(log)); } - public Task RemoveLoginAsync(string id, string loginProvider, string providerKey) + public Task RemoveLoginAsync(string id, string loginProvider, string providerKey, + CancellationToken ct = default) { Guard.NotNullOrEmpty(id, nameof(id)); return ForUserAsync(id, user => userManager.RemoveLoginAsync(user, loginProvider, providerKey).Throw(log)); } - public async Task DeleteAsync(string id) + public async Task DeleteAsync(string id, + CancellationToken ct = default) { Guard.NotNullOrEmpty(id, nameof(id)); diff --git a/backend/src/Squidex.Domain.Users/DefaultXmlRepository.cs b/backend/src/Squidex.Domain.Users/DefaultXmlRepository.cs index cdca50443..7a9c7fbba 100644 --- a/backend/src/Squidex.Domain.Users/DefaultXmlRepository.cs +++ b/backend/src/Squidex.Domain.Users/DefaultXmlRepository.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using System.Xml.Linq; using Microsoft.AspNetCore.DataProtection.Repositories; @@ -45,16 +46,7 @@ namespace Squidex.Domain.Users public IReadOnlyCollection GetAllElements() { - var result = new List(); - - store.ReadAllAsync((state, _) => - { - result.Add(state.ToXml()); - - return Task.CompletedTask; - }).Wait(); - - return result; + return GetAllElementsAsync().Result; } public void StoreElement(XElement element, string friendlyName) @@ -63,5 +55,10 @@ namespace Squidex.Domain.Users store.WriteAsync(DomainId.Create(friendlyName), state, EtagVersion.Any, 0); } + + private async Task> GetAllElementsAsync() + { + return await store.ReadAllAsync().Select(x => x.State.ToXml()).ToListAsync(); + } } } diff --git a/backend/src/Squidex.Domain.Users/IUserPictureStore.cs b/backend/src/Squidex.Domain.Users/IUserPictureStore.cs index d6bd06fad..8f16f8d1b 100644 --- a/backend/src/Squidex.Domain.Users/IUserPictureStore.cs +++ b/backend/src/Squidex.Domain.Users/IUserPictureStore.cs @@ -13,8 +13,10 @@ namespace Squidex.Domain.Users { public interface IUserPictureStore { - Task UploadAsync(string userId, Stream stream, CancellationToken ct = default); + Task UploadAsync(string userId, Stream stream, + CancellationToken ct = default); - Task DownloadAsync(string userId, Stream stream, CancellationToken ct = default); + Task DownloadAsync(string userId, Stream stream, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Domain.Users/IUserService.cs b/backend/src/Squidex.Domain.Users/IUserService.cs index 9d98742f2..df6966e34 100644 --- a/backend/src/Squidex.Domain.Users/IUserService.cs +++ b/backend/src/Squidex.Domain.Users/IUserService.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Security.Claims; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Squidex.Infrastructure; @@ -16,40 +17,58 @@ namespace Squidex.Domain.Users { public interface IUserService { - Task> QueryAsync(IEnumerable ids); + Task> QueryAsync(IEnumerable ids, + CancellationToken ct = default); - Task> QueryAsync(string? query = null, int take = 10, int skip = 0); + Task> QueryAsync(string? query = null, int take = 10, int skip = 0, + CancellationToken ct = default); - string GetUserId(ClaimsPrincipal user); + string GetUserId(ClaimsPrincipal user, + CancellationToken ct = default); - Task> GetLoginsAsync(IUser user); + Task> GetLoginsAsync(IUser user, + CancellationToken ct = default); - Task HasPasswordAsync(IUser user); + Task HasPasswordAsync(IUser user, + CancellationToken ct = default); - Task IsEmptyAsync(); + Task IsEmptyAsync( + CancellationToken ct = default); - Task CreateAsync(string email, UserValues? values = null, bool lockAutomatically = false); + Task CreateAsync(string email, UserValues? values = null, bool lockAutomatically = false, + CancellationToken ct = default); - Task GetAsync(ClaimsPrincipal principal); + Task GetAsync(ClaimsPrincipal principal, + CancellationToken ct = default); - Task FindByEmailAsync(string email); + Task FindByEmailAsync(string email, + CancellationToken ct = default); - Task FindByIdAsync(string id); + Task FindByIdAsync(string id, + CancellationToken ct = default); - Task FindByLoginAsync(string provider, string key); + Task FindByLoginAsync(string provider, string key, + CancellationToken ct = default); - Task SetPasswordAsync(string id, string password, string? oldPassword = null); + Task SetPasswordAsync(string id, string password, string? oldPassword = null, + CancellationToken ct = default); - Task AddLoginAsync(string id, ExternalLoginInfo externalLogin); + Task AddLoginAsync(string id, ExternalLoginInfo externalLogin, + CancellationToken ct = default); - Task RemoveLoginAsync(string id, string loginProvider, string providerKey); + Task RemoveLoginAsync(string id, string loginProvider, string providerKey, + CancellationToken ct = default); - Task LockAsync(string id); + Task LockAsync(string id, + CancellationToken ct = default); - Task UnlockAsync(string id); + Task UnlockAsync(string id, + CancellationToken ct = default); - Task UpdateAsync(string id, UserValues values, bool silent = false); + Task UpdateAsync(string id, UserValues values, bool silent = false, + CancellationToken ct = default); - Task DeleteAsync(string id); + Task DeleteAsync(string id, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Domain.Users/InMemory/InMemoryApplicationStore.cs b/backend/src/Squidex.Domain.Users/InMemory/InMemoryApplicationStore.cs index f8e1ac2c9..42106cb19 100644 --- a/backend/src/Squidex.Domain.Users/InMemory/InMemoryApplicationStore.cs +++ b/backend/src/Squidex.Domain.Users/InMemory/InMemoryApplicationStore.cs @@ -32,38 +32,44 @@ namespace Squidex.Domain.Users.InMemory this.applications = applications.Select(x => new ImmutableApplication(x.Id, x.Descriptor)).ToList(); } - public virtual ValueTask CountAsync(CancellationToken cancellationToken) + public virtual ValueTask CountAsync( + CancellationToken cancellationToken) { return new ValueTask(applications.Count); } - public virtual ValueTask CountAsync(Func, IQueryable> query, CancellationToken cancellationToken) + public virtual ValueTask CountAsync(Func, IQueryable> query, + CancellationToken cancellationToken) { return query(applications.AsQueryable()).LongCount().AsValueTask(); } - public virtual ValueTask GetAsync(Func, TState, IQueryable> query, TState state, CancellationToken cancellationToken) + public virtual ValueTask GetAsync(Func, TState, IQueryable> query, TState state, + CancellationToken cancellationToken) { var result = query(applications.AsQueryable(), state).First(); return result.AsValueTask(); } - public virtual ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken) + public virtual ValueTask FindByIdAsync(string identifier, + CancellationToken cancellationToken) { var result = applications.Find(x => x.Id == identifier); return result.AsValueTask(); } - public virtual ValueTask FindByClientIdAsync(string identifier, CancellationToken cancellationToken) + public virtual ValueTask FindByClientIdAsync(string identifier, + CancellationToken cancellationToken) { var result = applications.Find(x => x.ClientId == identifier); return result.AsValueTask(); } - public virtual async IAsyncEnumerable FindByPostLogoutRedirectUriAsync(string address, [EnumeratorCancellation] CancellationToken cancellationToken) + public virtual async IAsyncEnumerable FindByPostLogoutRedirectUriAsync(string address, + [EnumeratorCancellation] CancellationToken cancellationToken) { var result = applications.Where(x => x.PostLogoutRedirectUris.Contains(address)); @@ -73,7 +79,8 @@ namespace Squidex.Domain.Users.InMemory } } - public virtual async IAsyncEnumerable FindByRedirectUriAsync(string address, [EnumeratorCancellation] CancellationToken cancellationToken) + public virtual async IAsyncEnumerable FindByRedirectUriAsync(string address, + [EnumeratorCancellation] CancellationToken cancellationToken) { var result = applications.Where(x => x.RedirectUris.Contains(address)); @@ -83,7 +90,8 @@ namespace Squidex.Domain.Users.InMemory } } - public virtual async IAsyncEnumerable ListAsync(int? count, int? offset, [EnumeratorCancellation] CancellationToken cancellationToken) + public virtual async IAsyncEnumerable ListAsync(int? count, int? offset, + [EnumeratorCancellation] CancellationToken cancellationToken) { var result = applications; @@ -93,7 +101,8 @@ namespace Squidex.Domain.Users.InMemory } } - public virtual async IAsyncEnumerable ListAsync(Func, TState, IQueryable> query, TState state, [EnumeratorCancellation] CancellationToken cancellationToken) + public virtual async IAsyncEnumerable ListAsync(Func, TState, IQueryable> query, TState state, + [EnumeratorCancellation] CancellationToken cancellationToken) { var result = query(applications.AsQueryable(), state); @@ -103,137 +112,164 @@ namespace Squidex.Domain.Users.InMemory } } - public virtual ValueTask GetIdAsync(ImmutableApplication application, CancellationToken cancellationToken) + public virtual ValueTask GetIdAsync(ImmutableApplication application, + CancellationToken cancellationToken) { return new ValueTask(application.Id); } - public virtual ValueTask GetClientIdAsync(ImmutableApplication application, CancellationToken cancellationToken) + public virtual ValueTask GetClientIdAsync(ImmutableApplication application, + CancellationToken cancellationToken) { return application.ClientId.AsValueTask(); } - public virtual ValueTask GetClientSecretAsync(ImmutableApplication application, CancellationToken cancellationToken) + public virtual ValueTask GetClientSecretAsync(ImmutableApplication application, + CancellationToken cancellationToken) { return application.ClientSecret.AsValueTask(); } - public virtual ValueTask GetClientTypeAsync(ImmutableApplication application, CancellationToken cancellationToken) + public virtual ValueTask GetClientTypeAsync(ImmutableApplication application, + CancellationToken cancellationToken) { return application.Type.AsValueTask(); } - public virtual ValueTask GetConsentTypeAsync(ImmutableApplication application, CancellationToken cancellationToken) + public virtual ValueTask GetConsentTypeAsync(ImmutableApplication application, + CancellationToken cancellationToken) { return application.ConsentType.AsValueTask(); } - public virtual ValueTask GetDisplayNameAsync(ImmutableApplication application, CancellationToken cancellationToken) + public virtual ValueTask GetDisplayNameAsync(ImmutableApplication application, + CancellationToken cancellationToken) { return application.DisplayName.AsValueTask(); } - public virtual ValueTask> GetDisplayNamesAsync(ImmutableApplication application, CancellationToken cancellationToken) + public virtual ValueTask> GetDisplayNamesAsync(ImmutableApplication application, + CancellationToken cancellationToken) { return application.DisplayNames.AsValueTask(); } - public virtual ValueTask> GetPermissionsAsync(ImmutableApplication application, CancellationToken cancellationToken) + public virtual ValueTask> GetPermissionsAsync(ImmutableApplication application, + CancellationToken cancellationToken) { return application.Permissions.AsValueTask(); } - public virtual ValueTask> GetPostLogoutRedirectUrisAsync(ImmutableApplication application, CancellationToken cancellationToken) + public virtual ValueTask> GetPostLogoutRedirectUrisAsync(ImmutableApplication application, + CancellationToken cancellationToken) { return application.PostLogoutRedirectUris.AsValueTask(); } - public virtual ValueTask> GetRedirectUrisAsync(ImmutableApplication application, CancellationToken cancellationToken) + public virtual ValueTask> GetRedirectUrisAsync(ImmutableApplication application, + CancellationToken cancellationToken) { return application.RedirectUris.AsValueTask(); } - public virtual ValueTask> GetRequirementsAsync(ImmutableApplication application, CancellationToken cancellationToken) + public virtual ValueTask> GetRequirementsAsync(ImmutableApplication application, + CancellationToken cancellationToken) { return application.Requirements.AsValueTask(); } - public virtual ValueTask> GetPropertiesAsync(ImmutableApplication application, CancellationToken cancellationToken) + public virtual ValueTask> GetPropertiesAsync(ImmutableApplication application, + CancellationToken cancellationToken) { return application.Properties.AsValueTask(); } - public virtual ValueTask CreateAsync(ImmutableApplication application, CancellationToken cancellationToken) + public virtual ValueTask CreateAsync(ImmutableApplication application, + CancellationToken cancellationToken) { throw new NotSupportedException(); } - public virtual ValueTask UpdateAsync(ImmutableApplication application, CancellationToken cancellationToken) + public virtual ValueTask UpdateAsync(ImmutableApplication application, + CancellationToken cancellationToken) { throw new NotSupportedException(); } - public virtual ValueTask DeleteAsync(ImmutableApplication application, CancellationToken cancellationToken) + public virtual ValueTask DeleteAsync(ImmutableApplication application, + CancellationToken cancellationToken) { throw new NotSupportedException(); } - public virtual ValueTask InstantiateAsync(CancellationToken cancellationToken) + public virtual ValueTask InstantiateAsync( + CancellationToken cancellationToken) { throw new NotSupportedException(); } - public virtual ValueTask SetClientIdAsync(ImmutableApplication application, string? identifier, CancellationToken cancellationToken) + public virtual ValueTask SetClientIdAsync(ImmutableApplication application, string? identifier, + CancellationToken cancellationToken) { throw new NotSupportedException(); } - public virtual ValueTask SetClientSecretAsync(ImmutableApplication application, string? secret, CancellationToken cancellationToken) + public virtual ValueTask SetClientSecretAsync(ImmutableApplication application, string? secret, + CancellationToken cancellationToken) { throw new NotSupportedException(); } - public virtual ValueTask SetClientTypeAsync(ImmutableApplication application, string? type, CancellationToken cancellationToken) + public virtual ValueTask SetClientTypeAsync(ImmutableApplication application, string? type, + CancellationToken cancellationToken) { throw new NotSupportedException(); } - public virtual ValueTask SetConsentTypeAsync(ImmutableApplication application, string? type, CancellationToken cancellationToken) + public virtual ValueTask SetConsentTypeAsync(ImmutableApplication application, string? type, + CancellationToken cancellationToken) { throw new NotSupportedException(); } - public virtual ValueTask SetDisplayNameAsync(ImmutableApplication application, string? name, CancellationToken cancellationToken) + public virtual ValueTask SetDisplayNameAsync(ImmutableApplication application, string? name, + CancellationToken cancellationToken) { throw new NotSupportedException(); } - public virtual ValueTask SetDisplayNamesAsync(ImmutableApplication application, ImmutableDictionary names, CancellationToken cancellationToken) + public virtual ValueTask SetDisplayNamesAsync(ImmutableApplication application, ImmutableDictionary names, + CancellationToken cancellationToken) { throw new NotSupportedException(); } - public virtual ValueTask SetPermissionsAsync(ImmutableApplication application, ImmutableArray permissions, CancellationToken cancellationToken) + public virtual ValueTask SetPermissionsAsync(ImmutableApplication application, ImmutableArray permissions, + CancellationToken cancellationToken) { throw new NotSupportedException(); } - public virtual ValueTask SetPostLogoutRedirectUrisAsync(ImmutableApplication application, ImmutableArray addresses, CancellationToken cancellationToken) + public virtual ValueTask SetPostLogoutRedirectUrisAsync(ImmutableApplication application, ImmutableArray addresses, + CancellationToken cancellationToken) { throw new NotSupportedException(); } - public virtual ValueTask SetRedirectUrisAsync(ImmutableApplication application, ImmutableArray addresses, CancellationToken cancellationToken) + public virtual ValueTask SetRedirectUrisAsync(ImmutableApplication application, ImmutableArray addresses, + CancellationToken cancellationToken) { throw new NotSupportedException(); } - public virtual ValueTask SetPropertiesAsync(ImmutableApplication application, ImmutableDictionary properties, CancellationToken cancellationToken) + public virtual ValueTask SetPropertiesAsync(ImmutableApplication application, ImmutableDictionary properties, + CancellationToken cancellationToken) { throw new NotSupportedException(); } - public virtual ValueTask SetRequirementsAsync(ImmutableApplication application, ImmutableArray requirements, CancellationToken cancellationToken) + public virtual ValueTask SetRequirementsAsync(ImmutableApplication application, ImmutableArray requirements, + CancellationToken cancellationToken) { throw new NotSupportedException(); } diff --git a/backend/src/Squidex.Domain.Users/InMemory/InMemoryScopeStore.cs b/backend/src/Squidex.Domain.Users/InMemory/InMemoryScopeStore.cs index e437d0878..50889ec5c 100644 --- a/backend/src/Squidex.Domain.Users/InMemory/InMemoryScopeStore.cs +++ b/backend/src/Squidex.Domain.Users/InMemory/InMemoryScopeStore.cs @@ -32,38 +32,44 @@ namespace Squidex.Domain.Users.InMemory this.scopes = scopes.Select(x => new ImmutableScope(x.Id, x.Descriptor)).ToList(); } - public virtual ValueTask CountAsync(CancellationToken cancellationToken) + public virtual ValueTask CountAsync( + CancellationToken cancellationToken) { return new ValueTask(scopes.Count); } - public virtual ValueTask CountAsync(Func, IQueryable> query, CancellationToken cancellationToken) + public virtual ValueTask CountAsync(Func, IQueryable> query, + CancellationToken cancellationToken) { return query(scopes.AsQueryable()).LongCount().AsValueTask(); } - public virtual ValueTask GetAsync(Func, TState, IQueryable> query, TState state, CancellationToken cancellationToken) + public virtual ValueTask GetAsync(Func, TState, IQueryable> query, TState state, + CancellationToken cancellationToken) { var result = query(scopes.AsQueryable(), state).First(); return result.AsValueTask(); } - public virtual ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken) + public virtual ValueTask FindByIdAsync(string identifier, + CancellationToken cancellationToken) { var result = scopes.Find(x => x.Id == identifier); return result.AsValueTask(); } - public virtual ValueTask FindByNameAsync(string name, CancellationToken cancellationToken) + public virtual ValueTask FindByNameAsync(string name, + CancellationToken cancellationToken) { var result = scopes.Find(x => x.Name == name); return result.AsValueTask(); } - public virtual async IAsyncEnumerable FindByNamesAsync(ImmutableArray names, [EnumeratorCancellation] CancellationToken cancellationToken) + public virtual async IAsyncEnumerable FindByNamesAsync(ImmutableArray names, + [EnumeratorCancellation] CancellationToken cancellationToken) { var result = scopes.Where(x => x.Name != null && names.Contains(x.Name)); @@ -73,7 +79,8 @@ namespace Squidex.Domain.Users.InMemory } } - public virtual async IAsyncEnumerable FindByResourceAsync(string resource, [EnumeratorCancellation] CancellationToken cancellationToken) + public virtual async IAsyncEnumerable FindByResourceAsync(string resource, + [EnumeratorCancellation] CancellationToken cancellationToken) { var result = scopes.Where(x => x.Resources.Contains(resource)); @@ -83,7 +90,8 @@ namespace Squidex.Domain.Users.InMemory } } - public virtual async IAsyncEnumerable ListAsync(int? count, int? offset, [EnumeratorCancellation] CancellationToken cancellationToken) + public virtual async IAsyncEnumerable ListAsync(int? count, int? offset, + [EnumeratorCancellation] CancellationToken cancellationToken) { var result = scopes; @@ -93,7 +101,8 @@ namespace Squidex.Domain.Users.InMemory } } - public virtual async IAsyncEnumerable ListAsync(Func, TState, IQueryable> query, TState state, [EnumeratorCancellation] CancellationToken cancellationToken) + public virtual async IAsyncEnumerable ListAsync(Func, TState, IQueryable> query, TState state, + [EnumeratorCancellation] CancellationToken cancellationToken) { var result = query(scopes.AsQueryable(), state); @@ -103,97 +112,116 @@ namespace Squidex.Domain.Users.InMemory } } - public virtual ValueTask GetIdAsync(ImmutableScope scope, CancellationToken cancellationToken) + public virtual ValueTask GetIdAsync(ImmutableScope scope, + CancellationToken cancellationToken) { return new ValueTask(scope.Id); } - public virtual ValueTask GetNameAsync(ImmutableScope scope, CancellationToken cancellationToken) + public virtual ValueTask GetNameAsync(ImmutableScope scope, + CancellationToken cancellationToken) { return scope.Name.AsValueTask(); } - public virtual ValueTask GetDescriptionAsync(ImmutableScope scope, CancellationToken cancellationToken) + public virtual ValueTask GetDescriptionAsync(ImmutableScope scope, + CancellationToken cancellationToken) { return scope.Description.AsValueTask(); } - public virtual ValueTask GetDisplayNameAsync(ImmutableScope scope, CancellationToken cancellationToken) + public virtual ValueTask GetDisplayNameAsync(ImmutableScope scope, + CancellationToken cancellationToken) { return scope.DisplayName.AsValueTask(); } - public virtual ValueTask> GetDescriptionsAsync(ImmutableScope scope, CancellationToken cancellationToken) + public virtual ValueTask> GetDescriptionsAsync(ImmutableScope scope, + CancellationToken cancellationToken) { return scope.Descriptions.AsValueTask(); } - public virtual ValueTask> GetDisplayNamesAsync(ImmutableScope scope, CancellationToken cancellationToken) + public virtual ValueTask> GetDisplayNamesAsync(ImmutableScope scope, + CancellationToken cancellationToken) { return scope.DisplayNames.AsValueTask(); } - public virtual ValueTask> GetPropertiesAsync(ImmutableScope scope, CancellationToken cancellationToken) + public virtual ValueTask> GetPropertiesAsync(ImmutableScope scope, + CancellationToken cancellationToken) { return scope.Properties.AsValueTask(); } - public virtual ValueTask> GetResourcesAsync(ImmutableScope scope, CancellationToken cancellationToken) + public virtual ValueTask> GetResourcesAsync(ImmutableScope scope, + CancellationToken cancellationToken) { return scope.Resources.AsValueTask(); } - public virtual ValueTask CreateAsync(ImmutableScope scope, CancellationToken cancellationToken) + public virtual ValueTask CreateAsync(ImmutableScope scope, + CancellationToken cancellationToken) { throw new NotSupportedException(); } - public virtual ValueTask UpdateAsync(ImmutableScope scope, CancellationToken cancellationToken) + public virtual ValueTask UpdateAsync(ImmutableScope scope, + CancellationToken cancellationToken) { throw new NotSupportedException(); } - public virtual ValueTask DeleteAsync(ImmutableScope scope, CancellationToken cancellationToken) + public virtual ValueTask DeleteAsync(ImmutableScope scope, + CancellationToken cancellationToken) { throw new NotSupportedException(); } - public virtual ValueTask InstantiateAsync(CancellationToken cancellationToken) + public virtual ValueTask InstantiateAsync( + CancellationToken cancellationToken) { throw new NotSupportedException(); } - public virtual ValueTask SetDescriptionAsync(ImmutableScope scope, string? description, CancellationToken cancellationToken) + public virtual ValueTask SetDescriptionAsync(ImmutableScope scope, string? description, + CancellationToken cancellationToken) { throw new NotSupportedException(); } - public virtual ValueTask SetDescriptionsAsync(ImmutableScope scope, ImmutableDictionary descriptions, CancellationToken cancellationToken) + public virtual ValueTask SetDescriptionsAsync(ImmutableScope scope, ImmutableDictionary descriptions, + CancellationToken cancellationToken) { throw new NotSupportedException(); } - public virtual ValueTask SetDisplayNameAsync(ImmutableScope scope, string? name, CancellationToken cancellationToken) + public virtual ValueTask SetDisplayNameAsync(ImmutableScope scope, string? name, + CancellationToken cancellationToken) { throw new NotSupportedException(); } - public virtual ValueTask SetDisplayNamesAsync(ImmutableScope scope, ImmutableDictionary names, CancellationToken cancellationToken) + public virtual ValueTask SetDisplayNamesAsync(ImmutableScope scope, ImmutableDictionary names, + CancellationToken cancellationToken) { throw new NotSupportedException(); } - public virtual ValueTask SetNameAsync(ImmutableScope scope, string? name, CancellationToken cancellationToken) + public virtual ValueTask SetNameAsync(ImmutableScope scope, string? name, + CancellationToken cancellationToken) { throw new NotSupportedException(); } - public virtual ValueTask SetPropertiesAsync(ImmutableScope scope, ImmutableDictionary properties, CancellationToken cancellationToken) + public virtual ValueTask SetPropertiesAsync(ImmutableScope scope, ImmutableDictionary properties, + CancellationToken cancellationToken) { throw new NotSupportedException(); } - public virtual ValueTask SetResourcesAsync(ImmutableScope scope, ImmutableArray resources, CancellationToken cancellationToken) + public virtual ValueTask SetResourcesAsync(ImmutableScope scope, ImmutableArray resources, + CancellationToken cancellationToken) { throw new NotSupportedException(); } diff --git a/backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj b/backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj index 49dac07fb..08c6b589c 100644 --- a/backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj +++ b/backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj @@ -17,6 +17,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/backend/src/Squidex.Infrastructure.Azure/Diagnostics/CosmosDbHealthCheck.cs b/backend/src/Squidex.Infrastructure.Azure/Diagnostics/CosmosDbHealthCheck.cs index 09698e250..9625c9571 100644 --- a/backend/src/Squidex.Infrastructure.Azure/Diagnostics/CosmosDbHealthCheck.cs +++ b/backend/src/Squidex.Infrastructure.Azure/Diagnostics/CosmosDbHealthCheck.cs @@ -22,7 +22,8 @@ namespace Squidex.Infrastructure.Diagnostics documentClient = new DocumentClient(uri, masterKey); } - public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + public async Task CheckHealthAsync(HealthCheckContext context, + CancellationToken cancellationToken = default) { await documentClient.ReadDatabaseFeedAsync(); diff --git a/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore.cs b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore.cs index 704260f98..f756ca0b2 100644 --- a/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore.cs +++ b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbEventStore.cs @@ -56,7 +56,8 @@ namespace Squidex.Infrastructure.EventSourcing } } - public async Task InitializeAsync(CancellationToken ct = default) + public async Task InitializeAsync( + CancellationToken ct = default) { await documentClient.CreateDatabaseIfNotExistsAsync(new Database { Id = DatabaseId }); diff --git a/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbSubscription.cs b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbSubscription.cs index b29635c27..97ff67e3d 100644 --- a/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbSubscription.cs +++ b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/CosmosDbSubscription.cs @@ -104,7 +104,8 @@ namespace Squidex.Infrastructure.EventSourcing return Task.CompletedTask; } - public async Task ProcessChangesAsync(IChangeFeedObserverContext context, IReadOnlyList docs, CancellationToken cancellationToken) + public async Task ProcessChangesAsync(IChangeFeedObserverContext context, IReadOnlyList docs, + CancellationToken cancellationToken) { if (!processorStopRequested.Task.IsCompleted) { diff --git a/backend/src/Squidex.Infrastructure.Azure/EventSourcing/FilterExtensions.cs b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/FilterExtensions.cs index f5e074b55..012baf0b6 100644 --- a/backend/src/Squidex.Infrastructure.Azure/EventSourcing/FilterExtensions.cs +++ b/backend/src/Squidex.Infrastructure.Azure/EventSourcing/FilterExtensions.cs @@ -24,7 +24,8 @@ namespace Squidex.Infrastructure.EventSourcing EnableCrossPartitionQuery = true }; - public static async Task FirstOrDefaultAsync(this IQueryable queryable, CancellationToken ct = default) + public static async Task FirstOrDefaultAsync(this IQueryable queryable, + CancellationToken ct = default) { var documentQuery = queryable.AsDocumentQuery(); diff --git a/backend/src/Squidex.Infrastructure.GetEventStore/Diagnostics/GetEventStoreHealthCheck.cs b/backend/src/Squidex.Infrastructure.GetEventStore/Diagnostics/GetEventStoreHealthCheck.cs index 4636480f4..fd2f40436 100644 --- a/backend/src/Squidex.Infrastructure.GetEventStore/Diagnostics/GetEventStoreHealthCheck.cs +++ b/backend/src/Squidex.Infrastructure.GetEventStore/Diagnostics/GetEventStoreHealthCheck.cs @@ -21,7 +21,8 @@ namespace Squidex.Infrastructure.Diagnostics this.connection = connection; } - public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + public async Task CheckHealthAsync(HealthCheckContext context, + CancellationToken cancellationToken = default) { await connection.ReadEventAsync("test", 1, false); diff --git a/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs index 25c26a8a8..f7e128785 100644 --- a/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs +++ b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStore.cs @@ -43,7 +43,8 @@ namespace Squidex.Infrastructure.EventSourcing projectionClient = new ProjectionClient(connection, this.prefix, projectionHost); } - public async Task InitializeAsync(CancellationToken ct = default) + public async Task InitializeAsync( + CancellationToken ct = default) { try { diff --git a/backend/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoDBHealthCheck.cs b/backend/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoDBHealthCheck.cs index 971b08bb1..6f842f832 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoDBHealthCheck.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/Diagnostics/MongoDBHealthCheck.cs @@ -21,7 +21,8 @@ namespace Squidex.Infrastructure.Diagnostics this.mongoDatabase = mongoDatabase; } - public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + public async Task CheckHealthAsync(HealthCheckContext context, + CancellationToken cancellationToken = default) { var collectionNames = await mongoDatabase.ListCollectionNamesAsync(cancellationToken: cancellationToken); diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/FilterExtensions.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/FilterExtensions.cs index da66bffb5..70506163d 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/FilterExtensions.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/FilterExtensions.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; using MongoDB.Driver; @@ -31,7 +32,7 @@ namespace Squidex.Infrastructure.EventSourcing return null; } - if (streamFilter.Contains("^")) + if (streamFilter.Contains("^", StringComparison.Ordinal)) { return Builders.Filter.Regex(x => x.EventStream, streamFilter); } @@ -48,7 +49,7 @@ namespace Squidex.Infrastructure.EventSourcing return null; } - if (streamFilter.Contains("^")) + if (streamFilter.Contains("^", StringComparison.Ordinal)) { return Builders>.Filter.Regex(x => x.FullDocument.EventStream, streamFilter); } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs index 974fb710e..07d2948e9 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs @@ -77,10 +77,10 @@ namespace Squidex.Infrastructure.EventSourcing }) }, ct); - var clusterVersion = await Database.GetVersionAsync(); - var clustered = Database.Client.Cluster.Description.Type == ClusterType.ReplicaSet; + var clusterVersion = await Database.GetVersionAsync(ct); + var clusteredAsReplica = Database.Client.Cluster.Description.Type == ClusterType.ReplicaSet; - CanUseChangeStreams = clustered && clusterVersion >= new Version("4.0"); + CanUseChangeStreams = clusteredAsReplica && clusterVersion >= new Version("4.0"); } } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStoreSubscription.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStoreSubscription.cs index 26ab00340..f75f046cc 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStoreSubscription.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStoreSubscription.cs @@ -100,7 +100,7 @@ namespace Squidex.Infrastructure.EventSourcing if (!isRead) { - await Task.Delay(1000); + await Task.Delay(1000, stopToken.Token); } } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs index 66092a5bc..cfb9b90b0 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs @@ -38,7 +38,8 @@ namespace Squidex.Infrastructure.EventSourcing } } - public async Task> QueryLatestAsync(string streamName, int count) + public async Task> QueryLatestAsync(string streamName, int count = int.MaxValue, + CancellationToken ct = default) { Guard.NotNullOrEmpty(streamName, nameof(streamName)); @@ -49,10 +50,11 @@ namespace Squidex.Infrastructure.EventSourcing using (Telemetry.Activities.StartActivity("MongoEventStore/QueryLatestAsync")) { + var filter = Filter.Eq(EventStreamField, streamName); + var commits = - await Collection.Find( - Filter.Eq(EventStreamField, streamName)) - .Sort(Sort.Descending(TimestampField)).Limit(count).ToListAsync(); + await Collection.Find(filter).Sort(Sort.Descending(TimestampField)).Limit(count) + .ToListAsync(ct); var result = commits.Select(x => x.Filtered()).Reverse().SelectMany(x => x).TakeLast(count).ToList(); @@ -60,18 +62,21 @@ namespace Squidex.Infrastructure.EventSourcing } } - public async Task> QueryAsync(string streamName, long streamPosition = 0) + public async Task> QueryAsync(string streamName, long streamPosition = 0, + CancellationToken ct = default) { Guard.NotNullOrEmpty(streamName, nameof(streamName)); using (Telemetry.Activities.StartActivity("MongoEventStore/QueryAsync")) { + var filter = + Filter.And( + Filter.Eq(EventStreamField, streamName), + Filter.Gte(EventStreamOffsetField, streamPosition - MaxCommitSize)); + var commits = - await Collection.Find( - Filter.And( - Filter.Eq(EventStreamField, streamName), - Filter.Gte(EventStreamOffsetField, streamPosition - MaxCommitSize))) - .Sort(Sort.Ascending(TimestampField)).ToListAsync(); + await Collection.Find(filter).Sort(Sort.Ascending(TimestampField)) + .ToListAsync(ct); var result = commits.SelectMany(x => x.Filtered(streamPosition)).ToList(); @@ -79,7 +84,8 @@ namespace Squidex.Infrastructure.EventSourcing } } - public async Task>> QueryManyAsync(IEnumerable streamNames) + public async Task>> QueryManyAsync(IEnumerable streamNames, + CancellationToken ct = default) { Guard.NotNull(streamNames, nameof(streamNames)); @@ -87,12 +93,14 @@ namespace Squidex.Infrastructure.EventSourcing { var position = EtagVersion.Empty; + var filter = + Filter.And( + Filter.In(EventStreamField, streamNames), + Filter.Gte(EventStreamOffsetField, position)); + var commits = - await Collection.Find( - Filter.And( - Filter.In(EventStreamField, streamNames), - Filter.Gte(EventStreamOffsetField, position))) - .Sort(Sort.Ascending(TimestampField)).ToListAsync(); + await Collection.Find(filter).Sort(Sort.Ascending(TimestampField)) + .ToListAsync(ct); var result = commits.GroupBy(x => x.EventStream) .ToDictionary( @@ -116,7 +124,7 @@ namespace Squidex.Infrastructure.EventSourcing var filterDefinition = CreateFilter(streamFilter, lastPosition); var find = - Collection.Find(filterDefinition, options: Batching.Options) + Collection.Find(filterDefinition, Batching.Options) .Limit(take).Sort(Sort.Descending(TimestampField).Ascending(EventStreamField)); var taken = 0; @@ -161,28 +169,17 @@ namespace Squidex.Infrastructure.EventSourcing var taken = 0; - using (var cursor = await find.ToCursorAsync(ct)) + await foreach (var current in find.ToAsyncEnumerable(ct)) { - while (taken < take && await cursor.MoveNextAsync(ct)) + foreach (var @event in current.Filtered(lastPosition)) { - foreach (var current in cursor.Current) - { - foreach (var @event in current.Filtered(lastPosition)) - { - yield return @event; + yield return @event; - taken++; + taken++; - if (taken == take) - { - break; - } - } - - if (taken == take) - { - break; - } + if (taken == take) + { + break; } } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs index 41bf5160b..ab226e6be 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using MongoDB.Bson; using MongoDB.Driver; @@ -19,19 +20,30 @@ namespace Squidex.Infrastructure.EventSourcing private const int MaxWriteAttempts = 20; private static readonly BsonTimestamp EmptyTimestamp = new BsonTimestamp(0); - public Task DeleteStreamAsync(string streamName) + public Task DeleteStreamAsync(string streamName, + CancellationToken ct = default) { Guard.NotNullOrEmpty(streamName, nameof(streamName)); - return Collection.DeleteManyAsync(x => x.EventStream == streamName); + return Collection.DeleteManyAsync(x => x.EventStream == streamName, ct); } - public Task AppendAsync(Guid commitId, string streamName, ICollection events) + public Task DeleteAsync(string streamFilter, + CancellationToken ct = default) { - return AppendAsync(commitId, streamName, EtagVersion.Any, events); + Guard.NotNullOrEmpty(streamFilter, nameof(streamFilter)); + + return Collection.DeleteManyAsync(FilterExtensions.ByStream(streamFilter), ct); + } + + public Task AppendAsync(Guid commitId, string streamName, ICollection events, + CancellationToken ct = default) + { + return AppendAsync(commitId, streamName, EtagVersion.Any, events, ct); } - public async Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection events) + public async Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection events, + CancellationToken ct = default) { Guard.NotEmpty(commitId, nameof(commitId)); Guard.NotNullOrEmpty(streamName, nameof(streamName)); @@ -46,7 +58,7 @@ namespace Squidex.Infrastructure.EventSourcing return; } - var currentVersion = await GetEventStreamOffsetAsync(streamName); + var currentVersion = await GetEventStreamOffsetAsync(streamName, ct); if (expectedVersion > EtagVersion.Any && expectedVersion != currentVersion) { @@ -59,7 +71,7 @@ namespace Squidex.Infrastructure.EventSourcing { try { - await Collection.InsertOneAsync(commit); + await Collection.InsertOneAsync(commit, cancellationToken: ct); if (!CanUseChangeStreams) { @@ -72,7 +84,7 @@ namespace Squidex.Infrastructure.EventSourcing { if (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey) { - currentVersion = await GetEventStreamOffsetAsync(streamName); + currentVersion = await GetEventStreamOffsetAsync(streamName, ct); if (expectedVersion > EtagVersion.Any) { @@ -97,7 +109,8 @@ namespace Squidex.Infrastructure.EventSourcing } } - public async Task AppendUnsafeAsync(IEnumerable commits) + public async Task AppendUnsafeAsync(IEnumerable commits, + CancellationToken ct = default) { Guard.NotNull(commits, nameof(commits)); @@ -114,12 +127,13 @@ namespace Squidex.Infrastructure.EventSourcing if (writes.Count > 0) { - await Collection.BulkWriteAsync(writes, BulkUnordered); + await Collection.BulkWriteAsync(writes, BulkUnordered, ct); } } } - private async Task GetEventStreamOffsetAsync(string streamName) + private async Task GetEventStreamOffsetAsync(string streamName, + CancellationToken ct = default) { var document = await Collection.Find(Filter.Eq(EventStreamField, streamName)) @@ -127,7 +141,7 @@ namespace Squidex.Infrastructure.EventSourcing .Include(EventStreamOffsetField) .Include(EventsCountField)) .Sort(Sort.Descending(EventStreamOffsetField)).Limit(1) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); if (document != null) { diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/StreamPosition.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/StreamPosition.cs index fc683ad3c..c21a9cbbc 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/StreamPosition.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/StreamPosition.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Globalization; using MongoDB.Bson; using NodaTime; using Squidex.Infrastructure.ObjectPool; @@ -62,10 +63,14 @@ namespace Squidex.Infrastructure.EventSourcing if (parts.Length == 4) { + var culture = CultureInfo.InvariantCulture; + return new StreamPosition( - new BsonTimestamp(int.Parse(parts[0]), int.Parse(parts[1])), - long.Parse(parts[2]), - long.Parse(parts[3])); + new BsonTimestamp( + int.Parse(parts[0], NumberStyles.Integer, culture), + int.Parse(parts[1], NumberStyles.Integer, culture)), + long.Parse(parts[2], NumberStyles.Integer, culture), + long.Parse(parts[3], NumberStyles.Integer, culture)); } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/Log/MongoRequest.cs b/backend/src/Squidex.Infrastructure.MongoDb/Log/MongoRequest.cs index f2b54d568..fe7ed1284 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/Log/MongoRequest.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/Log/MongoRequest.cs @@ -29,5 +29,15 @@ namespace Squidex.Infrastructure.Log [BsonElement] [BsonRequired] public Dictionary Properties { get; set; } + + public static MongoRequest FromRequest(Request request) + { + return new MongoRequest { Key = request.Key, Timestamp = request.Timestamp, Properties = request.Properties }; + } + + public Request ToRequest() + { + return new Request { Key = Key, Timestamp = Timestamp, Properties = Properties }; + } } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/Log/MongoRequestLogRepository.cs b/backend/src/Squidex.Infrastructure.MongoDb/Log/MongoRequestLogRepository.cs index 14f353afa..cbdd59fc0 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/Log/MongoRequestLogRepository.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/Log/MongoRequestLogRepository.cs @@ -35,7 +35,7 @@ namespace Squidex.Infrastructure.Log } protected override Task SetupCollectionAsync(IMongoCollection collection, - CancellationToken ct = default) + CancellationToken ct) { return collection.Indexes.CreateManyAsync(new[] { @@ -53,34 +53,40 @@ namespace Squidex.Infrastructure.Log }, ct); } - public Task InsertManyAsync(IEnumerable items) + public Task InsertManyAsync(IEnumerable items, + CancellationToken ct = default) { Guard.NotNull(items, nameof(items)); - var entities = items.Select(x => new MongoRequest { Key = x.Key, Timestamp = x.Timestamp, Properties = x.Properties }).ToList(); + var entities = items.Select(MongoRequest.FromRequest).ToList(); if (entities.Count == 0) { return Task.CompletedTask; } - return Collection.InsertManyAsync(entities, InsertUnordered); + return Collection.InsertManyAsync(entities, InsertUnordered, ct); } - public Task QueryAllAsync(Func callback, string key, DateTime fromDate, DateTime toDate, CancellationToken ct = default) + public Task DeleteAsync(string key, + CancellationToken ct = default) + { + Guard.NotNullOrEmpty(key, nameof(key)); + + return Collection.DeleteManyAsync(Filter.Eq(x => x.Key, key), ct); + } + + public IAsyncEnumerable QueryAllAsync(string key, DateTime fromDate, DateTime toDate, + CancellationToken ct = default) { - Guard.NotNull(callback, nameof(callback)); Guard.NotNullOrEmpty(key, nameof(key)); var timestampStart = Instant.FromDateTimeUtc(fromDate); var timestampEnd = Instant.FromDateTimeUtc(toDate.AddDays(1)); - return Collection.Find(x => x.Key == key && x.Timestamp >= timestampStart && x.Timestamp < timestampEnd).ForEachAsync(x => - { - var request = new Request { Key = x.Key, Timestamp = x.Timestamp, Properties = x.Properties }; + var find = Collection.Find(x => x.Key == key && x.Timestamp >= timestampStart && x.Timestamp < timestampEnd); - return callback(request); - }, ct); + return find.ToAsyncEnumerable(ct).Select(x => x.ToRequest()); } } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs b/backend/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs index abd1a1336..74f39017e 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Threading; using System.Threading.Tasks; using MongoDB.Driver; using Squidex.Infrastructure.MongoDb; @@ -26,37 +27,44 @@ namespace Squidex.Infrastructure.Migrations return "Migration"; } - public async Task GetVersionAsync() + public async Task GetVersionAsync( + CancellationToken ct = default) { - var entity = await Collection.Find(x => x.Id == DefaultId).FirstOrDefaultAsync(); + var entity = await Collection.Find(x => x.Id == DefaultId).FirstOrDefaultAsync(ct); return entity.Version; } - public async Task TryLockAsync() + public async Task TryLockAsync( + CancellationToken ct = default) { var entity = await Collection.FindOneAndUpdateAsync(x => x.Id == DefaultId, Update .Set(x => x.IsLocked, true) .SetOnInsert(x => x.Version, 0), - UpsertFind); + UpsertFind, + ct); - return entity == null || entity.IsLocked == false; + return entity == null || !entity.IsLocked; } - public Task CompleteAsync(int newVersion) + public Task CompleteAsync(int newVersion, + CancellationToken ct = default) { return Collection.UpdateOneAsync(x => x.Id == DefaultId, Update - .Set(x => x.Version, newVersion)); + .Set(x => x.Version, newVersion), + cancellationToken: ct); } - public Task UnlockAsync() + public Task UnlockAsync( + CancellationToken ct = default) { return Collection.UpdateOneAsync(x => x.Id == DefaultId, Update - .Set(x => x.IsLocked, false)); + .Set(x => x.IsLocked, false), + cancellationToken: ct); } } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonHelper.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonHelper.cs index 9a737c3a9..ba1cbce97 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonHelper.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonHelper.cs @@ -5,6 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; + namespace Squidex.Infrastructure.MongoDb { public static class BsonHelper @@ -27,7 +29,7 @@ namespace Squidex.Infrastructure.MongoDb return TypeJson; } - var result = value.ReplaceFirst('§', '$').Replace(DotReplacement, DotSource); + var result = value.ReplaceFirst('§', '$').Replace(DotReplacement, DotSource, StringComparison.Ordinal); return result; } @@ -44,7 +46,7 @@ namespace Squidex.Infrastructure.MongoDb return TypeBson; } - var result = value.ReplaceFirst('$', '§').Replace(DotSource, DotReplacement); + var result = value.ReplaceFirst('$', '§').Replace(DotSource, DotReplacement, StringComparison.Ordinal); return result; } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs index f424ed71f..6562f559e 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs @@ -6,42 +6,44 @@ // ========================================================================== using System; +using System.Collections.Generic; using System.Linq.Expressions; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using MongoDB.Bson; using MongoDB.Driver; using Squidex.Infrastructure.States; -#pragma warning disable RECS0022 // A catch clause that catches System.Exception and has an empty body - namespace Squidex.Infrastructure.MongoDb { public static class MongoExtensions { - private static readonly UpdateOptions Upsert = new UpdateOptions { IsUpsert = true }; private static readonly ReplaceOptions UpsertReplace = new ReplaceOptions { IsUpsert = true }; - public static async Task CollectionExistsAsync(this IMongoDatabase database, string collectionName) + public static async Task CollectionExistsAsync(this IMongoDatabase database, string collectionName, + CancellationToken ct = default) { var options = new ListCollectionNamesOptions { Filter = new BsonDocument("name", collectionName) }; - var collections = await database.ListCollectionNamesAsync(options); + var collections = await database.ListCollectionNamesAsync(options, ct); - return await collections.AnyAsync(); + return await collections.AnyAsync(ct); } - public static Task AnyAsync(this IMongoCollection collection) + public static Task AnyAsync(this IMongoCollection collection, + CancellationToken ct = default) { var find = collection.Find(new BsonDocument()).Limit(1); - return find.AnyAsync(); + return find.AnyAsync(ct); } - public static async Task InsertOneIfNotExistsAsync(this IMongoCollection collection, T document, CancellationToken ct = default) + public static async Task InsertOneIfNotExistsAsync(this IMongoCollection collection, T document, + CancellationToken ct = default) { try { @@ -55,15 +57,19 @@ namespace Squidex.Infrastructure.MongoDb return true; } - public static async Task TryDropOneAsync(this IMongoIndexManager indexes, string name) + public static async IAsyncEnumerable ToAsyncEnumerable(this IFindFluent find, + [EnumeratorCancellation] CancellationToken ct = default) { - try - { - await indexes.DropOneAsync(name); - } - catch + var cursor = await find.ToCursorAsync(ct); + + while (await cursor.MoveNextAsync(ct)) { - /* NOOP */ + foreach (var item in cursor.Current) + { + ct.ThrowIfCancellationRequested(); + + yield return item; + } } } @@ -101,42 +107,8 @@ namespace Squidex.Infrastructure.MongoDb return find.Project(Builders.Projection.Exclude(exclude1).Exclude(exclude2)); } - public static async Task UpsertVersionedAsync(this IMongoCollection collection, TKey key, long oldVersion, long newVersion, Func, UpdateDefinition> updater) - where T : IVersionedEntity where TKey : notnull - { - try - { - var update = updater(Builders.Update.Set(x => x.Version, newVersion)); - - if (oldVersion > EtagVersion.Any) - { - await collection.UpdateOneAsync(x => x.DocumentId.Equals(key) && x.Version == oldVersion, update, Upsert); - } - else - { - await collection.UpdateOneAsync(x => x.DocumentId.Equals(key), update, Upsert); - } - } - catch (MongoWriteException ex) when (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey) - { - var existingVersion = - await collection.Find(x => x.DocumentId.Equals(key)).Only(x => x.DocumentId, x => x.Version) - .FirstOrDefaultAsync(); - - if (existingVersion != null) - { - var field = Field.Of(x => nameof(x.Version)); - - throw new InconsistentStateException(existingVersion[field].AsInt64, oldVersion, ex); - } - else - { - throw new InconsistentStateException(EtagVersion.Any, oldVersion, ex); - } - } - } - - public static async Task UpsertVersionedAsync(this IMongoCollection collection, TKey key, long oldVersion, long newVersion, T document) + public static async Task UpsertVersionedAsync(this IMongoCollection collection, TKey key, long oldVersion, long newVersion, T document, + CancellationToken ct = default) where T : IVersionedEntity where TKey : notnull { try @@ -146,18 +118,18 @@ namespace Squidex.Infrastructure.MongoDb if (oldVersion > EtagVersion.Any) { - await collection.ReplaceOneAsync(x => x.DocumentId.Equals(key) && x.Version == oldVersion, document, UpsertReplace); + await collection.ReplaceOneAsync(x => x.DocumentId.Equals(key) && x.Version == oldVersion, document, UpsertReplace, ct); } else { - await collection.ReplaceOneAsync(x => x.DocumentId.Equals(key), document, UpsertReplace); + await collection.ReplaceOneAsync(x => x.DocumentId.Equals(key), document, UpsertReplace, ct); } } catch (MongoWriteException ex) when (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey) { var existingVersion = await collection.Find(x => x.DocumentId.Equals(key)).Only(x => x.DocumentId, x => x.Version) - .FirstOrDefaultAsync(); + .FirstOrDefaultAsync(ct); if (existingVersion != null) { @@ -172,7 +144,8 @@ namespace Squidex.Infrastructure.MongoDb } } - public static async Task GetVersionAsync(this IMongoDatabase database) + public static async Task GetVersionAsync(this IMongoDatabase database, + CancellationToken ct = default) { var command = new BsonDocumentCommand(new BsonDocument @@ -180,7 +153,7 @@ namespace Squidex.Infrastructure.MongoDb { "buildInfo", 1 } }); - var result = await database.RunCommandAsync(command); + var result = await database.RunCommandAsync(command, cancellationToken: ct); return Version.Parse(result["version"].AsString); } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs index de3a9b1f3..2b6fafd12 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs @@ -90,11 +90,12 @@ namespace Squidex.Infrastructure.MongoDb return Task.CompletedTask; } - public virtual async Task ClearAsync() + public virtual async Task ClearAsync( + CancellationToken ct = default) { try { - await Database.DropCollectionAsync(CollectionName()); + await Database.DropCollectionAsync(CollectionName(), ct); } catch (MongoCommandException ex) { @@ -104,10 +105,11 @@ namespace Squidex.Infrastructure.MongoDb } } - await InitializeAsync(); + await InitializeAsync(ct); } - public async Task InitializeAsync(CancellationToken ct = default) + public async Task InitializeAsync( + CancellationToken ct) { try { diff --git a/backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj b/backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj index 58c4a2330..2f0c286ed 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj +++ b/backend/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj @@ -13,6 +13,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs b/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs index 36871729d..032718502 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs @@ -5,108 +5,16 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MongoDB.Bson; using MongoDB.Driver; using Newtonsoft.Json; -using Squidex.Infrastructure.MongoDb; namespace Squidex.Infrastructure.States { - public class MongoSnapshotStore : MongoRepositoryBase>, ISnapshotStore + public sealed class MongoSnapshotStore : MongoSnapshotStoreBase> { public MongoSnapshotStore(IMongoDatabase database, JsonSerializer jsonSerializer) - : base(database, Register(jsonSerializer)) + : base(database, jsonSerializer) { } - - private static bool Register(JsonSerializer jsonSerializer) - { - Guard.NotNull(jsonSerializer, nameof(jsonSerializer)); - - BsonJsonConvention.Register(jsonSerializer); - - return true; - } - - protected override string CollectionName() - { - var attribute = typeof(T).GetCustomAttributes(true).OfType().FirstOrDefault(); - - var name = attribute?.Name ?? typeof(T).Name; - - return $"States_{name}"; - } - - public async Task<(T Value, bool Valid, long Version)> ReadAsync(DomainId key) - { - using (Telemetry.Activities.StartActivity("ContentQueryService/ReadAsync")) - { - var existing = - await Collection.Find(x => x.DocumentId.Equals(key)) - .FirstOrDefaultAsync(); - - if (existing != null) - { - return (existing.Doc, true, existing.Version); - } - - return (default!, true, EtagVersion.Empty); - } - } - - public async Task WriteAsync(DomainId key, T value, long oldVersion, long newVersion) - { - using (Telemetry.Activities.StartActivity("ContentQueryService/WriteAsync")) - { - await Collection.UpsertVersionedAsync(key, oldVersion, newVersion, u => u.Set(x => x.Doc, value)); - } - } - - public Task WriteManyAsync(IEnumerable<(DomainId Key, T Value, long Version)> snapshots) - { - using (Telemetry.Activities.StartActivity("ContentQueryService/WriteManyAsync")) - { - var writes = snapshots.Select(x => new ReplaceOneModel>( - Filter.Eq(y => y.DocumentId, x.Key), - new MongoState - { - Doc = x.Value, - DocumentId = x.Key, - Version = x.Version - }) - { - IsUpsert = true - }).ToList(); - - if (writes.Count == 0) - { - return Task.CompletedTask; - } - - return Collection.BulkWriteAsync(writes, BulkUnordered); - } - } - - public async Task ReadAllAsync(Func callback, - CancellationToken ct = default) - { - using (Telemetry.Activities.StartActivity("ContentQueryService/ReadAllAsync")) - { - await Collection.Find(new BsonDocument(), options: Batching.Options).ForEachAsync(x => callback(x.Doc, x.Version), ct); - } - } - - public async Task RemoveAsync(DomainId key) - { - using (Telemetry.Activities.StartActivity("ContentQueryService/RemoveAsync")) - { - await Collection.DeleteOneAsync(x => x.DocumentId.Equals(key)); - } - } } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStoreBase.cs b/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStoreBase.cs new file mode 100644 index 000000000..1ffdf5f67 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStoreBase.cs @@ -0,0 +1,131 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver; +using Newtonsoft.Json; +using Squidex.Infrastructure.MongoDb; + +namespace Squidex.Infrastructure.States +{ + public abstract class MongoSnapshotStoreBase : MongoRepositoryBase, ISnapshotStore where TState : MongoState, new() + { + protected MongoSnapshotStoreBase(IMongoDatabase database, JsonSerializer jsonSerializer) + : base(database, Register(jsonSerializer)) + { + } + + private static bool Register(JsonSerializer jsonSerializer) + { + Guard.NotNull(jsonSerializer, nameof(jsonSerializer)); + + BsonJsonConvention.Register(jsonSerializer); + + return true; + } + + protected override string CollectionName() + { + var attribute = typeof(T).GetCustomAttributes(true).OfType().FirstOrDefault(); + + var name = attribute?.Name ?? typeof(T).Name; + + return $"States_{name}"; + } + + public async Task<(T Value, bool Valid, long Version)> ReadAsync(DomainId key, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("ContentQueryService/ReadAsync")) + { + var existing = + await Collection.Find(x => x.DocumentId.Equals(key)) + .FirstOrDefaultAsync(ct); + + if (existing != null) + { + return (existing.Document, true, existing.Version); + } + + return (default!, true, EtagVersion.Empty); + } + } + + public async Task WriteAsync(DomainId key, T value, long oldVersion, long newVersion, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("ContentQueryService/WriteAsync")) + { + var document = CreateDocument(key, value, newVersion); + + await Collection.UpsertVersionedAsync(key, oldVersion, newVersion, document, ct); + } + } + + public async Task WriteManyAsync(IEnumerable<(DomainId Key, T Value, long Version)> snapshots, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("ContentQueryService/WriteManyAsync")) + { + var writes = snapshots.Select(x => + new ReplaceOneModel(Filter.Eq(y => y.DocumentId, x.Key), CreateDocument(x.Key, x.Value, x.Version)) + { + IsUpsert = true + }).ToList(); + + if (writes.Count == 0) + { + return; + } + + await Collection.BulkWriteAsync(writes, BulkUnordered, ct); + } + } + + public async Task RemoveAsync(DomainId key, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("ContentQueryService/RemoveAsync")) + { + await Collection.DeleteOneAsync(x => x.DocumentId.Equals(key), ct); + } + } + + public async IAsyncEnumerable<(T State, long Version)> ReadAllAsync( + [EnumeratorCancellation] CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("ContentQueryService/ReadAllAsync")) + { + var find = Collection.Find(new BsonDocument(), Batching.Options); + + await foreach (var document in find.ToAsyncEnumerable(ct)) + { + yield return (document.Document, document.Version); + } + } + } + + private static TState CreateDocument(DomainId id, T doc, long version) + { + var result = new TState + { + Document = doc, + DocumentId = id, + Version = version + }; + + result.Prepare(); + + return result; + } + } +} diff --git a/backend/src/Squidex.Infrastructure.MongoDb/States/MongoState.cs b/backend/src/Squidex.Infrastructure.MongoDb/States/MongoState.cs index 04e539994..90773b2ea 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/States/MongoState.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/States/MongoState.cs @@ -12,7 +12,7 @@ using Squidex.Infrastructure.MongoDb; namespace Squidex.Infrastructure.States { [BsonIgnoreExtraElements] - public sealed class MongoState : IVersionedEntity + public class MongoState : IVersionedEntity { [BsonId] [BsonElement] @@ -20,12 +20,16 @@ namespace Squidex.Infrastructure.States public DomainId DocumentId { get; set; } [BsonRequired] - [BsonElement] + [BsonElement("Doc")] [BsonJson] - public T Doc { get; set; } + public T Document { get; set; } [BsonRequired] [BsonElement] public long Version { get; set; } + + public virtual void Prepare() + { + } } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageRepository.cs b/backend/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageRepository.cs index 24b59b801..87c95036b 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageRepository.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/UsageTracking/MongoUsageRepository.cs @@ -28,7 +28,7 @@ namespace Squidex.Infrastructure.UsageTracking } protected override Task SetupCollectionAsync(IMongoCollection collection, - CancellationToken ct = default) + CancellationToken ct) { return collection.Indexes.CreateOneAsync( new CreateIndexModel( @@ -39,7 +39,16 @@ namespace Squidex.Infrastructure.UsageTracking cancellationToken: ct = default); } - public async Task TrackUsagesAsync(UsageUpdate update) + public Task DeleteAsync(string key, + CancellationToken ct = default) + { + Guard.NotNull(key, nameof(key)); + + return Collection.DeleteManyAsync(x => x.Key == key, ct); + } + + public async Task TrackUsagesAsync(UsageUpdate update, + CancellationToken ct = default) { Guard.NotNull(update, nameof(update)); @@ -47,17 +56,18 @@ namespace Squidex.Infrastructure.UsageTracking { var (filter, updateStatement) = CreateOperation(update); - await Collection.UpdateOneAsync(filter, updateStatement, Upsert); + await Collection.UpdateOneAsync(filter, updateStatement, Upsert, ct); } } - public async Task TrackUsagesAsync(params UsageUpdate[] updates) + public async Task TrackUsagesAsync(UsageUpdate[] updates, + CancellationToken ct = default) { Guard.NotNull(updates, nameof(updates)); if (updates.Length == 1) { - await TrackUsagesAsync(updates[0]); + await TrackUsagesAsync(updates[0], ct); } else if (updates.Length > 0) { @@ -73,7 +83,7 @@ namespace Squidex.Infrastructure.UsageTracking } } - await Collection.BulkWriteAsync(writes, BulkUnordered); + await Collection.BulkWriteAsync(writes, BulkUnordered, ct); } } @@ -96,9 +106,10 @@ namespace Squidex.Infrastructure.UsageTracking return (filter, update); } - public async Task> QueryAsync(string key, DateTime fromDate, DateTime toDate) + public async Task> QueryAsync(string key, DateTime fromDate, DateTime toDate, + CancellationToken ct = default) { - var entities = await Collection.Find(x => x.Key == key && x.Date >= fromDate && x.Date <= toDate).ToListAsync(); + var entities = await Collection.Find(x => x.Key == key && x.Date >= fromDate && x.Date <= toDate).ToListAsync(ct); return entities.Select(x => new StoredUsage(x.Category, x.Date, x.Counters)).ToList(); } diff --git a/backend/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs b/backend/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs index c8add61f2..2ba7d18af 100644 --- a/backend/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs +++ b/backend/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs @@ -58,7 +58,8 @@ namespace Squidex.Infrastructure.CQRS.Events } } - public Task InitializeAsync(CancellationToken ct = default) + public Task InitializeAsync( + CancellationToken ct) { try { diff --git a/backend/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj b/backend/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj index 36ff6a906..df49191bc 100644 --- a/backend/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj +++ b/backend/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj @@ -10,6 +10,10 @@ True + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/backend/src/Squidex.Infrastructure.Redis/RedisPubSub.cs b/backend/src/Squidex.Infrastructure.Redis/RedisPubSub.cs index b6bd45ec0..33286c134 100644 --- a/backend/src/Squidex.Infrastructure.Redis/RedisPubSub.cs +++ b/backend/src/Squidex.Infrastructure.Redis/RedisPubSub.cs @@ -31,7 +31,8 @@ namespace Squidex.Infrastructure this.serializer = serializer; } - public Task InitializeAsync(CancellationToken ct = default) + public Task InitializeAsync( + CancellationToken ct = default) { try { diff --git a/backend/src/Squidex.Infrastructure/Commands/InMemoryCommandBus.cs b/backend/src/Squidex.Infrastructure/Commands/InMemoryCommandBus.cs index 9d776c4d8..3521834a9 100644 --- a/backend/src/Squidex.Infrastructure/Commands/InMemoryCommandBus.cs +++ b/backend/src/Squidex.Infrastructure/Commands/InMemoryCommandBus.cs @@ -20,7 +20,7 @@ namespace Squidex.Infrastructure.Commands Task InvokeAsync(CommandContext context); } - private class NoopStep : IStep + private sealed class NoopStep : IStep { public Task InvokeAsync(CommandContext context) { @@ -28,7 +28,7 @@ namespace Squidex.Infrastructure.Commands } } - private class DefaultStep : IStep + private sealed class DefaultStep : IStep { private readonly IStep next; private readonly ICommandMiddleware middleware; @@ -71,4 +71,4 @@ namespace Squidex.Infrastructure.Commands return context; } } -} \ No newline at end of file +} diff --git a/backend/src/Squidex.Infrastructure/Commands/Is.cs b/backend/src/Squidex.Infrastructure/Commands/Is.cs index 309672b1e..8011b8d68 100644 --- a/backend/src/Squidex.Infrastructure/Commands/Is.cs +++ b/backend/src/Squidex.Infrastructure/Commands/Is.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -29,7 +30,7 @@ namespace Squidex.Infrastructure.Commands public static bool OptionalChange(string oldValue, [NotNullWhen(true)] string? newValue) { - return !string.IsNullOrWhiteSpace(newValue) && !string.Equals(oldValue, newValue); + return !string.IsNullOrWhiteSpace(newValue) && !string.Equals(oldValue, newValue, StringComparison.Ordinal); } public static bool OptionalSetChange(ISet oldValue, [NotNullWhen(true)] ISet? newValue) diff --git a/backend/src/Squidex.Infrastructure/Commands/Rebuilder.cs b/backend/src/Squidex.Infrastructure/Commands/Rebuilder.cs index 9b4fbd01e..8dd377ca7 100644 --- a/backend/src/Squidex.Infrastructure/Commands/Rebuilder.cs +++ b/backend/src/Squidex.Infrastructure/Commands/Rebuilder.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Threading.Tasks.Dataflow; @@ -50,19 +51,11 @@ namespace Squidex.Infrastructure.Commands CancellationToken ct = default) where T : DomainObject where TState : class, IDomainState, new() { - var store = serviceProvider.GetRequiredService>(); - - await store.ClearSnapshotsAsync(); + await ClearAsync(); - await InsertManyAsync(store, async target => - { - await foreach (var storedEvent in eventStore.QueryAllAsync(filter, ct: ct)) - { - var id = storedEvent.Data.Headers.AggregateId(); + var ids = eventStore.QueryAllAsync(filter, ct: ct).Select(x => x.Data.Headers.AggregateId()); - await target(id); - } - }, batchSize, ct); + await InsertManyAsync(ids, batchSize, ct); } public virtual async Task InsertManyAsync(IEnumerable source, int batchSize, @@ -70,23 +63,18 @@ namespace Squidex.Infrastructure.Commands where T : DomainObject where TState : class, IDomainState, new() { Guard.NotNull(source, nameof(source)); - Guard.Between(batchSize, 1, 1000, nameof(batchSize)); - var store = serviceProvider.GetRequiredService>(); + var ids = source.ToAsyncEnumerable(); - await InsertManyAsync(store, async target => - { - foreach (var id in source) - { - await target(id); - } - }, batchSize, ct); + await InsertManyAsync(ids, batchSize, ct); } - private async Task InsertManyAsync(IStore store, Func, Task> source, int batchSize, + private async Task InsertManyAsync(IAsyncEnumerable source, int batchSize, CancellationToken ct = default) where T : DomainObject where TState : class, IDomainState, new() { + var store = serviceProvider.GetRequiredService>(); + var parallelism = Environment.ProcessorCount; var workerBlock = new ActionBlock(async ids => @@ -138,20 +126,28 @@ namespace Squidex.Infrastructure.Commands using (localCache.StartContext()) { - await source(id => + await foreach (var id in source.WithCancellation(ct)) { if (handledIds.Add(id)) { - return batchBlock.SendAsync(id, ct); + if (!await batchBlock.SendAsync(id, ct)) + { + break; + } } - - return Task.CompletedTask; - }); + } batchBlock.Complete(); - - await workerBlock.Completion; } + + await workerBlock.Completion; + } + + private async Task ClearAsync() where TState : class, IDomainState, new() + { + var store = serviceProvider.GetRequiredService>(); + + await store.ClearSnapshotsAsync(); } } } diff --git a/backend/src/Squidex.Infrastructure/Commands/SnapshotList.cs b/backend/src/Squidex.Infrastructure/Commands/SnapshotList.cs index 6cbbe73ca..ab770273b 100644 --- a/backend/src/Squidex.Infrastructure/Commands/SnapshotList.cs +++ b/backend/src/Squidex.Infrastructure/Commands/SnapshotList.cs @@ -39,7 +39,7 @@ namespace Squidex.Infrastructure.Commands public T Current { - get => items.Last()!; + get => items[^1]!; } public SnapshotList() diff --git a/backend/src/Squidex.Infrastructure/Diagnostics/GCHealthCheck.cs b/backend/src/Squidex.Infrastructure/Diagnostics/GCHealthCheck.cs index 234784986..0e314b815 100644 --- a/backend/src/Squidex.Infrastructure/Diagnostics/GCHealthCheck.cs +++ b/backend/src/Squidex.Infrastructure/Diagnostics/GCHealthCheck.cs @@ -23,7 +23,8 @@ namespace Squidex.Infrastructure.Diagnostics threshold = 1024 * 1024 * options.Value.Threshold; } - public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + public Task CheckHealthAsync(HealthCheckContext context, + CancellationToken cancellationToken = default) { var allocated = GC.GetTotalMemory(false); diff --git a/backend/src/Squidex.Infrastructure/Diagnostics/OrleansHealthCheck.cs b/backend/src/Squidex.Infrastructure/Diagnostics/OrleansHealthCheck.cs index 698574412..670d00bc0 100644 --- a/backend/src/Squidex.Infrastructure/Diagnostics/OrleansHealthCheck.cs +++ b/backend/src/Squidex.Infrastructure/Diagnostics/OrleansHealthCheck.cs @@ -22,7 +22,8 @@ namespace Squidex.Infrastructure.Diagnostics managementGrain = grainFactory.GetGrain(0); } - public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + public async Task CheckHealthAsync(HealthCheckContext context, + CancellationToken cancellationToken = default) { var activationCount = await managementGrain.GetTotalActivationCount(); diff --git a/backend/src/Squidex.Infrastructure/DomainId.cs b/backend/src/Squidex.Infrastructure/DomainId.cs index ca571a2cf..337d186c9 100644 --- a/backend/src/Squidex.Infrastructure/DomainId.cs +++ b/backend/src/Squidex.Infrastructure/DomainId.cs @@ -61,12 +61,12 @@ namespace Squidex.Infrastructure public bool Equals(DomainId other) { - return string.Equals(ToString(), other.ToString()); + return string.Equals(ToString(), other.ToString(), StringComparison.Ordinal); } public override int GetHashCode() { - return ToString().GetHashCode(); + return ToString().GetHashCode(StringComparison.Ordinal); } public override string ToString() diff --git a/backend/src/Squidex.Infrastructure/Email/IEmailSender.cs b/backend/src/Squidex.Infrastructure/Email/IEmailSender.cs index 07c30766d..cd282f612 100644 --- a/backend/src/Squidex.Infrastructure/Email/IEmailSender.cs +++ b/backend/src/Squidex.Infrastructure/Email/IEmailSender.cs @@ -12,6 +12,7 @@ namespace Squidex.Infrastructure.Email { public interface IEmailSender { - Task SendAsync(string recipient, string subject, string body, CancellationToken ct = default); + Task SendAsync(string recipient, string subject, string body, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Infrastructure/Email/SmtpEmailSender.cs b/backend/src/Squidex.Infrastructure/Email/SmtpEmailSender.cs index bf1dd89c8..eeba45556 100644 --- a/backend/src/Squidex.Infrastructure/Email/SmtpEmailSender.cs +++ b/backend/src/Squidex.Infrastructure/Email/SmtpEmailSender.cs @@ -29,7 +29,8 @@ namespace Squidex.Infrastructure.Email clientPool = new DefaultObjectPoolProvider().Create(new DefaultPooledObjectPolicy()); } - public async Task SendAsync(string recipient, string subject, string body, CancellationToken ct = default) + public async Task SendAsync(string recipient, string subject, string body, + CancellationToken ct = default) { var smtpClient = clientPool.Get(); try @@ -65,7 +66,8 @@ namespace Squidex.Infrastructure.Email } } - private async Task EnsureConnectedAsync(SmtpClient smtpClient, CancellationToken ct) + private async Task EnsureConnectedAsync(SmtpClient smtpClient, + CancellationToken ct) { if (!smtpClient.IsConnected) { diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/EventConsumersHealthCheck.cs b/backend/src/Squidex.Infrastructure/EventSourcing/EventConsumersHealthCheck.cs index 019ee25e8..7a0a4a012 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/EventConsumersHealthCheck.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/EventConsumersHealthCheck.cs @@ -24,7 +24,8 @@ namespace Squidex.Infrastructure.EventSourcing this.grainFactory = grainFactory; } - public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + public async Task CheckHealthAsync(HealthCheckContext context, + CancellationToken cancellationToken = default) { var eventConsumers = await GetGrain().GetConsumersAsync(); diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Grains/BatchSubscriber.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Grains/BatchSubscriber.cs index 7633dfac9..c06ea0c7d 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/Grains/BatchSubscriber.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/Grains/BatchSubscriber.cs @@ -66,6 +66,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains SingleWriter = true }); +#pragma warning disable MA0040 // Flow the cancellation token batchQueue.Batch(taskQueue, x => new BatchJob(x.ToArray()), batchSize, batchDelay); Task.Run(async () => @@ -74,8 +75,6 @@ namespace Squidex.Infrastructure.EventSourcing.Grains { try { - var shouldHandle = eventConsumer.Handles(storedEvent); - Envelope? @event = null; if (eventConsumer.Handles(storedEvent)) @@ -91,49 +90,57 @@ namespace Squidex.Infrastructure.EventSourcing.Grains } } }).ContinueWith(x => batchQueue.Writer.TryComplete(x.Exception)); +#pragma warning restore MA0040 // Flow the cancellation token handleTask = Run(grain); } private async Task Run(EventConsumerGrain grain) { - await foreach (var task in taskQueue.Reader.ReadAllAsync()) + try { - var sender = eventSubscription?.Sender; - - if (sender == null) + await foreach (var task in taskQueue.Reader.ReadAllAsync(completed.Token)) { - continue; - } + var sender = eventSubscription?.Sender; - switch (task) - { - case ErrorJob error when error.Exception is not OperationCanceledException: - { - if (ReferenceEquals(error.Sender, sender)) + if (sender == null) + { + continue; + } + + switch (task) + { + case ErrorJob error when error.Exception is not OperationCanceledException: { - await grain.OnErrorAsync(sender, error.Exception); - } + if (ReferenceEquals(error.Sender, sender)) + { + await grain.OnErrorAsync(sender, error.Exception); + } - break; - } + break; + } - case BatchJob batch: - { - foreach (var itemsBySender in batch.Items.GroupBy(x => x.Sender)) + case BatchJob batch: { - if (ReferenceEquals(itemsBySender.Key, sender)) + foreach (var itemsBySender in batch.Items.GroupBy(x => x.Sender)) { - var position = itemsBySender.Last().Position; + if (ReferenceEquals(itemsBySender.Key, sender)) + { + var position = itemsBySender.Last().Position; - await grain.OnEventsAsync(sender, itemsBySender.Select(x => x.Event).NotNull().ToList(), position); + await grain.OnEventsAsync(sender, itemsBySender.Select(x => x.Event).NotNull().ToList(), position); + } } - } - break; - } + break; + } + } } } + catch (OperationCanceledException) + { + return; + } } public Task CompleteAsync() diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs b/backend/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs index 4524e12ba..ae988501b 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/IEventStore.cs @@ -15,37 +15,49 @@ namespace Squidex.Infrastructure.EventSourcing { public interface IEventStore { - Task> QueryLatestAsync(string streamName, int take = int.MaxValue); + Task> QueryLatestAsync(string streamName, int take = int.MaxValue, + CancellationToken ct = default); - Task> QueryAsync(string streamName, long streamPosition = 0); + Task> QueryAsync(string streamName, long streamPosition = 0, + CancellationToken ct = default); - IAsyncEnumerable QueryAllReverseAsync(string? streamFilter = null, Instant timestamp = default, int take = int.MaxValue, CancellationToken ct = default); + IAsyncEnumerable QueryAllReverseAsync(string? streamFilter = null, Instant timestamp = default, int take = int.MaxValue, + CancellationToken ct = default); - IAsyncEnumerable QueryAllAsync(string? streamFilter = null, string? position = null, int take = int.MaxValue, CancellationToken ct = default); + IAsyncEnumerable QueryAllAsync(string? streamFilter = null, string? position = null, int take = int.MaxValue, + CancellationToken ct = default); - Task AppendAsync(Guid commitId, string streamName, ICollection events); + Task AppendAsync(Guid commitId, string streamName, ICollection events, + CancellationToken ct = default); - Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection events); + Task AppendAsync(Guid commitId, string streamName, long expectedVersion, ICollection events, + CancellationToken ct = default); - Task DeleteStreamAsync(string streamName); + Task DeleteAsync(string streamFilter, + CancellationToken ct = default); + + Task DeleteStreamAsync(string streamName, + CancellationToken ct = default); IEventSubscription CreateSubscription(IEventSubscriber subscriber, string? streamFilter = null, string? position = null); - async Task AppendUnsafeAsync(IEnumerable commits) + async Task AppendUnsafeAsync(IEnumerable commits, + CancellationToken ct = default) { foreach (var commit in commits) { - await AppendAsync(commit.Id, commit.StreamName, commit.Offset, commit.Events); + await AppendAsync(commit.Id, commit.StreamName, commit.Offset, commit.Events, ct); } } - async Task>> QueryManyAsync(IEnumerable streamNames) + async Task>> QueryManyAsync(IEnumerable streamNames, + CancellationToken ct = default) { var result = new Dictionary>(); foreach (var streamName in streamNames) { - result[streamName] = await QueryAsync(streamName, 0); + result[streamName] = await QueryAsync(streamName, 0, ct); } return result; diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs b/backend/src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs index 64d12dc93..2f6f2be4a 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs @@ -16,6 +16,7 @@ namespace Squidex.Infrastructure.EventSourcing private readonly RetryWindow retryWindow = new RetryWindow(TimeSpan.FromMinutes(5), 5); private readonly IEventSubscriber eventSubscriber; private readonly Func eventSubscriptionFactory; + private readonly object lockObject = new object(); private CancellationTokenSource timerCancellation = new CancellationTokenSource(); private IEventSubscription? currentSubscription; @@ -38,7 +39,7 @@ namespace Squidex.Infrastructure.EventSourcing { if (currentSubscription == null) { - lock (this) + lock (lockObject) { if (currentSubscription == null) { @@ -52,7 +53,7 @@ namespace Squidex.Infrastructure.EventSourcing { if (currentSubscription != null) { - lock (this) + lock (lockObject) { if (currentSubscription != null) { diff --git a/backend/src/Squidex.Infrastructure/GravatarHelper.cs b/backend/src/Squidex.Infrastructure/GravatarHelper.cs index 3f122b4ee..eba120a67 100644 --- a/backend/src/Squidex.Infrastructure/GravatarHelper.cs +++ b/backend/src/Squidex.Infrastructure/GravatarHelper.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Globalization; using System.Security.Cryptography; using System.Text; @@ -37,7 +38,7 @@ namespace Squidex.Infrastructure for (var i = 0; i < hashBytes.Length; i++) { - hashBuilder.Append(hashBytes[i].ToString("x2")); + hashBuilder.Append(hashBytes[i].ToString("x2", CultureInfo.InvariantCulture)); } return hashBuilder.ToString(); diff --git a/backend/src/Squidex.Infrastructure/Json/Newtonsoft/NewtonsoftJsonSerializer.cs b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/NewtonsoftJsonSerializer.cs index 165ceca56..94eac432b 100644 --- a/backend/src/Squidex.Infrastructure/Json/Newtonsoft/NewtonsoftJsonSerializer.cs +++ b/backend/src/Squidex.Infrastructure/Json/Newtonsoft/NewtonsoftJsonSerializer.cs @@ -26,7 +26,7 @@ namespace Squidex.Infrastructure.Json.Newtonsoft serializer = JsonSerializer.Create(settings); } - public string Serialize(T value, bool intented) + public string Serialize(T value, bool intented = false) { var formatting = intented ? Formatting.Indented : Formatting.None; diff --git a/backend/src/Squidex.Infrastructure/Json/Objects/JsonValue.cs b/backend/src/Squidex.Infrastructure/Json/Objects/JsonValue.cs index 581b18bf2..ba1bc9dc0 100644 --- a/backend/src/Squidex.Infrastructure/Json/Objects/JsonValue.cs +++ b/backend/src/Squidex.Infrastructure/Json/Objects/JsonValue.cs @@ -97,7 +97,7 @@ namespace Squidex.Infrastructure.Json.Objects return Object(obj); } - throw new ArgumentException("Invalid json type"); + throw new ArgumentException("Invalid json type", nameof(value)); } public static IJsonValue Create(Guid value) diff --git a/backend/src/Squidex.Infrastructure/Language.cs b/backend/src/Squidex.Infrastructure/Language.cs index 054492ec5..7b1d290c2 100644 --- a/backend/src/Squidex.Infrastructure/Language.cs +++ b/backend/src/Squidex.Infrastructure/Language.cs @@ -16,7 +16,9 @@ namespace Squidex.Infrastructure [TypeConverter(typeof(LanguageTypeConverter))] public partial record Language { +#pragma warning disable MA0023 // Add RegexOptions.ExplicitCapture private static readonly Regex CultureRegex = new Regex("^([a-z]{2})(\\-[a-z]{2})?$", RegexOptions.IgnoreCase); +#pragma warning restore MA0023 // Add RegexOptions.ExplicitCapture public static Language GetLanguage(string iso2Code) { @@ -107,4 +109,4 @@ namespace Squidex.Infrastructure return EnglishName; } } -} \ No newline at end of file +} diff --git a/backend/src/Squidex.Infrastructure/LanguagesInitializer.cs b/backend/src/Squidex.Infrastructure/LanguagesInitializer.cs index 3f8669af8..f97284491 100644 --- a/backend/src/Squidex.Infrastructure/LanguagesInitializer.cs +++ b/backend/src/Squidex.Infrastructure/LanguagesInitializer.cs @@ -23,7 +23,8 @@ namespace Squidex.Infrastructure this.options = options.Value; } - public Task InitializeAsync(CancellationToken ct = default) + public Task InitializeAsync( + CancellationToken ct) { foreach (var (key, value) in options) { diff --git a/backend/src/Squidex.Infrastructure/Log/BackgroundRequestLogStore.cs b/backend/src/Squidex.Infrastructure/Log/BackgroundRequestLogStore.cs index d6ed93ddc..6e5452f94 100644 --- a/backend/src/Squidex.Infrastructure/Log/BackgroundRequestLogStore.cs +++ b/backend/src/Squidex.Infrastructure/Log/BackgroundRequestLogStore.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -24,10 +25,9 @@ namespace Squidex.Infrastructure.Log private readonly RequestLogStoreOptions options; private ConcurrentQueue jobs = new ConcurrentQueue(); - public bool IsEnabled - { - get => options.StoreEnabled; - } + public bool ForceWrite { get; set; } + + public bool IsEnabled => options.StoreEnabled; public BackgroundRequestLogStore(IOptions options, IRequestLogRepository logRepository, ISemanticLog log) @@ -35,9 +35,10 @@ namespace Squidex.Infrastructure.Log this.options = options.Value; this.logRepository = logRepository; - this.log = log; - timer = new CompletionTimer(options.Value.WriteIntervall, ct => TrackAsync(), options.Value.WriteIntervall); + timer = new CompletionTimer(options.Value.WriteIntervall, TrackAsync, options.Value.WriteIntervall); + + this.log = log; } protected override void DisposeObject(bool disposing) @@ -55,7 +56,8 @@ namespace Squidex.Infrastructure.Log timer.SkipCurrentDelay(); } - private async Task TrackAsync() + private async Task TrackAsync( + CancellationToken ct) { if (!IsEnabled) { @@ -74,7 +76,14 @@ namespace Squidex.Infrastructure.Log for (var i = 0; i < pages; i++) { - await logRepository.InsertManyAsync(localJobs.Skip(i * batchSize).Take(batchSize)); + var batch = localJobs.Skip(i * batchSize).Take(batchSize); + + if (ForceWrite) + { + ct = default; + } + + await logRepository.InsertManyAsync(batch, ct); } } } @@ -86,15 +95,33 @@ namespace Squidex.Infrastructure.Log } } - public Task QueryAllAsync(Func callback, string key, DateTime fromDate, DateTime toDate, CancellationToken ct = default) + public Task DeleteAsync(string key, + CancellationToken ct = default) { - return logRepository.QueryAllAsync(callback, key, fromDate, toDate, ct); + return logRepository.DeleteAsync(key, ct); } - public Task LogAsync(Request request) + public IAsyncEnumerable QueryAllAsync(string key, DateTime fromDate, DateTime toDate, + CancellationToken ct = default) + { + if (!IsEnabled) + { + return AsyncEnumerable.Empty(); + } + + return logRepository.QueryAllAsync(key, fromDate, toDate, ct); + } + + public Task LogAsync(Request request, + CancellationToken ct = default) { Guard.NotNull(request, nameof(request)); + if (!IsEnabled) + { + return Task.CompletedTask; + } + jobs.Enqueue(request); return Task.CompletedTask; diff --git a/backend/src/Squidex.Infrastructure/Log/IRequestLogRepository.cs b/backend/src/Squidex.Infrastructure/Log/IRequestLogRepository.cs index 48ca3bd6d..4fd44f53f 100644 --- a/backend/src/Squidex.Infrastructure/Log/IRequestLogRepository.cs +++ b/backend/src/Squidex.Infrastructure/Log/IRequestLogRepository.cs @@ -14,8 +14,13 @@ namespace Squidex.Infrastructure.Log { public interface IRequestLogRepository { - Task InsertManyAsync(IEnumerable items); + Task InsertManyAsync(IEnumerable items, + CancellationToken ct = default); - Task QueryAllAsync(Func callback, string key, DateTime fromDate, DateTime toDate, CancellationToken ct = default); + Task DeleteAsync(string key, + CancellationToken ct = default); + + IAsyncEnumerable QueryAllAsync(string key, DateTime fromDate, DateTime toDate, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Infrastructure/Log/IRequestLogStore.cs b/backend/src/Squidex.Infrastructure/Log/IRequestLogStore.cs index 21db736af..00310db98 100644 --- a/backend/src/Squidex.Infrastructure/Log/IRequestLogStore.cs +++ b/backend/src/Squidex.Infrastructure/Log/IRequestLogStore.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -15,8 +16,13 @@ namespace Squidex.Infrastructure.Log { bool IsEnabled { get; } - Task LogAsync(Request request); + Task LogAsync(Request request, + CancellationToken ct = default); - Task QueryAllAsync(Func callback, string key, DateTime fromDate, DateTime toDate, CancellationToken ct = default); + Task DeleteAsync(string key, + CancellationToken ct = default); + + IAsyncEnumerable QueryAllAsync(string key, DateTime fromDate, DateTime toDate, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Infrastructure/Migrations/IMigration.cs b/backend/src/Squidex.Infrastructure/Migrations/IMigration.cs index 760a14a2e..754cd8558 100644 --- a/backend/src/Squidex.Infrastructure/Migrations/IMigration.cs +++ b/backend/src/Squidex.Infrastructure/Migrations/IMigration.cs @@ -12,6 +12,7 @@ namespace Squidex.Infrastructure.Migrations { public interface IMigration { - Task UpdateAsync(CancellationToken ct); + Task UpdateAsync( + CancellationToken ct); } } diff --git a/backend/src/Squidex.Infrastructure/Migrations/IMigrationStatus.cs b/backend/src/Squidex.Infrastructure/Migrations/IMigrationStatus.cs index bde1fe1f1..e004f23c3 100644 --- a/backend/src/Squidex.Infrastructure/Migrations/IMigrationStatus.cs +++ b/backend/src/Squidex.Infrastructure/Migrations/IMigrationStatus.cs @@ -5,18 +5,23 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Threading; using System.Threading.Tasks; namespace Squidex.Infrastructure.Migrations { public interface IMigrationStatus { - Task GetVersionAsync(); + Task GetVersionAsync( + CancellationToken ct = default); - Task TryLockAsync(); + Task TryLockAsync( + CancellationToken ct = default); - Task CompleteAsync(int newVersion); + Task CompleteAsync(int newVersion, + CancellationToken ct = default); - Task UnlockAsync(); + Task UnlockAsync( + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Infrastructure/Migrations/Migrator.cs b/backend/src/Squidex.Infrastructure/Migrations/Migrator.cs index 98189076a..3c2e80f67 100644 --- a/backend/src/Squidex.Infrastructure/Migrations/Migrator.cs +++ b/backend/src/Squidex.Infrastructure/Migrations/Migrator.cs @@ -29,11 +29,12 @@ namespace Squidex.Infrastructure.Migrations this.log = log; } - public async Task MigrateAsync(CancellationToken ct = default) + public async Task MigrateAsync( + CancellationToken ct = default) { try { - while (!await migrationStatus.TryLockAsync()) + while (!await migrationStatus.TryLockAsync(ct)) { log.LogInformation(w => w .WriteProperty("action", "Migrate") @@ -42,7 +43,7 @@ namespace Squidex.Infrastructure.Migrations await Task.Delay(LockWaitMs, ct); } - var version = await migrationStatus.GetVersionAsync(); + var version = await migrationStatus.GetVersionAsync(ct); while (!ct.IsCancellationRequested) { @@ -85,12 +86,16 @@ namespace Squidex.Infrastructure.Migrations version = newVersion; - await migrationStatus.CompleteAsync(newVersion); + await migrationStatus.CompleteAsync(newVersion, ct); } } finally { +#pragma warning disable CA2016 // Forward the 'CancellationToken' parameter to methods that take one +#pragma warning disable MA0040 // Flow the cancellation token await migrationStatus.UnlockAsync(); +#pragma warning restore MA0040 // Flow the cancellation token +#pragma warning restore CA2016 // Forward the 'CancellationToken' parameter to methods that take one } } } diff --git a/backend/src/Squidex.Infrastructure/NamedId{T}.cs b/backend/src/Squidex.Infrastructure/NamedId{T}.cs index 0c7e52108..66062a641 100644 --- a/backend/src/Squidex.Infrastructure/NamedId{T}.cs +++ b/backend/src/Squidex.Infrastructure/NamedId{T}.cs @@ -57,7 +57,7 @@ namespace Squidex.Infrastructure } else { - var index = value.IndexOf(','); + var index = value.IndexOf(',', StringComparison.Ordinal); if (index > 0 && index < value.Length - 1) { diff --git a/backend/src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs b/backend/src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs index 1ef4f7d75..f4d9264cd 100644 --- a/backend/src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs +++ b/backend/src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs @@ -27,7 +27,8 @@ namespace Squidex.Infrastructure.Orleans this.grainFactory = grainFactory; } - public async Task StartAsync(CancellationToken ct = default) + public async Task StartAsync( + CancellationToken ct) { for (var i = 1; i <= NumTries; i++) { diff --git a/backend/src/Squidex.Infrastructure/Orleans/GrainState.cs b/backend/src/Squidex.Infrastructure/Orleans/GrainState.cs index 5808c7305..04ecf552b 100644 --- a/backend/src/Squidex.Infrastructure/Orleans/GrainState.cs +++ b/backend/src/Squidex.Infrastructure/Orleans/GrainState.cs @@ -37,7 +37,8 @@ namespace Squidex.Infrastructure.Orleans context.ObservableLifecycle.Subscribe("Persistence", GrainLifecycleStage.SetupState, SetupAsync); } - public Task SetupAsync(CancellationToken ct = default) + public Task SetupAsync( + CancellationToken ct = default) { if (ct.IsCancellationRequested) { diff --git a/backend/src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexState.cs b/backend/src/Squidex.Infrastructure/Orleans/Indexes/IUniqueNameGrain.cs similarity index 66% rename from backend/src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexState.cs rename to backend/src/Squidex.Infrastructure/Orleans/Indexes/IUniqueNameGrain.cs index d6e4a150d..148342f13 100644 --- a/backend/src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexState.cs +++ b/backend/src/Squidex.Infrastructure/Orleans/Indexes/IUniqueNameGrain.cs @@ -5,12 +5,15 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Collections.Generic; +using System.Threading.Tasks; +using Orleans; namespace Squidex.Infrastructure.Orleans.Indexes { - public class IdsIndexState + public interface IUniqueNameGrain : IGrainWithStringKey { - public HashSet Ids { get; set; } = new HashSet(); + Task ReserveAsync(T id, string name); + + Task RemoveReservationAsync(string? token); } } diff --git a/backend/src/Squidex.Infrastructure/Orleans/Indexes/IUniqueNameIndexGrain.cs b/backend/src/Squidex.Infrastructure/Orleans/Indexes/IUniqueNameIndexGrain.cs deleted file mode 100644 index 966d16cae..000000000 --- a/backend/src/Squidex.Infrastructure/Orleans/Indexes/IUniqueNameIndexGrain.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Squidex.Infrastructure.Orleans.Indexes -{ - public interface IUniqueNameIndexGrain - { - Task ReserveAsync(T id, string name); - - Task AddAsync(string? token); - - Task CountAsync(); - - Task RemoveReservationAsync(string? token); - - Task RemoveAsync(T id); - - Task RebuildAsync(Dictionary values); - - Task ClearAsync(); - - Task GetIdAsync(string name); - - Task> GetIdsAsync(string[] names); - - Task> GetIdsAsync(); - } -} diff --git a/backend/src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexGrain.cs b/backend/src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexGrain.cs deleted file mode 100644 index c062e7e2c..000000000 --- a/backend/src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexGrain.cs +++ /dev/null @@ -1,60 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Orleans; - -namespace Squidex.Infrastructure.Orleans.Indexes -{ - public class IdsIndexGrain : Grain, IIdsIndexGrain where TState : IdsIndexState, new() - { - private readonly IGrainState state; - - public IdsIndexGrain(IGrainState state) - { - this.state = state; - } - - public Task CountAsync() - { - return Task.FromResult(state.Value.Ids.Count); - } - - public Task RebuildAsync(HashSet ids) - { - state.Value = new TState { Ids = ids }; - - return state.WriteAsync(); - } - - public Task AddAsync(T id) - { - state.Value.Ids.Add(id); - - return state.WriteAsync(); - } - - public Task RemoveAsync(T id) - { - state.Value.Ids.Remove(id); - - return state.WriteAsync(); - } - - public Task ClearAsync() - { - return state.ClearAsync(); - } - - public Task> GetIdsAsync() - { - return Task.FromResult(state.Value.Ids.ToList()); - } - } -} diff --git a/backend/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameGrain.cs b/backend/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameGrain.cs new file mode 100644 index 000000000..e0afbcb43 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameGrain.cs @@ -0,0 +1,45 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.Orleans.Indexes +{ + public class UniqueNameGrain : GrainOfString, IUniqueNameGrain + { + private readonly Dictionary reservations = new Dictionary(); + + public 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/Indexes/UniqueNameIndexGrain.cs b/backend/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexGrain.cs deleted file mode 100644 index 3ae13f609..000000000 --- a/backend/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexGrain.cs +++ /dev/null @@ -1,135 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Orleans; - -namespace Squidex.Infrastructure.Orleans.Indexes -{ - public class UniqueNameIndexGrain : Grain, IUniqueNameIndexGrain where TState : UniqueNameIndexState, new() - { - private readonly Dictionary reservations = new Dictionary(); - private readonly IGrainState state; - - public UniqueNameIndexGrain(IGrainState state) - { - this.state = state; - } - - public Task CountAsync() - { - return Task.FromResult(state.Value.Names.Count); - } - - public Task ClearAsync() - { - reservations.Clear(); - - return state.ClearAsync(); - } - - public Task RebuildAsync(Dictionary names) - { - state.Value = new TState { Names = names }; - - return state.WriteAsync(); - } - - public Task ReserveAsync(T id, string name) - { - string? token = null; - - if (!IsInUse(name) && !IsReserved(name)) - { - token = RandomHash.Simple(); - - reservations.Add(token, (name, id)); - } - - return Task.FromResult(token); - } - - public async Task AddAsync(string? token) - { - token ??= string.Empty; - - if (reservations.TryGetValue(token, out var reservation)) - { - state.Value.Names.Add(reservation.Name, reservation.Id); - - await state.WriteAsync(); - - reservations.Remove(token); - - return true; - } - - return false; - } - - public Task RemoveReservationAsync(string? token) - { - reservations.Remove(token ?? string.Empty); - - return Task.CompletedTask; - } - - public async Task RemoveAsync(T id) - { - var name = state.Value.Names.FirstOrDefault(x => Equals(x.Value, id)).Key; - - if (name != null) - { - state.Value.Names.Remove(name); - - await state.WriteAsync(); - } - } - - public Task> GetIdsAsync(string[] names) - { - var result = new List(); - - if (names != null) - { - foreach (var name in names) - { - if (state.Value.Names.TryGetValue(name, out var id)) - { - result.Add(id); - } - } - } - - return Task.FromResult(result); - } - - public Task GetIdAsync(string name) - { - state.Value.Names.TryGetValue(name, out var id); - - return Task.FromResult(id!); - } - - public Task> GetIdsAsync() - { - return Task.FromResult(state.Value.Names.Values.ToList()); - } - - private bool IsInUse(string name) - { - return state.Value.Names.ContainsKey(name); - } - - private bool IsReserved(string name) - { - return reservations.Values.Any(x => x.Name == name); - } - } -} diff --git a/backend/src/Squidex.Infrastructure/Queries/ClrValue.cs b/backend/src/Squidex.Infrastructure/Queries/ClrValue.cs index ca5aa8def..6ceddd45e 100644 --- a/backend/src/Squidex.Infrastructure/Queries/ClrValue.cs +++ b/backend/src/Squidex.Infrastructure/Queries/ClrValue.cs @@ -131,7 +131,7 @@ namespace Squidex.Infrastructure.Queries if (value is string s) { - return $"'{s.Replace("'", "\\'")}'"; + return $"'{s.Replace("'", "\\'", StringComparison.Ordinal)}'"; } return string.Format(CultureInfo.InvariantCulture, "{0}", value); diff --git a/backend/src/Squidex.Infrastructure/Queries/FilterNodeVisitor.cs b/backend/src/Squidex.Infrastructure/Queries/FilterNodeVisitor.cs index 0cf3d2b4d..b5b5a68cf 100644 --- a/backend/src/Squidex.Infrastructure/Queries/FilterNodeVisitor.cs +++ b/backend/src/Squidex.Infrastructure/Queries/FilterNodeVisitor.cs @@ -28,4 +28,4 @@ namespace Squidex.Infrastructure.Queries throw new NotImplementedException(); } } -} \ No newline at end of file +} diff --git a/backend/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs b/backend/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs index 89b4b8eef..969be15fc 100644 --- a/backend/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs +++ b/backend/src/Squidex.Infrastructure/Queries/OData/EdmModelExtensions.cs @@ -49,9 +49,9 @@ namespace Squidex.Infrastructure.Queries.OData query ??= string.Empty; - var path = model.EntityContainer.EntitySets().First().Path.Path.Split('.').Last(); + var path = model.EntityContainer.EntitySets().First().Path.Path.Split('.')[^1]; - if (query.StartsWith("?", StringComparison.Ordinal)) + if (query.StartsWith('?')) { query = query[1..]; } diff --git a/backend/src/Squidex.Infrastructure/Queries/Query.cs b/backend/src/Squidex.Infrastructure/Queries/Query.cs index 990473253..a759f2c2e 100644 --- a/backend/src/Squidex.Infrastructure/Queries/Query.cs +++ b/backend/src/Squidex.Infrastructure/Queries/Query.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; namespace Squidex.Infrastructure.Queries @@ -54,7 +55,7 @@ namespace Squidex.Infrastructure.Queries if (FullText != null) { - parts.Add($"FullText: '{FullText.Replace("'", "\'")}'"); + parts.Add($"FullText: '{FullText.Replace("'", "\'", StringComparison.Ordinal)}'"); } if (Skip > 0) diff --git a/backend/src/Squidex.Infrastructure/RandomHash.cs b/backend/src/Squidex.Infrastructure/RandomHash.cs index 31789d0a7..1d5dd2a63 100644 --- a/backend/src/Squidex.Infrastructure/RandomHash.cs +++ b/backend/src/Squidex.Infrastructure/RandomHash.cs @@ -18,14 +18,14 @@ namespace Squidex.Infrastructure return Guid.NewGuid() .ToString().ToSha256Base64() .ToLowerInvariant() - .Replace("+", "x") - .Replace("=", "x") - .Replace("/", "x"); + .Replace("+", "x", StringComparison.Ordinal) + .Replace("=", "x", StringComparison.Ordinal) + .Replace("/", "x", StringComparison.Ordinal); } public static string Simple() { - return Guid.NewGuid().ToString().Replace("-", string.Empty); + return Guid.NewGuid().ToString().Replace("-", string.Empty, StringComparison.Ordinal); } public static string ToSha256Base64(this string value) diff --git a/backend/src/Squidex.Infrastructure/RefToken.cs b/backend/src/Squidex.Infrastructure/RefToken.cs index 45325a1d2..d9bfe137b 100644 --- a/backend/src/Squidex.Infrastructure/RefToken.cs +++ b/backend/src/Squidex.Infrastructure/RefToken.cs @@ -66,7 +66,7 @@ namespace Squidex.Infrastructure value = value.Trim(); - var idx = value.IndexOf(':'); + var idx = value.IndexOf(':', StringComparison.Ordinal); if (idx > 0 && idx < value.Length - 1) { diff --git a/backend/src/Squidex.Infrastructure/Reflection/SimpleMapper.cs b/backend/src/Squidex.Infrastructure/Reflection/SimpleMapper.cs index 644db9475..554054f23 100644 --- a/backend/src/Squidex.Infrastructure/Reflection/SimpleMapper.cs +++ b/backend/src/Squidex.Infrastructure/Reflection/SimpleMapper.cs @@ -116,7 +116,7 @@ namespace Squidex.Infrastructure.Reflection foreach (var sourceProperty in sourceProperties) { - var targetProperty = targetProperties.FirstOrDefault(x => x.Name == sourceProperty.Name); + var targetProperty = targetProperties.Find(x => x.Name == sourceProperty.Name); if (targetProperty == null) { diff --git a/backend/src/Squidex.Infrastructure/Security/Permission.cs b/backend/src/Squidex.Infrastructure/Security/Permission.cs index de7744086..dda5b32f6 100644 --- a/backend/src/Squidex.Infrastructure/Security/Permission.cs +++ b/backend/src/Squidex.Infrastructure/Security/Permission.cs @@ -93,12 +93,12 @@ namespace Squidex.Infrastructure.Security public bool Equals(Permission? other) { - return other != null && other.Id.Equals(Id); + return other != null && other.Id.Equals(Id, StringComparison.Ordinal); } public override int GetHashCode() { - return Id.GetHashCode(); + return Id.GetHashCode(StringComparison.Ordinal); } public override string ToString() diff --git a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj index 580bfd80f..c4e3866a3 100644 --- a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj +++ b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj @@ -12,6 +12,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -26,9 +30,9 @@ - + - + diff --git a/backend/src/Squidex.Infrastructure/States/BatchPersistence.cs b/backend/src/Squidex.Infrastructure/States/BatchPersistence.cs index d599bc56c..23e98ec22 100644 --- a/backend/src/Squidex.Infrastructure/States/BatchPersistence.cs +++ b/backend/src/Squidex.Infrastructure/States/BatchPersistence.cs @@ -12,7 +12,7 @@ using Squidex.Infrastructure.EventSourcing; namespace Squidex.Infrastructure.States { - internal class BatchPersistence : IPersistence + internal sealed class BatchPersistence : IPersistence { private readonly DomainId ownerKey; private readonly BatchContext context; diff --git a/backend/src/Squidex.Infrastructure/States/ISnapshotStore.cs b/backend/src/Squidex.Infrastructure/States/ISnapshotStore.cs index a8a51d4ed..68f404698 100644 --- a/backend/src/Squidex.Infrastructure/States/ISnapshotStore.cs +++ b/backend/src/Squidex.Infrastructure/States/ISnapshotStore.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -14,16 +13,22 @@ namespace Squidex.Infrastructure.States { public interface ISnapshotStore { - Task WriteAsync(DomainId key, T value, long oldVersion, long newVersion); + Task WriteAsync(DomainId key, T value, long oldVersion, long newVersion, + CancellationToken ct = default); - Task WriteManyAsync(IEnumerable<(DomainId Key, T Value, long Version)> snapshots); + Task WriteManyAsync(IEnumerable<(DomainId Key, T Value, long Version)> snapshots, + CancellationToken ct = default); - Task<(T Value, bool Valid, long Version)> ReadAsync(DomainId key); + Task<(T Value, bool Valid, long Version)> ReadAsync(DomainId key, + CancellationToken ct = default); - Task ClearAsync(); + Task ClearAsync( + CancellationToken ct = default); - Task RemoveAsync(DomainId key); + Task RemoveAsync(DomainId key, + CancellationToken ct = default); - Task ReadAllAsync(Func callback, CancellationToken ct = default); + IAsyncEnumerable<(T State, long Version)> ReadAllAsync( + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Infrastructure/States/Persistence.cs b/backend/src/Squidex.Infrastructure/States/Persistence.cs index 554010d07..0049db756 100644 --- a/backend/src/Squidex.Infrastructure/States/Persistence.cs +++ b/backend/src/Squidex.Infrastructure/States/Persistence.cs @@ -15,7 +15,7 @@ using Squidex.Infrastructure.EventSourcing; namespace Squidex.Infrastructure.States { - internal class Persistence : IPersistence + internal sealed class Persistence : IPersistence { private readonly DomainId ownerKey; private readonly ISnapshotStore snapshotStore; diff --git a/backend/src/Squidex.Infrastructure/StringExtensions.cs b/backend/src/Squidex.Infrastructure/StringExtensions.cs index 6ab22336e..a847ddc5c 100644 --- a/backend/src/Squidex.Infrastructure/StringExtensions.cs +++ b/backend/src/Squidex.Infrastructure/StringExtensions.cs @@ -14,8 +14,8 @@ namespace Squidex.Infrastructure { public static class StringExtensions { - private static readonly Regex EmailRegex = new Regex(@"^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-||_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+([a-z]+|\d|-|\.{0,1}|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])?([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private static readonly Regex PropertyNameRegex = new Regex("^[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*$", RegexOptions.Compiled); + private static readonly Regex EmailRegex = new Regex(@"^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-||_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+([a-z]+|\d|-|\.{0,1}|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])?([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture); + private static readonly Regex PropertyNameRegex = new Regex("^[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*$", RegexOptions.Compiled | RegexOptions.ExplicitCapture); public static bool IsEmail(this string? value) { diff --git a/backend/src/Squidex.Infrastructure/Tasks/AsyncHelper.cs b/backend/src/Squidex.Infrastructure/Tasks/AsyncHelper.cs index 11a91b5b7..6603c87a8 100644 --- a/backend/src/Squidex.Infrastructure/Tasks/AsyncHelper.cs +++ b/backend/src/Squidex.Infrastructure/Tasks/AsyncHelper.cs @@ -25,14 +25,18 @@ namespace Squidex.Infrastructure.Tasks { await Task.WhenAll(task1, task2); +#pragma warning disable MA0042 // Do not use blocking calls in an async method return (task1.Result, task2.Result); +#pragma warning restore MA0042 // Do not use blocking calls in an async method } public static async Task<(T1, T2, T3)> WhenAll(Task task1, Task task2, Task task3) { await Task.WhenAll(task1, task2, task3); +#pragma warning disable MA0042 // Do not use blocking calls in an async method return (task1.Result, task2.Result, task3.Result); +#pragma warning restore MA0042 // Do not use blocking calls in an async method } public static TResult Sync(Func> func) @@ -60,7 +64,7 @@ namespace Squidex.Infrastructure.Tasks var force = new object(); - using var timer = new Timer(_ => source.Writer.TryWrite(force)); + await using var timer = new Timer(_ => source.Writer.TryWrite(force)); async Task TrySendAsync() { diff --git a/backend/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs b/backend/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs index 74fe44b2e..dda76012d 100644 --- a/backend/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs +++ b/backend/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs @@ -35,11 +35,12 @@ namespace Squidex.Infrastructure.Tasks } } - public static async Task WithCancellation(this Task task, CancellationToken cancellationToken) + public static async Task WithCancellation(this Task task, + CancellationToken cancellationToken) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - using (cancellationToken.Register(state => + await using (cancellationToken.Register(state => { ((TaskCompletionSource)state!).TrySetResult(null!); }, diff --git a/backend/src/Squidex.Infrastructure/Translations/ResourcesLocalizer.cs b/backend/src/Squidex.Infrastructure/Translations/ResourcesLocalizer.cs index df345ceba..975ed47b4 100644 --- a/backend/src/Squidex.Infrastructure/Translations/ResourcesLocalizer.cs +++ b/backend/src/Squidex.Infrastructure/Translations/ResourcesLocalizer.cs @@ -74,13 +74,13 @@ namespace Squidex.Infrastructure.Translations if (variable.Length > 0) { - if (variable.EndsWith("|lower")) + if (variable.EndsWith("|lower", StringComparison.OrdinalIgnoreCase)) { variable = variable[..^6]; shouldLower = true; } - if (variable.EndsWith("|upper")) + if (variable.EndsWith("|upper", StringComparison.OrdinalIgnoreCase)) { variable = variable[..^6]; shouldUpper = true; @@ -115,13 +115,13 @@ namespace Squidex.Infrastructure.Translations { if (shouldLower && !char.IsLower(variableValue[0])) { - sb.Append(char.ToLower(variableValue[0])); + sb.Append(char.ToLower(variableValue[0], CultureInfo.InvariantCulture)); sb.Append(variableValue.AsSpan()[1..]); } else if (shouldUpper && !char.IsUpper(variableValue[0])) { - sb.Append(char.ToUpper(variableValue[0])); + sb.Append(char.ToUpper(variableValue[0], CultureInfo.InvariantCulture)); sb.Append(variableValue.AsSpan()[1..]); } diff --git a/backend/src/Squidex.Infrastructure/UsageTracking/ApiUsageTracker.cs b/backend/src/Squidex.Infrastructure/UsageTracking/ApiUsageTracker.cs index a17ebdfc2..ab8ae01cb 100644 --- a/backend/src/Squidex.Infrastructure/UsageTracking/ApiUsageTracker.cs +++ b/backend/src/Squidex.Infrastructure/UsageTracking/ApiUsageTracker.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; namespace Squidex.Infrastructure.UsageTracking @@ -23,25 +24,36 @@ namespace Squidex.Infrastructure.UsageTracking this.usageTracker = usageTracker; } - public async Task GetMonthCallsAsync(string key, DateTime date, string? category) + public Task DeleteAsync(string key, + CancellationToken ct = default) { var apiKey = GetKey(key); - var counters = await usageTracker.GetForMonthAsync(apiKey, date, category); + return usageTracker.DeleteAsync(apiKey, ct); + } + + public async Task GetMonthCallsAsync(string key, DateTime date, string? category, + CancellationToken ct = default) + { + var apiKey = GetKey(key); + + var counters = await usageTracker.GetForMonthAsync(apiKey, date, category, ct); return counters.GetInt64(CounterTotalCalls); } - public async Task GetMonthBytesAsync(string key, DateTime date, string? category) + public async Task GetMonthBytesAsync(string key, DateTime date, string? category, + CancellationToken ct = default) { var apiKey = GetKey(key); - var counters = await usageTracker.GetForMonthAsync(apiKey, date, category); + var counters = await usageTracker.GetForMonthAsync(apiKey, date, category, ct); return counters.GetInt64(CounterTotalBytes); } - public Task TrackAsync(DateTime date, string key, string? category, double weight, long elapsedMs, long bytes) + public Task TrackAsync(DateTime date, string key, string? category, double weight, long elapsedMs, long bytes, + CancellationToken ct = default) { var apiKey = GetKey(key); @@ -52,14 +64,15 @@ namespace Squidex.Infrastructure.UsageTracking [CounterTotalBytes] = bytes }; - return usageTracker.TrackAsync(date, apiKey, category, counters); + return usageTracker.TrackAsync(date, apiKey, category, counters, ct); } - public async Task<(ApiStatsSummary, Dictionary> Details)> QueryAsync(string key, DateTime fromDate, DateTime toDate) + public async Task<(ApiStatsSummary, Dictionary> Details)> QueryAsync(string key, DateTime fromDate, DateTime toDate, + CancellationToken ct = default) { var apiKey = GetKey(key); - var queries = await usageTracker.QueryAsync(apiKey, fromDate, toDate); + var queries = await usageTracker.QueryAsync(apiKey, fromDate, toDate, ct); var details = new Dictionary>(); @@ -90,7 +103,7 @@ namespace Squidex.Infrastructure.UsageTracking var summaryElapsedAvg = CalculateAverage(summaryCalls, summaryElapsed); - var monthStats = await usageTracker.GetForMonthAsync(apiKey, DateTime.Today, null); + var monthStats = await usageTracker.GetForMonthAsync(apiKey, DateTime.Today, null, ct); var summary = new ApiStatsSummary( summaryElapsedAvg, diff --git a/backend/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs b/backend/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs index e0815a25d..ea38d344d 100644 --- a/backend/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs +++ b/backend/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs @@ -25,13 +25,15 @@ namespace Squidex.Infrastructure.UsageTracking private readonly CompletionTimer timer; private ConcurrentDictionary<(string Key, string Category, DateTime Date), Counters> jobs = new ConcurrentDictionary<(string Key, string Category, DateTime Date), Counters>(); + public bool ForceWrite { get; set; } + public BackgroundUsageTracker(IUsageRepository usageRepository, ISemanticLog log) { this.usageRepository = usageRepository; this.log = log; - timer = new CompletionTimer(Intervall, ct => TrackAsync(), Intervall); + timer = new CompletionTimer(Intervall, TrackAsync, Intervall); } protected override void DisposeObject(bool disposing) @@ -49,7 +51,8 @@ namespace Squidex.Infrastructure.UsageTracking timer.SkipCurrentDelay(); } - private async Task TrackAsync() + private async Task TrackAsync( + CancellationToken ct) { try { @@ -70,7 +73,12 @@ namespace Squidex.Infrastructure.UsageTracking updateIndex++; } - await usageRepository.TrackUsagesAsync(updates); + if (ForceWrite) + { + ct = default; + } + + await usageRepository.TrackUsagesAsync(updates, ct); } } catch (Exception ex) @@ -81,7 +89,16 @@ namespace Squidex.Infrastructure.UsageTracking } } - public Task TrackAsync(DateTime date, string key, string? category, Counters counters) + public Task DeleteAsync(string key, + CancellationToken ct = default) + { + Guard.NotNull(key, nameof(key)); + + return usageRepository.DeleteAsync(key, ct); + } + + public Task TrackAsync(DateTime date, string key, string? category, Counters counters, + CancellationToken ct = default) { Guard.NotNullOrEmpty(key, nameof(key)); Guard.NotNull(counters, nameof(counters)); @@ -95,13 +112,14 @@ namespace Squidex.Infrastructure.UsageTracking return Task.CompletedTask; } - public async Task>> QueryAsync(string key, DateTime fromDate, DateTime toDate) + public async Task>> QueryAsync(string key, DateTime fromDate, DateTime toDate, + CancellationToken ct = default) { Guard.NotNullOrEmpty(key, nameof(key)); ThrowIfDisposed(); - var usages = await usageRepository.QueryAsync(key, fromDate, toDate); + var usages = await usageRepository.QueryAsync(key, fromDate, toDate, ct); var result = new Dictionary>(); @@ -125,7 +143,7 @@ namespace Squidex.Infrastructure.UsageTracking for (var date = fromDate; date <= toDate; date = date.AddDays(1)) { - var counters = value.FirstOrDefault(x => x.Date == date)?.Counters; + var counters = value.Find(x => x.Date == date)?.Counters; enriched.Add((date, counters ?? new Counters())); } @@ -136,21 +154,23 @@ namespace Squidex.Infrastructure.UsageTracking return result; } - public Task GetForMonthAsync(string key, DateTime date, string? category) + public Task GetForMonthAsync(string key, DateTime date, string? category, + CancellationToken ct = default) { var dateFrom = new DateTime(date.Year, date.Month, 1); var dateTo = dateFrom.AddMonths(1).AddDays(-1); - return GetAsync(key, dateFrom, dateTo, category); + return GetAsync(key, dateFrom, dateTo, category, ct); } - public async Task GetAsync(string key, DateTime fromDate, DateTime toDate, string? category) + public async Task GetAsync(string key, DateTime fromDate, DateTime toDate, string? category, + CancellationToken ct = default) { Guard.NotNullOrEmpty(key, nameof(key)); ThrowIfDisposed(); - var queried = await usageRepository.QueryAsync(key, fromDate, toDate); + var queried = await usageRepository.QueryAsync(key, fromDate, toDate, ct); if (category != null) { diff --git a/backend/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs b/backend/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs index dd01e9945..760ca8cef 100644 --- a/backend/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs +++ b/backend/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; @@ -27,21 +28,32 @@ namespace Squidex.Infrastructure.UsageTracking this.cache = cache; } - public Task>> QueryAsync(string key, DateTime fromDate, DateTime toDate) + public Task DeleteAsync(string key, + CancellationToken ct = default) { Guard.NotNull(key, nameof(key)); - return inner.QueryAsync(key, fromDate, toDate); + return inner.DeleteAsync(key, ct); } - public Task TrackAsync(DateTime date, string key, string? category, Counters counters) + public Task>> QueryAsync(string key, DateTime fromDate, DateTime toDate, + CancellationToken ct = default) { Guard.NotNull(key, nameof(key)); - return inner.TrackAsync(date, key, category, counters); + return inner.QueryAsync(key, fromDate, toDate, ct); } - public Task GetForMonthAsync(string key, DateTime date, string? category) + public Task TrackAsync(DateTime date, string key, string? category, Counters counters, + CancellationToken ct = default) + { + Guard.NotNull(key, nameof(key)); + + return inner.TrackAsync(date, key, category, counters, ct); + } + + public Task GetForMonthAsync(string key, DateTime date, string? category, + CancellationToken ct = default) { Guard.NotNull(key, nameof(key)); @@ -51,11 +63,12 @@ namespace Squidex.Infrastructure.UsageTracking { entry.AbsoluteExpirationRelativeToNow = CacheDuration; - return inner.GetForMonthAsync(key, date, category); + return inner.GetForMonthAsync(key, date, category, ct); }); } - public Task GetAsync(string key, DateTime fromDate, DateTime toDate, string? category) + public Task GetAsync(string key, DateTime fromDate, DateTime toDate, string? category, + CancellationToken ct = default) { Guard.NotNull(key, nameof(key)); @@ -65,7 +78,7 @@ namespace Squidex.Infrastructure.UsageTracking { entry.AbsoluteExpirationRelativeToNow = CacheDuration; - return inner.GetAsync(key, fromDate, toDate, category); + return inner.GetAsync(key, fromDate, toDate, category, ct); }); } } diff --git a/backend/src/Squidex.Infrastructure/UsageTracking/IApiUsageTracker.cs b/backend/src/Squidex.Infrastructure/UsageTracking/IApiUsageTracker.cs index 7a70a516e..7f6b8598c 100644 --- a/backend/src/Squidex.Infrastructure/UsageTracking/IApiUsageTracker.cs +++ b/backend/src/Squidex.Infrastructure/UsageTracking/IApiUsageTracker.cs @@ -7,18 +7,26 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; namespace Squidex.Infrastructure.UsageTracking { public interface IApiUsageTracker { - Task TrackAsync(DateTime date, string key, string? category, double weight, long elapsedMs, long bytes); + Task DeleteAsync(string key, + CancellationToken ct = default); - Task GetMonthCallsAsync(string key, DateTime date, string? category); + Task TrackAsync(DateTime date, string key, string? category, double weight, long elapsedMs, long bytes, + CancellationToken ct = default); - Task GetMonthBytesAsync(string key, DateTime date, string? category); + Task GetMonthCallsAsync(string key, DateTime date, string? category, + CancellationToken ct = default); - Task<(ApiStatsSummary, Dictionary> Details)> QueryAsync(string key, DateTime fromDate, DateTime toDate); + Task GetMonthBytesAsync(string key, DateTime date, string? category, + CancellationToken ct = default); + + Task<(ApiStatsSummary, Dictionary> Details)> QueryAsync(string key, DateTime fromDate, DateTime toDate, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Infrastructure/UsageTracking/IUsageRepository.cs b/backend/src/Squidex.Infrastructure/UsageTracking/IUsageRepository.cs index 9a8b855a7..6fd0d7e12 100644 --- a/backend/src/Squidex.Infrastructure/UsageTracking/IUsageRepository.cs +++ b/backend/src/Squidex.Infrastructure/UsageTracking/IUsageRepository.cs @@ -7,16 +7,23 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; namespace Squidex.Infrastructure.UsageTracking { public interface IUsageRepository { - Task TrackUsagesAsync(UsageUpdate update); + Task TrackUsagesAsync(UsageUpdate update, + CancellationToken ct = default); - Task TrackUsagesAsync(params UsageUpdate[] updates); + Task TrackUsagesAsync(UsageUpdate[] updates, + CancellationToken ct = default); - Task> QueryAsync(string key, DateTime fromDate, DateTime toDate); + Task> QueryAsync(string key, DateTime fromDate, DateTime toDate, + CancellationToken ct = default); + + Task DeleteAsync(string key, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs b/backend/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs index 03816ca0b..d6178ccf1 100644 --- a/backend/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs +++ b/backend/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs @@ -7,18 +7,26 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; namespace Squidex.Infrastructure.UsageTracking { public interface IUsageTracker { - Task TrackAsync(DateTime date, string key, string? category, Counters counters); + Task TrackAsync(DateTime date, string key, string? category, Counters counters, + CancellationToken ct = default); - Task GetForMonthAsync(string key, DateTime date, string? category); + Task GetForMonthAsync(string key, DateTime date, string? category, + CancellationToken ct = default); - Task GetAsync(string key, DateTime fromDate, DateTime toDate, string? category); + Task GetAsync(string key, DateTime fromDate, DateTime toDate, string? category, + CancellationToken ct = default); - Task>> QueryAsync(string key, DateTime fromDate, DateTime toDate); + Task>> QueryAsync(string key, DateTime fromDate, DateTime toDate, + CancellationToken ct = default); + + Task DeleteAsync(string key, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Infrastructure/Validation/AbsoluteUrlAttribute.cs b/backend/src/Squidex.Infrastructure/Validation/AbsoluteUrlAttribute.cs index 07c70f850..e8c4b5a36 100644 --- a/backend/src/Squidex.Infrastructure/Validation/AbsoluteUrlAttribute.cs +++ b/backend/src/Squidex.Infrastructure/Validation/AbsoluteUrlAttribute.cs @@ -1,4 +1,4 @@ -// ========================================================================== +// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschraenkt) @@ -12,6 +12,7 @@ using Squidex.Text; namespace Squidex.Infrastructure.Validation { + [AttributeUsage(AttributeTargets.Property)] public sealed class AbsoluteUrlAttribute : ValidationAttribute { public override string FormatErrorMessage(string name) diff --git a/backend/src/Squidex.Shared/Identity/SquidexClaimsExtensions.cs b/backend/src/Squidex.Shared/Identity/SquidexClaimsExtensions.cs index a4731c2dc..3aae4498b 100644 --- a/backend/src/Squidex.Shared/Identity/SquidexClaimsExtensions.cs +++ b/backend/src/Squidex.Shared/Identity/SquidexClaimsExtensions.cs @@ -123,9 +123,9 @@ namespace Squidex.Shared.Identity { var url = user.FirstOrDefault(x => x.Type == SquidexClaimTypes.PictureUrl)?.Value; - if (!string.IsNullOrWhiteSpace(url) && Uri.IsWellFormedUriString(url, UriKind.Absolute) && url.Contains("gravatar")) + if (!string.IsNullOrWhiteSpace(url) && Uri.IsWellFormedUriString(url, UriKind.Absolute) && url.Contains("gravatar", StringComparison.Ordinal)) { - if (url.Contains("?")) + if (url.Contains("?", StringComparison.Ordinal)) { url += "&d=404"; } diff --git a/backend/src/Squidex.Shared/Permissions.cs b/backend/src/Squidex.Shared/Permissions.cs index 368719496..4221011c0 100644 --- a/backend/src/Squidex.Shared/Permissions.cs +++ b/backend/src/Squidex.Shared/Permissions.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using Squidex.Infrastructure; using Squidex.Infrastructure.Security; @@ -170,7 +171,9 @@ namespace Squidex.Shared { Guard.NotNull(id, nameof(id)); - return new Permission(id.Replace("{app}", app ?? Permission.Any).Replace("{schema}", schema ?? Permission.Any)); + return new Permission(id + .Replace("{app}", app ?? Permission.Any, StringComparison.Ordinal) + .Replace("{schema}", schema ?? Permission.Any, StringComparison.Ordinal)); } } } diff --git a/backend/src/Squidex.Shared/Squidex.Shared.csproj b/backend/src/Squidex.Shared/Squidex.Shared.csproj index 9497a56f5..007762ef1 100644 --- a/backend/src/Squidex.Shared/Squidex.Shared.csproj +++ b/backend/src/Squidex.Shared/Squidex.Shared.csproj @@ -9,6 +9,10 @@ True + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/backend/src/Squidex.Shared/Users/ClientUser.cs b/backend/src/Squidex.Shared/Users/ClientUser.cs index 7bae92707..2ed86807d 100644 --- a/backend/src/Squidex.Shared/Users/ClientUser.cs +++ b/backend/src/Squidex.Shared/Users/ClientUser.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; using System.Security.Claims; using Squidex.Infrastructure; @@ -38,7 +39,7 @@ namespace Squidex.Shared.Users get => claims; } - public object Identity => throw new System.NotImplementedException(); + public object Identity => throw new NotSupportedException(); public ClientUser(RefToken token) { diff --git a/backend/src/Squidex.Shared/Users/IUserResolver.cs b/backend/src/Squidex.Shared/Users/IUserResolver.cs index 8082228d1..31f679844 100644 --- a/backend/src/Squidex.Shared/Users/IUserResolver.cs +++ b/backend/src/Squidex.Shared/Users/IUserResolver.cs @@ -6,24 +6,32 @@ // ========================================================================== using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; namespace Squidex.Shared.Users { public interface IUserResolver { - Task<(IUser? User, bool Created)> CreateUserIfNotExistsAsync(string email, bool invited = false); + Task<(IUser? User, bool Created)> CreateUserIfNotExistsAsync(string email, bool invited = false, + CancellationToken ct = default); - Task FindByIdOrEmailAsync(string idOrEmail); + Task SetClaimAsync(string id, string type, string value, bool silent = false, + CancellationToken ct = default); - Task FindByIdAsync(string idOrEmail); + Task FindByIdOrEmailAsync(string idOrEmail, + CancellationToken ct = default); - Task SetClaimAsync(string id, string type, string value, bool silent = false); + Task FindByIdAsync(string idOrEmail, + CancellationToken ct = default); - Task> QueryByEmailAsync(string email); + Task> QueryByEmailAsync(string email, + CancellationToken ct = default); - Task> QueryAllAsync(); + Task> QueryAllAsync( + CancellationToken ct = default); - Task> QueryManyAsync(string[] ids); + Task> QueryManyAsync(string[] ids, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Web/ApiExceptionConverter.cs b/backend/src/Squidex.Web/ApiExceptionConverter.cs index bd1fdbb70..98ee76ff1 100644 --- a/backend/src/Squidex.Web/ApiExceptionConverter.cs +++ b/backend/src/Squidex.Web/ApiExceptionConverter.cs @@ -147,13 +147,13 @@ namespace Squidex.Web var builder = new StringBuilder(property.Length); - builder.Append(char.ToLower(property[0])); + builder.Append(char.ToLowerInvariant(property[0])); foreach (var character in property.Skip(1)) { if (prevChar == '.') { - builder.Append(char.ToLower(character)); + builder.Append(char.ToLowerInvariant(character)); } else { diff --git a/backend/src/Squidex.Web/CommandMiddlewares/ETagCommandMiddleware.cs b/backend/src/Squidex.Web/CommandMiddlewares/ETagCommandMiddleware.cs index 123f26053..19c106881 100644 --- a/backend/src/Squidex.Web/CommandMiddlewares/ETagCommandMiddleware.cs +++ b/backend/src/Squidex.Web/CommandMiddlewares/ETagCommandMiddleware.cs @@ -64,7 +64,7 @@ namespace Squidex.Web.CommandMiddlewares private static void SetResponsEtag(HttpContext httpContext, long version) { - httpContext.Response.Headers[HeaderNames.ETag] = version.ToString(); + httpContext.Response.Headers[HeaderNames.ETag] = version.ToString(CultureInfo.InvariantCulture); } private static bool TryParseEtag(HttpContext httpContext, out long version) diff --git a/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithContentIdCommandMiddleware.cs b/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithContentIdCommandMiddleware.cs index 9c1c1b240..457b5faa1 100644 --- a/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithContentIdCommandMiddleware.cs +++ b/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithContentIdCommandMiddleware.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Threading.Tasks; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Infrastructure.Commands; @@ -19,7 +20,7 @@ namespace Squidex.Web.CommandMiddlewares { if (context.Command is ContentCommand contentCommand && contentCommand is not CreateContent) { - if (contentCommand.ContentId.ToString().Equals(SingletonId)) + if (contentCommand.ContentId.ToString().Equals(SingletonId, StringComparison.Ordinal)) { contentCommand.ContentId = contentCommand.SchemaId.Id; } diff --git a/backend/src/Squidex.Web/ETagExtensions.cs b/backend/src/Squidex.Web/ETagExtensions.cs index 7c7a78a31..a3646b957 100644 --- a/backend/src/Squidex.Web/ETagExtensions.cs +++ b/backend/src/Squidex.Web/ETagExtensions.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Security.Cryptography; using System.Text; using Squidex.Domain.Apps.Entities; @@ -49,7 +50,7 @@ namespace Squidex.Web } var cacheBuffer = hasher.GetHashAndReset(); - var cacheString = BitConverter.ToString(cacheBuffer).Replace("-", string.Empty).ToUpperInvariant(); + var cacheString = BitConverter.ToString(cacheBuffer).Replace("-", string.Empty, StringComparison.Ordinal).ToUpperInvariant(); return cacheString; } @@ -57,7 +58,7 @@ namespace Squidex.Web public static string ToEtag(this T entity) where T : IEntity, IEntityWithVersion { - return entity.Version.ToString(); + return entity.Version.ToString(CultureInfo.InvariantCulture); } } } diff --git a/backend/src/Squidex.Web/FileCallbackResult.cs b/backend/src/Squidex.Web/FileCallbackResult.cs index bb423ee6c..ef930278e 100644 --- a/backend/src/Squidex.Web/FileCallbackResult.cs +++ b/backend/src/Squidex.Web/FileCallbackResult.cs @@ -16,7 +16,8 @@ using Squidex.Web.Pipeline; namespace Squidex.Web { - public delegate Task FileCallback(Stream body, BytesRange range, CancellationToken ct); + public delegate Task FileCallback(Stream body, BytesRange range, + CancellationToken ct); public sealed class FileCallbackResult : FileResult { diff --git a/backend/src/Squidex.Web/Pipeline/AppResolver.cs b/backend/src/Squidex.Web/Pipeline/AppResolver.cs index eeecd45e6..8ae304c04 100644 --- a/backend/src/Squidex.Web/Pipeline/AppResolver.cs +++ b/backend/src/Squidex.Web/Pipeline/AppResolver.cs @@ -42,7 +42,7 @@ namespace Squidex.Web.Pipeline { var isFrontend = user.IsInClient(DefaultClients.Frontend); - var app = await appProvider.GetAppAsync(appName, !isFrontend); + var app = await appProvider.GetAppAsync(appName, !isFrontend, context.HttpContext.RequestAborted); if (app == null) { diff --git a/backend/src/Squidex.Web/Pipeline/CachingFilter.cs b/backend/src/Squidex.Web/Pipeline/CachingFilter.cs index 29b538c37..36c34a515 100644 --- a/backend/src/Squidex.Web/Pipeline/CachingFilter.cs +++ b/backend/src/Squidex.Web/Pipeline/CachingFilter.cs @@ -12,6 +12,8 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Net.Http.Headers; +#pragma warning disable MA0073 // Avoid comparison with bool constant + namespace Squidex.Web.Pipeline { public sealed class CachingFilter : IAsyncActionFilter diff --git a/backend/src/Squidex.Web/Pipeline/CachingManager.cs b/backend/src/Squidex.Web/Pipeline/CachingManager.cs index 95bd17934..444485be2 100644 --- a/backend/src/Squidex.Web/Pipeline/CachingManager.cs +++ b/backend/src/Squidex.Web/Pipeline/CachingManager.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Security.Cryptography; using System.Text; @@ -101,7 +102,7 @@ namespace Squidex.Web.Pipeline using (Telemetry.Activities.StartActivity("CalculateEtag")) { var cacheBuffer = hasher.GetHashAndReset(); - var cacheString = BitConverter.ToString(cacheBuffer).Replace("-", string.Empty).ToUpperInvariant(); + var cacheString = BitConverter.ToString(cacheBuffer).Replace("-", string.Empty, StringComparison.Ordinal).ToUpperInvariant(); response.Headers.Add(HeaderNames.ETag, cacheString); } @@ -194,7 +195,7 @@ namespace Squidex.Web.Pipeline { var headers = httpContext.Request.Headers; - if (!headers.TryGetValue(SurrogateKeySizeHeader, out var header) || !int.TryParse(header, out var size)) + if (!headers.TryGetValue(SurrogateKeySizeHeader, out var header) || !int.TryParse(header, NumberStyles.Integer, CultureInfo.InvariantCulture, out var size)) { size = cachingOptions.MaxSurrogateKeysSize; } diff --git a/backend/src/Squidex.Web/Pipeline/RequestExceptionMiddleware.cs b/backend/src/Squidex.Web/Pipeline/RequestExceptionMiddleware.cs index 81e5ba9ea..c487f1025 100644 --- a/backend/src/Squidex.Web/Pipeline/RequestExceptionMiddleware.cs +++ b/backend/src/Squidex.Web/Pipeline/RequestExceptionMiddleware.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Globalization; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -76,7 +77,7 @@ namespace Squidex.Web.Pipeline { statusCode = 0; - return context.Request.Query.TryGetValue("error", out var header) && int.TryParse(header, out statusCode); + return context.Request.Query.TryGetValue("error", out var header) && int.TryParse(header, NumberStyles.Integer, CultureInfo.InvariantCulture, out statusCode); } private static bool IsErrorStatusCode(int statusCode) diff --git a/backend/src/Squidex.Web/Pipeline/SameSiteCookiesServiceCollectionExtensions.cs b/backend/src/Squidex.Web/Pipeline/SameSiteCookiesServiceCollectionExtensions.cs index 74dd089a3..f9a1db133 100644 --- a/backend/src/Squidex.Web/Pipeline/SameSiteCookiesServiceCollectionExtensions.cs +++ b/backend/src/Squidex.Web/Pipeline/SameSiteCookiesServiceCollectionExtensions.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; @@ -100,8 +101,8 @@ namespace Squidex.Web.Pipeline // than 12 are not supporting SameSite at all. Starting with version 13 // unknown values are NOT treated as strict anymore. Therefore we only // need to check version 12. - if (userAgent.Contains("CPU iPhone OS 12") || - userAgent.Contains("iPad; CPU OS 12")) + if (userAgent.Contains("CPU iPhone OS 12", StringComparison.Ordinal) || + userAgent.Contains("iPad; CPU OS 12", StringComparison.Ordinal)) { return true; } @@ -117,9 +118,9 @@ namespace Squidex.Web.Pipeline // than 10.14 are not supporting SameSite at all. Starting with version // 10.15 unknown values are NOT treated as strict anymore. Therefore we // only need to check version 10.14. - if (userAgent.Contains("Safari") && - userAgent.Contains("Macintosh; Intel Mac OS X 10_14") && - userAgent.Contains("Version/")) + if (userAgent.Contains("Safari", StringComparison.Ordinal) && + userAgent.Contains("Macintosh; Intel Mac OS X 10_14", StringComparison.Ordinal) && + userAgent.Contains("Version/", StringComparison.Ordinal)) { return true; } @@ -132,7 +133,8 @@ namespace Squidex.Web.Pipeline // We can not validate this assumption, but we trust Microsofts // evaluation. And overall not sending a SameSite value equals to the same // behavior as SameSite=None for these old versions anyways. - if (userAgent.Contains("Chrome/5") || userAgent.Contains("Chrome/6")) + if (userAgent.Contains("Chrome/5", StringComparison.Ordinal) || + userAgent.Contains("Chrome/6", StringComparison.Ordinal)) { return true; } diff --git a/backend/src/Squidex.Web/Pipeline/UsageMiddleware.cs b/backend/src/Squidex.Web/Pipeline/UsageMiddleware.cs index 0bbbea0f5..e51dcf75c 100644 --- a/backend/src/Squidex.Web/Pipeline/UsageMiddleware.cs +++ b/backend/src/Squidex.Web/Pipeline/UsageMiddleware.cs @@ -71,6 +71,7 @@ namespace Squidex.Web.Pipeline request.UserId = context.User.OpenIdSubject(); request.UserClientId = clientId; +#pragma warning disable MA0040 // Flow the cancellation token await usageLog.LogAsync(appId.Value, request); if (request.Costs > 0) @@ -83,6 +84,7 @@ namespace Squidex.Web.Pipeline request.ElapsedMs, request.Bytes); } +#pragma warning restore MA0040 // Flow the cancellation token } } } diff --git a/backend/src/Squidex.Web/Pipeline/UsagePipeWriter.cs b/backend/src/Squidex.Web/Pipeline/UsagePipeWriter.cs index 2e09d5bab..a7ab34cdc 100644 --- a/backend/src/Squidex.Web/Pipeline/UsagePipeWriter.cs +++ b/backend/src/Squidex.Web/Pipeline/UsagePipeWriter.cs @@ -44,7 +44,8 @@ namespace Squidex.Web.Pipeline inner.Complete(); } - public override ValueTask FlushAsync(CancellationToken cancellationToken = default) + public override ValueTask FlushAsync( + CancellationToken cancellationToken = default) { return inner.FlushAsync(cancellationToken); } diff --git a/backend/src/Squidex.Web/Pipeline/UsageResponseBodyFeature.cs b/backend/src/Squidex.Web/Pipeline/UsageResponseBodyFeature.cs index f610831f3..1e7fb73b9 100644 --- a/backend/src/Squidex.Web/Pipeline/UsageResponseBodyFeature.cs +++ b/backend/src/Squidex.Web/Pipeline/UsageResponseBodyFeature.cs @@ -43,7 +43,8 @@ namespace Squidex.Web.Pipeline this.inner = inner; } - public Task StartAsync(CancellationToken cancellationToken = default) + public Task StartAsync( + CancellationToken cancellationToken = default) { return inner.StartAsync(cancellationToken); } @@ -58,7 +59,8 @@ namespace Squidex.Web.Pipeline inner.DisableBuffering(); } - public async Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellationToken = default) + public async Task SendFileAsync(string path, long offset, long? count, + CancellationToken cancellationToken = default) { await inner.SendFileAsync(path, offset, count, cancellationToken); diff --git a/backend/src/Squidex.Web/Pipeline/UsageStream.cs b/backend/src/Squidex.Web/Pipeline/UsageStream.cs index 77661f81f..e9b78f165 100644 --- a/backend/src/Squidex.Web/Pipeline/UsageStream.cs +++ b/backend/src/Squidex.Web/Pipeline/UsageStream.cs @@ -64,7 +64,8 @@ namespace Squidex.Web.Pipeline return result; } - public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + public override async Task WriteAsync(byte[] buffer, int offset, int count, + CancellationToken cancellationToken) { await inner.WriteAsync(buffer, offset, count, cancellationToken); @@ -78,7 +79,8 @@ namespace Squidex.Web.Pipeline bytesWritten += count; } - public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + public override async ValueTask WriteAsync(ReadOnlyMemory buffer, + CancellationToken cancellationToken = default) { await inner.WriteAsync(buffer, cancellationToken); @@ -99,7 +101,8 @@ namespace Squidex.Web.Pipeline bytesWritten++; } - public override Task FlushAsync(CancellationToken cancellationToken) + public override Task FlushAsync( + CancellationToken cancellationToken) { return inner.FlushAsync(cancellationToken); } diff --git a/backend/src/Squidex.Web/Squidex.Web.csproj b/backend/src/Squidex.Web/Squidex.Web.csproj index dbab45ba9..3dbb9ebc9 100644 --- a/backend/src/Squidex.Web/Squidex.Web.csproj +++ b/backend/src/Squidex.Web/Squidex.Web.csproj @@ -18,6 +18,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/ErrorDtoProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/ErrorDtoProcessor.cs index 3faa62505..86f89e977 100644 --- a/backend/src/Squidex/Areas/Api/Config/OpenApi/ErrorDtoProcessor.cs +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/ErrorDtoProcessor.cs @@ -55,8 +55,8 @@ namespace Squidex.Areas.Api.Config.OpenApi foreach (var (code, response) in operation.Responses.ToList()) { if (string.IsNullOrWhiteSpace(response.Description) || - response.Description?.Contains("=>") == true || - response.Description?.Contains("=>") == true) + response.Description?.Contains("=>", StringComparison.Ordinal) == true || + response.Description?.Contains("=>", StringComparison.Ordinal) == true) { operation.Responses.Remove(code); } diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/SecurityProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/SecurityProcessor.cs index ad0d371aa..da4d4019b 100644 --- a/backend/src/Squidex/Areas/Api/Config/OpenApi/SecurityProcessor.cs +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/SecurityProcessor.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; using System.Linq; using NSwag; @@ -54,7 +55,7 @@ namespace Squidex.Areas.Api.Config.OpenApi private static void SetupDescription(OpenApiSecurityScheme securityScheme, string tokenUrl) { - var securityText = Properties.Resources.OpenApiSecurity.Replace("", tokenUrl); + var securityText = Properties.Resources.OpenApiSecurity.Replace("", tokenUrl, StringComparison.Ordinal); securityScheme.Description = securityText; } diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/XmlResponseTypesProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/XmlResponseTypesProcessor.cs index e7ef6a390..bbe640280 100644 --- a/backend/src/Squidex/Areas/Api/Config/OpenApi/XmlResponseTypesProcessor.cs +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/XmlResponseTypesProcessor.cs @@ -40,7 +40,7 @@ namespace Squidex.Areas.Api.Config.OpenApi var description = match.Groups["Description"].Value; - if (description.Contains("=>")) + if (description.Contains("=>", StringComparison.Ordinal)) { throw new InvalidOperationException("Description not formatted correcly."); } diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/XmlTagProcessor.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/XmlTagProcessor.cs index 0c6770f22..16c543735 100644 --- a/backend/src/Squidex/Areas/Api/Config/OpenApi/XmlTagProcessor.cs +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/XmlTagProcessor.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Linq; using System.Reflection; using Microsoft.AspNetCore.Mvc; @@ -36,7 +37,7 @@ namespace Squidex.Areas.Api.Config.OpenApi { tag.Description ??= string.Empty; - if (!tag.Description.Contains(description)) + if (!tag.Description.Contains(description, StringComparison.Ordinal)) { tag.Description += "\n\n" + description; } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs index ce5a25005..aeb0ad5ae 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs @@ -26,8 +26,6 @@ using Squidex.Infrastructure.Validation; using Squidex.Shared; using Squidex.Web; -#pragma warning disable CA2016 // Forward the 'CancellationToken' parameter to methods that take one - namespace Squidex.Areas.Api.Controllers.Apps { /// @@ -75,7 +73,7 @@ namespace Squidex.Areas.Api.Controllers.Apps var userOrClientId = HttpContext.User.UserOrClientId()!; var userPermissions = Resources.Context.UserPermissions; - var apps = await appProvider.GetUserAppsAsync(userOrClientId, userPermissions); + var apps = await appProvider.GetUserAppsAsync(userOrClientId, userPermissions, HttpContext.RequestAborted); var response = Deferred.Response(() => { @@ -223,30 +221,11 @@ namespace Squidex.Areas.Api.Controllers.Apps { using (Telemetry.Activities.StartActivity("Resize")) { - using (var sourceStream = GetTempStream()) + await using (var destinationStream = GetTempStream()) { - using (var destinationStream = GetTempStream()) - { - using (Telemetry.Activities.StartActivity("ResizeDownload")) - { - await appImageStore.DownloadAsync(App.Id, sourceStream); - sourceStream.Position = 0; - } - - using (Telemetry.Activities.StartActivity("ResizeImage")) - { - await assetThumbnailGenerator.CreateThumbnailAsync(sourceStream, destinationStream, ResizeOptions); - destinationStream.Position = 0; - } - - using (Telemetry.Activities.StartActivity("ResizeUpload")) - { - await assetStore.UploadAsync(resizedAsset, destinationStream); - destinationStream.Position = 0; - } - - await destinationStream.CopyToAsync(body, ct); - } + await ResizeAsync(resizedAsset, destinationStream); + + await destinationStream.CopyToAsync(body, ct); } } } @@ -258,6 +237,32 @@ namespace Squidex.Areas.Api.Controllers.Apps }; } + private async Task ResizeAsync(string resizedAsset, FileStream destinationStream) + { +#pragma warning disable MA0040 // Flow the cancellation token + await using (var sourceStream = GetTempStream()) + { + using (Telemetry.Activities.StartActivity("ResizeDownload")) + { + await appImageStore.DownloadAsync(App.Id, sourceStream); + sourceStream.Position = 0; + } + + using (Telemetry.Activities.StartActivity("ResizeImage")) + { + await assetThumbnailGenerator.CreateThumbnailAsync(sourceStream, destinationStream, ResizeOptions); + destinationStream.Position = 0; + } + + using (Telemetry.Activities.StartActivity("ResizeUpload")) + { + await assetStore.UploadAsync(resizedAsset, destinationStream); + destinationStream.Position = 0; + } + } +#pragma warning restore MA0040 // Flow the cancellation token + } + /// /// Remove the app image. /// @@ -279,11 +284,11 @@ namespace Squidex.Areas.Api.Controllers.Apps } /// - /// Archive the app. + /// Delete the app. /// - /// The name of the app to archive. + /// The name of the app to delete. /// - /// 204 => App archived. + /// 204 => App deleted. /// 404 => App not found. /// [HttpDelete] @@ -292,7 +297,7 @@ namespace Squidex.Areas.Api.Controllers.Apps [ApiCosts(0)] public async Task DeleteApp(string app) { - await CommandBus.PublishAsync(new ArchiveApp()); + await CommandBus.PublishAsync(new DeleteApp()); return NoContent(); } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs index 2ee5e9585..b70835d06 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Globalization; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -22,8 +23,6 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Web; -#pragma warning disable CA2016 // Forward the 'CancellationToken' parameter to methods that take one - namespace Squidex.Areas.Api.Controllers.Assets { /// @@ -145,7 +144,7 @@ namespace Squidex.Areas.Api.Controllers.Assets FileCallback callback; - Response.Headers[HeaderNames.ETag] = asset.FileVersion.ToString(); + Response.Headers[HeaderNames.ETag] = asset.FileVersion.ToString(CultureInfo.InvariantCulture); if (request.CacheDuration > 0) { @@ -198,53 +197,62 @@ namespace Squidex.Areas.Api.Controllers.Assets }; } - private async Task ResizeAsync(IAssetEntity asset, Stream bodyStream, ResizeOptions resizeOptions, bool overwrite, CancellationToken ct) + private async Task ResizeAsync(IAssetEntity asset, Stream bodyStream, ResizeOptions resizeOptions, bool overwrite, + CancellationToken ct) { - var suffix = resizeOptions.ToString(); - using (Telemetry.Activities.StartActivity("Resize")) { - using (var sourceStream = GetTempStream()) + await using (var destinationStream = GetTempStream()) { - using (var destinationStream = GetTempStream()) - { - using (Telemetry.Activities.StartActivity("ResizeDownload")) - { - await assetFileStore.DownloadAsync(asset.AppId.Id, asset.Id, asset.FileVersion, null, sourceStream); - sourceStream.Position = 0; - } + // Do not use cancellation for the resize process because it is valuable to complete it. + await ResizeAsync(asset, resizeOptions, destinationStream, overwrite); - using (Telemetry.Activities.StartActivity("ResizeImage")) - { - try - { - await assetThumbnailGenerator.CreateThumbnailAsync(sourceStream, destinationStream, resizeOptions); - destinationStream.Position = 0; - } - catch - { - sourceStream.Position = 0; - await sourceStream.CopyToAsync(destinationStream); - } - } + await destinationStream.CopyToAsync(bodyStream, ct); + } + } + } - try - { - using (Telemetry.Activities.StartActivity("ResizeUpload")) - { - await assetFileStore.UploadAsync(asset.AppId.Id, asset.Id, asset.FileVersion, suffix, destinationStream, overwrite); - destinationStream.Position = 0; - } - } - catch (AssetAlreadyExistsException) - { - destinationStream.Position = 0; - } + private async Task ResizeAsync(IAssetEntity asset, ResizeOptions resizeOptions, FileStream stream, bool overwrite) + { +#pragma warning disable MA0040 // Flow the cancellation token + var suffix = resizeOptions.ToString(); + + await using (var sourceStream = GetTempStream()) + { + using (Telemetry.Activities.StartActivity("ResizeDownload")) + { + await assetFileStore.DownloadAsync(asset.AppId.Id, asset.Id, asset.FileVersion, null, sourceStream); + sourceStream.Position = 0; + } + + using (Telemetry.Activities.StartActivity("ResizeImage")) + { + try + { + await assetThumbnailGenerator.CreateThumbnailAsync(sourceStream, stream, resizeOptions); + stream.Position = 0; + } + catch + { + sourceStream.Position = 0; + await sourceStream.CopyToAsync(stream); + } + } - await destinationStream.CopyToAsync(bodyStream, ct); + try + { + using (Telemetry.Activities.StartActivity("ResizeUpload")) + { + await assetFileStore.UploadAsync(asset.AppId.Id, asset.Id, asset.FileVersion, suffix, stream, overwrite); + stream.Position = 0; } } + catch (AssetAlreadyExistsException) + { + stream.Position = 0; + } } +#pragma warning restore MA0040 // Flow the cancellation token } private static FileStream GetTempStream() diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs index 8fac2350a..f50a2c941 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -74,7 +75,7 @@ namespace Squidex.Areas.Api.Controllers.Assets { var tags = await tagService.GetTagsAsync(AppId, TagGroups.Assets); - Response.Headers[HeaderNames.ETag] = tags.Version.ToString(); + Response.Headers[HeaderNames.ETag] = tags.Version.ToString(CultureInfo.InvariantCulture); return Ok(tags); } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs b/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs index 30c50716d..bc5d9c120 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs @@ -51,7 +51,7 @@ namespace Squidex.Areas.Api.Controllers.Backups [AllowAnonymous] public async Task GetBackupContent(string app, DomainId id) { - var backup = await backupservice.GetBackupAsync(AppId, id); + var backup = await backupservice.GetBackupAsync(AppId, id, HttpContext.RequestAborted); if (backup == null || backup.Status != JobStatus.Completed) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs index 900fe0c45..a6e070fc1 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Globalization; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -60,7 +61,7 @@ namespace Squidex.Areas.Api.Controllers.Comments return CommentsDto.FromResult(result); }); - Response.Headers[HeaderNames.ETag] = result.Version.ToString(); + Response.Headers[HeaderNames.ETag] = result.Version.ToString(CultureInfo.InvariantCulture); return Ok(response); } 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 6d7860f49..908b9f3e6 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Comments/Notifications/UserNotificationsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Comments/Notifications/UserNotificationsController.cs @@ -5,6 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; +using System.Globalization; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -61,7 +63,7 @@ namespace Squidex.Areas.Api.Controllers.Comments.Notifications return CommentsDto.FromResult(result); }); - Response.Headers[HeaderNames.ETag] = result.Version.ToString(); + Response.Headers[HeaderNames.ETag] = result.Version.ToString(CultureInfo.InvariantCulture); return Ok(response); } @@ -96,7 +98,7 @@ namespace Squidex.Areas.Api.Controllers.Comments.Notifications private void CheckPermissions(DomainId userId) { - if (!string.Equals(userId.ToString(), User.OpenIdSubject())) + if (!string.Equals(userId.ToString(), User.OpenIdSubject(), StringComparison.Ordinal)) { throw new DomainForbiddenException(T.Get("comments.noPermissions")); } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentOpenApiController.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentOpenApiController.cs index a63ba3147..5573fe003 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentOpenApiController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentOpenApiController.cs @@ -62,7 +62,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [AllowAnonymous] public async Task GetOpenApi(string app) { - var schemas = await appProvider.GetSchemasAsync(AppId); + var schemas = await appProvider.GetSchemasAsync(AppId, HttpContext.RequestAborted); var openApiDocument = await schemasOpenApiGenerator.GenerateAsync(HttpContext, App, schemas); @@ -75,7 +75,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [AllowAnonymous] public async Task GetFlatOpenApi(string app) { - var schemas = await appProvider.GetSchemasAsync(AppId); + var schemas = await appProvider.GetSchemasAsync(AppId, HttpContext.RequestAborted); var openApiDocument = await schemasOpenApiGenerator.GenerateAsync(HttpContext, App, schemas, true); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/OperationBuilder.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/OperationBuilder.cs index 3b1bbb403..ed7d457b6 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/OperationBuilder.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/OperationBuilder.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Collections.Generic; +using System.Globalization; using NJsonSchema; using NSwag; using Squidex.Areas.Api.Config.OpenApi; @@ -108,7 +109,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator { var response = new OpenApiResponse { Description = description, Schema = schema }; - operation.Responses.Add(statusCode.ToString(), response); + operation.Responses.Add(statusCode.ToString(CultureInfo.InvariantCulture), response); return this; } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs index 22eda976d..dc665f9d6 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs @@ -73,7 +73,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator foreach (var schema in validSchemas) { - var components = await appProvider.GetComponentsAsync(schema); + var components = await appProvider.GetComponentsAsync(schema, httpContext.RequestAborted); GenerateSchemaOperations(builder.Schema(schema.SchemaDef, components, flat)); } @@ -224,8 +224,8 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator Title = $"Squidex Content API for '{appName}' App", Description = Resources.OpenApiContentDescription - .Replace("[REDOC_LINK_NORMAL]", urlGenerator.BuildUrl($"api/content/{app.Name}/docs")) - .Replace("[REDOC_LINK_SIMPLE]", urlGenerator.BuildUrl($"api/content/{app.Name}/docs/flat")) + .Replace("[REDOC_LINK_NORMAL]", urlGenerator.BuildUrl($"api/content/{app.Name}/docs"), StringComparison.Ordinal) + .Replace("[REDOC_LINK_SIMPLE]", urlGenerator.BuildUrl($"api/content/{app.Name}/docs/flat"), StringComparison.Ordinal) }, SchemaType = SchemaType.OpenApi3 }; diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs index decb47ffd..cbf20eda0 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs @@ -186,7 +186,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models AddDeleteLink($"cancel", resources.Url(x => nameof(x.DeleteContentStatus), values)); } - if (content.IsSingleton == false && resources.CanDeleteContent(schema)) + if (!content.IsSingleton && resources.CanDeleteContent(schema)) { AddDeleteLink("delete", resources.Url(x => nameof(x.DeleteContent), values)); } diff --git a/backend/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs b/backend/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs index c55aad29e..4e1763aae 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs @@ -47,7 +47,7 @@ namespace Squidex.Areas.Api.Controllers.History [ApiCosts(0.1)] public async Task GetHistory(string app, string channel) { - var events = await historyService.QueryByChannelAsync(AppId, channel, 100); + var events = await historyService.QueryByChannelAsync(AppId, channel, 100, HttpContext.RequestAborted); var response = events.Select(HistoryEventDto.FromHistoryEvent).Where(x => x.Message != null).ToArray(); diff --git a/backend/src/Squidex/Areas/Api/Controllers/News/NewsController.cs b/backend/src/Squidex/Areas/Api/Controllers/News/NewsController.cs index be965fc2e..074cfb3ce 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/News/NewsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/News/NewsController.cs @@ -42,7 +42,7 @@ namespace Squidex.Areas.Api.Controllers.News [ApiPermission] public async Task GetNews([FromQuery] int version = 0) { - var features = await featuresService.GetFeaturesAsync(version); + var features = await featuresService.GetFeaturesAsync(version, HttpContext.RequestAborted); return Ok(features); } diff --git a/backend/src/Squidex/Areas/Api/Controllers/News/Service/FeaturesService.cs b/backend/src/Squidex/Areas/Api/Controllers/News/Service/FeaturesService.cs index 222aeb988..dc52f6a27 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/News/Service/FeaturesService.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/News/Service/FeaturesService.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Options; using Squidex.Areas.Api.Controllers.News.Models; @@ -42,7 +43,8 @@ namespace Squidex.Areas.Api.Controllers.News.Service } } - public async Task GetFeaturesAsync(int version = 0) + public async Task GetFeaturesAsync(int version = 0, + CancellationToken ct = default) { var result = new FeaturesDto { @@ -58,7 +60,7 @@ namespace Squidex.Areas.Api.Controllers.News.Service Filter = $"data/version/iv ge {FeatureVersion}" }; - var features = await client.GetAsync(query, flatten); + var features = await client.GetAsync(query, flatten, ct); result.Features = features.Items.Select(x => x.Data).ToList(); } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs index e8f459610..742c74f6c 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs @@ -97,7 +97,7 @@ namespace Squidex.Areas.Api.Controllers.Rules [ApiCosts(1)] public async Task GetRules(string app) { - var rules = await ruleQuery.QueryAsync(Context); + var rules = await ruleQuery.QueryAsync(Context, HttpContext.RequestAborted); var response = Deferred.AsyncResponse(() => { @@ -145,7 +145,7 @@ namespace Squidex.Areas.Api.Controllers.Rules [ApiCosts(1)] public async Task DeleteRuleRun(string app) { - await ruleRunnerService.CancelAsync(App.Id); + await ruleRunnerService.CancelAsync(App.Id, HttpContext.RequestAborted); return NoContent(); } @@ -259,7 +259,7 @@ namespace Squidex.Areas.Api.Controllers.Rules [ApiCosts(1)] public async Task PutRuleRun(string app, DomainId id, [FromQuery] bool fromSnapshots = false) { - await ruleRunnerService.RunAsync(App.Id, id, fromSnapshots); + await ruleRunnerService.RunAsync(App.Id, id, fromSnapshots, HttpContext.RequestAborted); return NoContent(); } @@ -279,7 +279,7 @@ namespace Squidex.Areas.Api.Controllers.Rules [ApiCosts(1)] public async Task DeleteRuleEvents(string app, DomainId id) { - await ruleEventsRepository.CancelByRuleAsync(id); + await ruleEventsRepository.CancelByRuleAsync(id, HttpContext.RequestAborted); return NoContent(); } @@ -300,7 +300,7 @@ namespace Squidex.Areas.Api.Controllers.Rules [ApiCosts(5)] public async Task Simulate(string app, DomainId id) { - var rule = await appProvider.GetRuleAsync(AppId, id); + var rule = await appProvider.GetRuleAsync(AppId, id, HttpContext.RequestAborted); if (rule == null) { @@ -352,7 +352,7 @@ namespace Squidex.Areas.Api.Controllers.Rules [ApiCosts(0)] public async Task GetEvents(string app, [FromQuery] DomainId? ruleId = null, [FromQuery] int skip = 0, [FromQuery] int take = 20) { - var ruleEvents = await ruleEventsRepository.QueryByAppAsync(AppId, ruleId, skip, take); + var ruleEvents = await ruleEventsRepository.QueryByAppAsync(AppId, ruleId, skip, take, HttpContext.RequestAborted); var response = RuleEventsDto.FromRuleEvents(ruleEvents, Resources, ruleId); @@ -374,14 +374,14 @@ namespace Squidex.Areas.Api.Controllers.Rules [ApiCosts(0)] public async Task PutEvent(string app, DomainId id) { - var ruleEvent = await ruleEventsRepository.FindAsync(id); + var ruleEvent = await ruleEventsRepository.FindAsync(id, HttpContext.RequestAborted); if (ruleEvent == null) { return NotFound(); } - await ruleEventsRepository.EnqueueAsync(id, SystemClock.Instance.GetCurrentInstant()); + await ruleEventsRepository.EnqueueAsync(id, SystemClock.Instance.GetCurrentInstant(), HttpContext.RequestAborted); return NoContent(); } @@ -400,7 +400,7 @@ namespace Squidex.Areas.Api.Controllers.Rules [ApiCosts(1)] public async Task DeleteEvents(string app) { - await ruleEventsRepository.CancelByAppAsync(App.Id); + await ruleEventsRepository.CancelByAppAsync(App.Id, HttpContext.RequestAborted); return NoContent(); } @@ -420,14 +420,14 @@ namespace Squidex.Areas.Api.Controllers.Rules [ApiCosts(0)] public async Task DeleteEvent(string app, DomainId id) { - var ruleEvent = await ruleEventsRepository.FindAsync(id); + var ruleEvent = await ruleEventsRepository.FindAsync(id, HttpContext.RequestAborted); if (ruleEvent == null) { return NotFound(); } - await ruleEventsRepository.CancelByRuleAsync(id); + await ruleEventsRepository.CancelByRuleAsync(id, HttpContext.RequestAborted); return NoContent(); } @@ -477,7 +477,7 @@ namespace Squidex.Areas.Api.Controllers.Rules { var context = await CommandBus.PublishAsync(command); - var runningRuleId = await ruleRunnerService.GetRunningRuleIdAsync(Context.App.Id); + var runningRuleId = await ruleRunnerService.GetRunningRuleIdAsync(Context.App.Id, HttpContext.RequestAborted); var result = context.Result(); var response = RuleDto.FromRule(result, runningRuleId == null, ruleRunnerService, Resources); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs index d8a799cbe..07178cd9c 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs @@ -53,7 +53,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas [ApiCosts(0)] public async Task GetSchemas(string app) { - var schemas = await appProvider.GetSchemasAsync(AppId); + var schemas = await appProvider.GetSchemasAsync(AppId, HttpContext.RequestAborted); var response = Deferred.Response(() => { @@ -350,11 +350,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas { var schemaId = DomainId.Create(guid); - return appProvider.GetSchemaAsync(AppId, schemaId); + return appProvider.GetSchemaAsync(AppId, schemaId, ct: HttpContext.RequestAborted); } else { - return appProvider.GetSchemaAsync(AppId, schema); + return appProvider.GetSchemaAsync(AppId, schema, ct: HttpContext.RequestAborted); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs index 1d3e68f90..d60a9c171 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs @@ -104,7 +104,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics return BadRequest(); } - var (summary, details) = await usageTracker.QueryAsync(AppId.ToString(), fromDate.Date, toDate.Date); + var (summary, details) = await usageTracker.QueryAsync(AppId.ToString(), fromDate.Date, toDate.Date, HttpContext.RequestAborted); var (plan, _) = appPlansProvider.GetPlanForApp(App); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs b/backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs index 8625bc851..9016bd16c 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs @@ -43,7 +43,7 @@ namespace Squidex.Areas.Api.Controllers.Users { AvatarBytes = new byte[resourceStream!.Length]; - resourceStream.Read(AvatarBytes, 0, AvatarBytes.Length); + _ = resourceStream.Read(AvatarBytes, 0, AvatarBytes.Length); } } @@ -97,7 +97,7 @@ namespace Squidex.Areas.Api.Controllers.Users { try { - var users = await userResolver.QueryByEmailAsync(query); + var users = await userResolver.QueryByEmailAsync(query, HttpContext.RequestAborted); var response = users.Where(x => !x.Claims.IsHidden()).Select(x => UserDto.FromUser(x, Resources)).ToArray(); @@ -129,7 +129,7 @@ namespace Squidex.Areas.Api.Controllers.Users { try { - var entity = await userResolver.FindByIdAsync(id); + var entity = await userResolver.FindByIdAsync(id, HttpContext.RequestAborted); if (entity != null) { @@ -164,7 +164,7 @@ namespace Squidex.Areas.Api.Controllers.Users { try { - var entity = await userResolver.FindByIdAsync(id); + var entity = await userResolver.FindByIdAsync(id, HttpContext.RequestAborted); if (entity != null) { @@ -191,7 +191,7 @@ namespace Squidex.Areas.Api.Controllers.Users if (!string.IsNullOrWhiteSpace(url)) { - var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); + var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, HttpContext.RequestAborted); if (response.IsSuccessStatusCode) { @@ -199,7 +199,7 @@ namespace Squidex.Areas.Api.Controllers.Users var etag = response.Headers.ETag; - var result = new FileStreamResult(await response.Content.ReadAsStreamAsync(), contentType); + var result = new FileStreamResult(await response.Content.ReadAsStreamAsync(HttpContext.RequestAborted), contentType); if (!string.IsNullOrWhiteSpace(etag?.Tag)) { diff --git a/backend/src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs b/backend/src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs index 5299cce05..f885cc01a 100644 --- a/backend/src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs +++ b/backend/src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs @@ -43,7 +43,7 @@ namespace Squidex.Areas.Frontend.Middlewares { if (httpContext.Request.PathBase != null) { - html = html.Replace("", $""); + html = html.Replace("", $"", StringComparison.OrdinalIgnoreCase); } return html; @@ -76,7 +76,7 @@ namespace Squidex.Areas.Frontend.Middlewares var texts = GetText(CultureInfo.CurrentUICulture.Name); - html = html.Replace("", $"\n"); + html = html.Replace("", $"\n", StringComparison.OrdinalIgnoreCase); } return html; diff --git a/backend/src/Squidex/Areas/Frontend/Middlewares/IndexMiddleware.cs b/backend/src/Squidex/Areas/Frontend/Middlewares/IndexMiddleware.cs index 7d65db6a9..495b6bdf4 100644 --- a/backend/src/Squidex/Areas/Frontend/Middlewares/IndexMiddleware.cs +++ b/backend/src/Squidex/Areas/Frontend/Middlewares/IndexMiddleware.cs @@ -48,7 +48,7 @@ namespace Squidex.Areas.Frontend.Middlewares context.Response.ContentLength = Encoding.UTF8.GetByteCount(html); context.Response.Body = responseBody; - await context.Response.WriteAsync(html); + await context.Response.WriteAsync(html, context.RequestAborted); } } else diff --git a/backend/src/Squidex/Areas/Frontend/Middlewares/NotifoMiddleware.cs b/backend/src/Squidex/Areas/Frontend/Middlewares/NotifoMiddleware.cs index 72cd287a0..b9e0312c7 100644 --- a/backend/src/Squidex/Areas/Frontend/Middlewares/NotifoMiddleware.cs +++ b/backend/src/Squidex/Areas/Frontend/Middlewares/NotifoMiddleware.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; @@ -27,13 +28,13 @@ namespace Squidex.Areas.Frontend.Middlewares public async Task InvokeAsync(HttpContext context) { - if (context.Request.Path.Equals("/notifo-sw.js") && workerUrl != null) + if (context.Request.Path.Equals("/notifo-sw.js", StringComparison.Ordinal) && workerUrl != null) { context.Response.Headers[HeaderNames.ContentType] = "text/javascript"; var script = $"importScripts('{workerUrl}')"; - await context.Response.WriteAsync(script); + await context.Response.WriteAsync(script, context.RequestAborted); } else { @@ -48,7 +49,7 @@ namespace Squidex.Areas.Frontend.Middlewares return null; } - if (options.ApiUrl.Contains("localhost:5002")) + if (options.ApiUrl.Contains("localhost:5002", StringComparison.Ordinal)) { return "https://localhost:3002/notifo-sdk-worker.js"; } diff --git a/backend/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs b/backend/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs index cdf5677b3..d417462a9 100644 --- a/backend/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs +++ b/backend/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs @@ -33,17 +33,17 @@ namespace Squidex.Areas.Frontend.Middlewares using (var client = new HttpClient(handler)) { - var result = await client.GetAsync(WebpackUrl); + var result = await client.GetAsync(WebpackUrl, context.RequestAborted); context.Response.StatusCode = (int)result.StatusCode; if (result.IsSuccessStatusCode) { - var html = await result.Content.ReadAsStringAsync(); + var html = await result.Content.ReadAsStringAsync(context.RequestAborted); html = html.AdjustBase(context); - await context.Response.WriteAsync(html); + await context.Response.WriteAsync(html, context.RequestAborted); } } } diff --git a/backend/src/Squidex/Areas/Frontend/Startup.cs b/backend/src/Squidex/Areas/Frontend/Startup.cs index 95e287b9a..a61182d1b 100644 --- a/backend/src/Squidex/Areas/Frontend/Startup.cs +++ b/backend/src/Squidex/Areas/Frontend/Startup.cs @@ -52,7 +52,7 @@ namespace Squidex.Areas.Frontend return next(); }); - app.UseWhen(x => x.Request.Path.StartsWithSegments(indexFile), builder => + app.UseWhen(x => x.Request.Path.StartsWithSegments(indexFile, StringComparison.Ordinal), builder => { builder.UseMiddleware(); }); diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/ApplicationManager.cs b/backend/src/Squidex/Areas/IdentityServer/Config/ApplicationManager.cs index 40be5a71f..6f25ae3ff 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Config/ApplicationManager.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Config/ApplicationManager.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -25,9 +26,10 @@ namespace Squidex.Areas.IdentityServer.Config { } - protected override ValueTask ValidateClientSecretAsync(string secret, string comparand, CancellationToken cancellationToken = default) + protected override ValueTask ValidateClientSecretAsync(string secret, string comparand, + CancellationToken cancellationToken = default) { - return new ValueTask(string.Equals(secret, comparand)); + return new ValueTask(string.Equals(secret, comparand, StringComparison.Ordinal)); } } } diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/CreateAdminInitializer.cs b/backend/src/Squidex/Areas/IdentityServer/Config/CreateAdminInitializer.cs index 23f6a7891..e9be6ad74 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Config/CreateAdminInitializer.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Config/CreateAdminInitializer.cs @@ -36,7 +36,8 @@ namespace Squidex.Areas.IdentityServer.Config this.identityOptions = identityOptions.Value; } - public async Task InitializeAsync(CancellationToken ct) + public async Task InitializeAsync( + CancellationToken ct) { IdentityModelEventSource.ShowPII = identityOptions.ShowPII; diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/DynamicApplicationStore.cs b/backend/src/Squidex/Areas/IdentityServer/Config/DynamicApplicationStore.cs index ccb1ba2f5..f0c351733 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Config/DynamicApplicationStore.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Config/DynamicApplicationStore.cs @@ -36,7 +36,8 @@ namespace Squidex.Areas.IdentityServer.Config this.serviceProvider = serviceProvider; } - public override async ValueTask FindByIdAsync(string identifier, CancellationToken cancellationToken) + public override async ValueTask FindByIdAsync(string identifier, + CancellationToken cancellationToken) { var application = await base.FindByIdAsync(identifier, cancellationToken); @@ -48,7 +49,8 @@ namespace Squidex.Areas.IdentityServer.Config return application; } - public override async ValueTask FindByClientIdAsync(string identifier, CancellationToken cancellationToken) + public override async ValueTask FindByClientIdAsync(string identifier, + CancellationToken cancellationToken) { var application = await base.FindByClientIdAsync(identifier, cancellationToken); diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/TokenStoreInitializer.cs b/backend/src/Squidex/Areas/IdentityServer/Config/TokenStoreInitializer.cs index 03681d100..64455418f 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Config/TokenStoreInitializer.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Config/TokenStoreInitializer.cs @@ -33,7 +33,8 @@ namespace Squidex.Areas.IdentityServer.Config this.serviceProvider = serviceProvider; } - public async Task InitializeAsync(CancellationToken ct) + public async Task InitializeAsync( + CancellationToken ct) { await SetupIndexAsync(ct); @@ -45,7 +46,8 @@ namespace Squidex.Areas.IdentityServer.Config }); } - private async Task PruneAsync(CancellationToken ct) + private async Task PruneAsync( + CancellationToken ct) { using (var scope = serviceProvider.CreateScope()) { @@ -55,7 +57,8 @@ namespace Squidex.Areas.IdentityServer.Config } } - private async Task SetupIndexAsync(CancellationToken ct) + private async Task SetupIndexAsync( + CancellationToken ct) { using (var scope = serviceProvider.CreateScope()) { @@ -71,7 +74,8 @@ namespace Squidex.Areas.IdentityServer.Config } } - public async Task ReleaseAsync(CancellationToken ct) + public async Task ReleaseAsync( + CancellationToken ct) { if (timer != null) { diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs index 3a421bc1c..96e68b3ff 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs @@ -253,7 +253,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account if (user != null) { - var update = CreateUserValues(externalLogin, email, user: user); + var update = CreateUserValues(externalLogin, email, user); await userService.UpdateAsync(user.Id, update); } diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Connect/ConnectController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Connect/ConnectController.cs index 891a2bc49..01e9c186a 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Connect/ConnectController.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Connect/ConnectController.cs @@ -102,7 +102,7 @@ namespace Notifo.Areas.Account.Controllers throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); } - var application = await applicationManager.FindByClientIdAsync(request.ClientId); + var application = await applicationManager.FindByClientIdAsync(request.ClientId, HttpContext.RequestAborted); if (application == null) { @@ -191,7 +191,7 @@ namespace Notifo.Areas.Account.Controllers Destinations.IdentityToken); } - var properties = await applicationManager.GetPropertiesAsync(application); + var properties = await applicationManager.GetPropertiesAsync(application, HttpContext.RequestAborted); foreach (var claim in properties.Claims()) { @@ -208,7 +208,7 @@ namespace Notifo.Areas.Account.Controllers var scopes = request.GetScopes(); principal.SetScopes(scopes); - principal.SetResources(await scopeManager.ListResourcesAsync(scopes).ToListAsync()); + principal.SetResources(await scopeManager.ListResourcesAsync(scopes, HttpContext.RequestAborted).ToListAsync(HttpContext.RequestAborted)); foreach (var claim in principal.Claims) { diff --git a/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs index f128bf529..993e93bb8 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs @@ -170,7 +170,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Profile throw new ValidationException(T.Get("validation.notAnImage")); } - await userPictureStore.UploadAsync(id, thumbnailStream); + await userPictureStore.UploadAsync(id, thumbnailStream, HttpContext.RequestAborted); } var update = new UserValues { PictureUrl = SquidexClaimTypes.PictureUrlStore }; diff --git a/backend/src/Squidex/Config/Domain/AppsServices.cs b/backend/src/Squidex/Config/Domain/AppsServices.cs index e60098e36..1c5e66ea9 100644 --- a/backend/src/Squidex/Config/Domain/AppsServices.cs +++ b/backend/src/Squidex/Config/Domain/AppsServices.cs @@ -1,4 +1,4 @@ -// ========================================================================== +// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschraenkt) @@ -6,6 +6,7 @@ // ========================================================================== using System.Collections.Generic; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Squidex.Areas.Api.Controllers.UI; @@ -13,22 +14,36 @@ using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.DomainObject; +using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.History; using Squidex.Domain.Apps.Entities.Search; using Squidex.Infrastructure.Collections; +using Squidex.Infrastructure.EventSourcing; namespace Squidex.Config.Domain { public static class AppsServices { - public static void AddSquidexApps(this IServiceCollection services) + public static void AddSquidexApps(this IServiceCollection services, IConfiguration config) { + if (config.GetValue("apps:deletePermanent")) + { + services.AddSingletonAs() + .As(); + } + services.AddTransientAs() .AsSelf(); services.AddSingletonAs() .AsSelf(); + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); @@ -39,7 +54,7 @@ namespace Squidex.Config.Domain .As(); services.AddSingletonAs() - .As(); + .As().As(); services.AddSingletonAs() .As(); @@ -72,4 +87,4 @@ namespace Squidex.Config.Domain }); } } -} \ No newline at end of file +} diff --git a/backend/src/Squidex/Config/Domain/AssetServices.cs b/backend/src/Squidex/Config/Domain/AssetServices.cs index c96964949..15ff09e6a 100644 --- a/backend/src/Squidex/Config/Domain/AssetServices.cs +++ b/backend/src/Squidex/Config/Domain/AssetServices.cs @@ -14,6 +14,7 @@ using MongoDB.Driver; using MongoDB.Driver.GridFS; using Squidex.Assets; using Squidex.Assets.ImageSharp; +using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets.DomainObject; using Squidex.Domain.Apps.Entities.Assets.Queries; @@ -56,6 +57,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .AsSelf(); + services.AddTransientAs() + .As(); + services.AddTransientAs() .As(); @@ -63,7 +67,7 @@ namespace Squidex.Config.Domain .As(); services.AddSingletonAs() - .As(); + .As().As(); services.AddSingletonAs() .As(); @@ -75,7 +79,7 @@ namespace Squidex.Config.Domain .As(); services.AddSingletonAs() - .As().As(); + .As().As().As(); services.AddSingletonAs() .As(); @@ -108,22 +112,28 @@ namespace Squidex.Config.Domain }, ["GoogleCloud"] = () => { - var bucketName = config.GetRequiredValue("assetStore:googleCloud:bucket"); + var options = new GoogleCloudAssetOptions + { + BucketName = config.GetRequiredValue("assetStore:googleCloud:bucket") + }; - services.AddSingletonAs(c => new GoogleCloudAssetStore(bucketName)) + services.AddSingletonAs(c => new GoogleCloudAssetStore(options)) .As(); }, ["AzureBlob"] = () => { - var connectionString = config.GetRequiredValue("assetStore:azureBlob:connectionString"); - var containerName = config.GetRequiredValue("assetStore:azureBlob:containerName"); + var options = new AzureBlobAssetOptions + { + ConnectionString = config.GetRequiredValue("assetStore:azureBlob:connectionString"), + ContainerName = config.GetRequiredValue("assetStore:azureBlob:containerName") + }; - services.AddSingletonAs(c => new AzureBlobAssetStore(connectionString, containerName)) + services.AddSingletonAs(c => new AzureBlobAssetStore(options)) .As(); }, ["AmazonS3"] = () => { - var amazonS3Options = config.GetSection("assetStore:amazonS3").Get() ?? new (); + var amazonS3Options = config.GetSection("assetStore:amazonS3").Get() ?? new (); services.AddSingletonAs(c => new AmazonS3AssetStore(amazonS3Options)) .As(); @@ -156,13 +166,16 @@ namespace Squidex.Config.Domain var username = config.GetRequiredValue("assetStore:ftp:username"); var password = config.GetRequiredValue("assetStore:ftp:password"); - var path = config.GetOptionalValue("assetStore:ftp:path", "/"); + var options = new FTPAssetOptions + { + Path = config.GetOptionalValue("assetStore:ftp:path", "/") + }; services.AddSingletonAs(c => { var factory = new Func(() => new FtpClient(serverHost, serverPort, username, password)); - return new FTPAssetStore(factory, path, c.GetRequiredService>()); + return new FTPAssetStore(factory, options, c.GetRequiredService>()); }) .As(); } diff --git a/backend/src/Squidex/Config/Domain/BackupsServices.cs b/backend/src/Squidex/Config/Domain/BackupsServices.cs index 31b650c2c..079aa1f93 100644 --- a/backend/src/Squidex/Config/Domain/BackupsServices.cs +++ b/backend/src/Squidex/Config/Domain/BackupsServices.cs @@ -1,4 +1,4 @@ -// ========================================================================== +// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschraenkt) @@ -6,6 +6,7 @@ // ========================================================================== using Microsoft.Extensions.DependencyInjection; +using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Backup; @@ -26,7 +27,7 @@ namespace Squidex.Config.Domain .As(); services.AddTransientAs() - .As(); + .As().As(); services.AddTransientAs() .As(); @@ -44,4 +45,4 @@ namespace Squidex.Config.Domain .As(); } } -} \ No newline at end of file +} diff --git a/backend/src/Squidex/Config/Domain/ContentsServices.cs b/backend/src/Squidex/Config/Domain/ContentsServices.cs index 8797414cf..8c31b4ffd 100644 --- a/backend/src/Squidex/Config/Domain/ContentsServices.cs +++ b/backend/src/Squidex/Config/Domain/ContentsServices.cs @@ -1,4 +1,4 @@ -// ========================================================================== +// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschraenkt) @@ -9,7 +9,9 @@ using System; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Squidex.Domain.Apps.Core.ValidateContent; +using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Contents.Counter; using Squidex.Domain.Apps.Entities.Contents.DomainObject; using Squidex.Domain.Apps.Entities.Contents.Queries; using Squidex.Domain.Apps.Entities.Contents.Queries.Steps; @@ -39,6 +41,9 @@ namespace Squidex.Config.Domain services.AddTransientAs() .AsSelf(); + services.AddTransientAs() + .As(); + services.AddSingletonAs() .As(); @@ -107,4 +112,4 @@ namespace Squidex.Config.Domain }); } } -} \ No newline at end of file +} diff --git a/backend/src/Squidex/Config/Domain/LoggingServices.cs b/backend/src/Squidex/Config/Domain/LoggingServices.cs index ad0f8bdd8..8c319497f 100644 --- a/backend/src/Squidex/Config/Domain/LoggingServices.cs +++ b/backend/src/Squidex/Config/Domain/LoggingServices.cs @@ -11,6 +11,7 @@ using System; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Infrastructure.Log; using Squidex.Log; @@ -48,7 +49,7 @@ namespace Squidex.Config.Domain .As(); services.AddSingletonAs() - .As(); + .As().As(); services.AddSingletonAs() .AsOptional(); diff --git a/backend/src/Squidex/Config/Domain/MigrationServices.cs b/backend/src/Squidex/Config/Domain/MigrationServices.cs index 520bafceb..1bb221cdb 100644 --- a/backend/src/Squidex/Config/Domain/MigrationServices.cs +++ b/backend/src/Squidex/Config/Domain/MigrationServices.cs @@ -44,9 +44,6 @@ namespace Squidex.Config.Domain services.AddTransientAs() .As(); - services.AddTransientAs() - .As(); - services.AddTransientAs() .As(); @@ -56,6 +53,12 @@ namespace Squidex.Config.Domain services.AddTransientAs() .As(); + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + services.AddTransientAs() .As(); diff --git a/backend/src/Squidex/Config/Domain/SerializationInitializer.cs b/backend/src/Squidex/Config/Domain/SerializationInitializer.cs index 2763c31fa..b72f0e1af 100644 --- a/backend/src/Squidex/Config/Domain/SerializationInitializer.cs +++ b/backend/src/Squidex/Config/Domain/SerializationInitializer.cs @@ -34,7 +34,8 @@ namespace Squidex.Config.Domain this.ruleRegistry = ruleRegistry; } - public Task InitializeAsync(CancellationToken ct = default) + public Task InitializeAsync( + CancellationToken ct) { SetupBson(); SetupOrleans(); diff --git a/backend/src/Squidex/Config/Domain/StoreServices.cs b/backend/src/Squidex/Config/Domain/StoreServices.cs index 5bdbfff97..e19e7ef21 100644 --- a/backend/src/Squidex/Config/Domain/StoreServices.cs +++ b/backend/src/Squidex/Config/Domain/StoreServices.cs @@ -11,6 +11,9 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Migrations.Migrations.MongoDb; using MongoDB.Driver; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Apps.DomainObject; +using Squidex.Domain.Apps.Entities.Apps.Repositories; using Squidex.Domain.Apps.Entities.Assets.DomainObject; using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Contents.DomainObject; @@ -18,14 +21,18 @@ using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Domain.Apps.Entities.Contents.Text; using Squidex.Domain.Apps.Entities.Contents.Text.State; using Squidex.Domain.Apps.Entities.History.Repositories; +using Squidex.Domain.Apps.Entities.MongoDb.Apps; using Squidex.Domain.Apps.Entities.MongoDb.Assets; using Squidex.Domain.Apps.Entities.MongoDb.Contents; using Squidex.Domain.Apps.Entities.MongoDb.FullText; using Squidex.Domain.Apps.Entities.MongoDb.History; using Squidex.Domain.Apps.Entities.MongoDb.Rules; using Squidex.Domain.Apps.Entities.MongoDb.Schemas; +using Squidex.Domain.Apps.Entities.Rules.DomainObject; using Squidex.Domain.Apps.Entities.Rules.Repositories; using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.DomainObject; +using Squidex.Domain.Apps.Entities.Schemas.Repositories; using Squidex.Domain.Users; using Squidex.Domain.Users.InMemory; using Squidex.Domain.Users.MongoDb; @@ -75,7 +82,7 @@ namespace Squidex.Config.Domain .As(); services.AddSingletonAs(c => ActivatorUtilities.CreateInstance(c, GetDatabase(c, mongoContentDatabaseName))) - .As().As>(); + .As().As>().As(); services.AddTransientAs() .As(); @@ -99,10 +106,10 @@ namespace Squidex.Config.Domain .As(); services.AddSingletonAs() - .As(); + .As().As(); services.AddSingletonAs() - .As(); + .As().As(); services.AddSingletonAs() .As>(); @@ -111,19 +118,28 @@ namespace Squidex.Config.Domain .As>().As(); services.AddSingletonAs() - .As().As>(); + .As().As>().As(); services.AddSingletonAs() - .As().As>(); + .As().As>().As(); + + services.AddSingletonAs() + .As().As>().As(); + + services.AddSingletonAs() + .As().As>().As(); + + services.AddSingletonAs() + .As().As>().As(); services.AddSingletonAs() - .AsOptional().As(); + .AsOptional().As().As(); services.AddSingletonAs() - .AsOptional(); + .AsOptional().As(); services.AddSingletonAs() - .As(); + .As().As(); services.AddOpenIddict() .AddCore(builder => diff --git a/backend/src/Squidex/Config/Startup/LogConfigurationHost.cs b/backend/src/Squidex/Config/Startup/LogConfigurationHost.cs index 980efc0a9..df8c0a614 100644 --- a/backend/src/Squidex/Config/Startup/LogConfigurationHost.cs +++ b/backend/src/Squidex/Config/Startup/LogConfigurationHost.cs @@ -28,7 +28,8 @@ namespace Squidex.Config.Startup this.log = log; } - public Task StartAsync(CancellationToken cancellationToken) + public Task StartAsync( + CancellationToken cancellationToken) { log.LogInformation(w => w .WriteProperty("message", "Application started") @@ -50,7 +51,8 @@ namespace Squidex.Config.Startup return Task.CompletedTask; } - public Task StopAsync(CancellationToken cancellationToken) + public Task StopAsync( + CancellationToken cancellationToken) { return Task.CompletedTask; } diff --git a/backend/src/Squidex/Config/Startup/MigrationRebuilderHost.cs b/backend/src/Squidex/Config/Startup/MigrationRebuilderHost.cs index c23fb052f..fc4ad188f 100644 --- a/backend/src/Squidex/Config/Startup/MigrationRebuilderHost.cs +++ b/backend/src/Squidex/Config/Startup/MigrationRebuilderHost.cs @@ -21,12 +21,14 @@ namespace Squidex.Config.Startup this.rebuildRunner = rebuildRunner; } - public Task StartAsync(CancellationToken cancellationToken) + public Task StartAsync( + CancellationToken cancellationToken) { return rebuildRunner.RunAsync(cancellationToken); } - public Task StopAsync(CancellationToken cancellationToken) + public Task StopAsync( + CancellationToken cancellationToken) { return Task.CompletedTask; } diff --git a/backend/src/Squidex/Config/Startup/MigratorHost.cs b/backend/src/Squidex/Config/Startup/MigratorHost.cs index 8cd31f20f..61e6f7813 100644 --- a/backend/src/Squidex/Config/Startup/MigratorHost.cs +++ b/backend/src/Squidex/Config/Startup/MigratorHost.cs @@ -21,12 +21,14 @@ namespace Squidex.Config.Startup this.migrator = migrator; } - public Task StartAsync(CancellationToken cancellationToken) + public Task StartAsync( + CancellationToken cancellationToken) { return migrator.MigrateAsync(cancellationToken); } - public Task StopAsync(CancellationToken cancellationToken) + public Task StopAsync( + CancellationToken cancellationToken) { return Task.CompletedTask; } diff --git a/backend/src/Squidex/Pipeline/Robots/RobotsTxtMiddleware.cs b/backend/src/Squidex/Pipeline/Robots/RobotsTxtMiddleware.cs index 163c782e2..78964a778 100644 --- a/backend/src/Squidex/Pipeline/Robots/RobotsTxtMiddleware.cs +++ b/backend/src/Squidex/Pipeline/Robots/RobotsTxtMiddleware.cs @@ -29,7 +29,7 @@ namespace Squidex.Pipeline.Robots context.Response.ContentType = "text/plain"; context.Response.StatusCode = 200; - await context.Response.WriteAsync(text); + await context.Response.WriteAsync(text, context.RequestAborted); } else { diff --git a/backend/src/Squidex/Pipeline/Squid/SquidMiddleware.cs b/backend/src/Squidex/Pipeline/Squid/SquidMiddleware.cs index 620baf2fd..83272254e 100644 --- a/backend/src/Squidex/Pipeline/Squid/SquidMiddleware.cs +++ b/backend/src/Squidex/Pipeline/Squid/SquidMiddleware.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; using System.IO; using System.Text; @@ -73,17 +74,17 @@ namespace Squidex.Pipeline.Squid var (l1, l2, l3) = SplitText(text); - svg = svg.Replace("{{TITLE}}", title.ToUpperInvariant()); - svg = svg.Replace("{{TEXT1}}", l1); - svg = svg.Replace("{{TEXT2}}", l2); - svg = svg.Replace("{{TEXT3}}", l3); - svg = svg.Replace("[COLOR]", background); + svg = svg.Replace("{{TITLE}}", title.ToUpperInvariant(), StringComparison.Ordinal); + svg = svg.Replace("{{TEXT1}}", l1, StringComparison.Ordinal); + svg = svg.Replace("{{TEXT2}}", l2, StringComparison.Ordinal); + svg = svg.Replace("{{TEXT3}}", l3, StringComparison.Ordinal); + svg = svg.Replace("[COLOR]", background, StringComparison.Ordinal); context.Response.StatusCode = 200; context.Response.ContentType = "image/svg+xml"; context.Response.Headers["Cache-Control"] = "public, max-age=604800"; - await context.Response.WriteAsync(svg); + await context.Response.WriteAsync(svg, context.RequestAborted); } private static (string, string, string) SplitText(string text) diff --git a/backend/src/Squidex/Squidex.csproj b/backend/src/Squidex/Squidex.csproj index 7f91d0610..e2ca33f1a 100644 --- a/backend/src/Squidex/Squidex.csproj +++ b/backend/src/Squidex/Squidex.csproj @@ -37,6 +37,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -64,14 +68,14 @@ - - - - - + + + + + - - + + diff --git a/backend/src/Squidex/Startup.cs b/backend/src/Squidex/Startup.cs index 3c34e1c91..680813aca 100644 --- a/backend/src/Squidex/Startup.cs +++ b/backend/src/Squidex/Startup.cs @@ -40,7 +40,7 @@ namespace Squidex services.AddSquidexMvcWithPlugins(config); - services.AddSquidexApps(); + services.AddSquidexApps(config); services.AddSquidexAssetInfrastructure(config); services.AddSquidexAssets(config); services.AddSquidexAuthentication(config); diff --git a/backend/src/Squidex/appsettings.json b/backend/src/Squidex/appsettings.json index f5c15060b..8c2fa8835 100644 --- a/backend/src/Squidex/appsettings.json +++ b/backend/src/Squidex/appsettings.json @@ -522,10 +522,7 @@ "rules": false, // Set to true to rebuild schemas. - "schemas": false, - - // Set to true to rebuild indexes. - "indexes": false + "schemas": false }, // A list of configuration values that should be exposed from the info endpoint and in the UI. diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs index 37750775b..5393ee56a 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; using System.Linq; using FluentAssertions; @@ -166,8 +167,8 @@ namespace Squidex.Domain.Apps.Core.Model.Apps foreach (var permission in result.Permissions) { - Assert.StartsWith("squidex.apps.app.", permission.Id); - Assert.DoesNotContain("{app}", permission.Id); + Assert.StartsWith("squidex.apps.app.", permission.Id, StringComparison.Ordinal); + Assert.DoesNotContain("{app}", permission.Id, StringComparison.Ordinal); } Assert.Equal(permissionCount, result!.Permissions.Count); 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 ee35f2759..c1b266284 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 @@ -84,7 +84,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules Assert.Null(enrichedEvent.User); - A.CallTo(() => userResolver.FindByIdAsync(A._)) + A.CallTo(() => userResolver.FindByIdAsync(A._, default)) .MustNotHaveHappened(); } @@ -95,7 +95,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules var user = A.Dummy(); - A.CallTo(() => userResolver.FindByIdAsync(actor.Identifier)) + A.CallTo(() => userResolver.FindByIdAsync(actor.Identifier, default)) .Returns(user); var @event = @@ -110,7 +110,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules Assert.Equal(user, enrichedEvent.User); - A.CallTo(() => userResolver.FindByIdAsync(A._)) + A.CallTo(() => userResolver.FindByIdAsync(A._, default)) .MustHaveHappenedOnceExactly(); } @@ -121,7 +121,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules var user = A.Dummy(); - A.CallTo(() => userResolver.FindByIdAsync(actor.Identifier)) + A.CallTo(() => userResolver.FindByIdAsync(actor.Identifier, default)) .Returns(user); var event1 = @@ -145,7 +145,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules Assert.Equal(user, enrichedEvent1.User); Assert.Equal(user, enrichedEvent2.User); - A.CallTo(() => userResolver.FindByIdAsync(A._)) + A.CallTo(() => userResolver.FindByIdAsync(A._, default)) .MustHaveHappenedOnceExactly(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs index da80b0c38..dd1bb459c 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs @@ -44,7 +44,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules private readonly DomainId assetId = DomainId.NewGuid(); private readonly RuleEventFormatter sut; - private class FakeContentResolver : IRuleEventFormatter + private sealed class FakeContentResolver : IRuleEventFormatter { public (bool Match, ValueTask) Format(EnrichedEvent @event, object value, string[] path) { @@ -719,7 +719,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules var result = await sut.FormatAsync(script, @event); - Assert.Equal("[1,2,3]", result?.Replace(" ", string.Empty)); + Assert.Equal("[1,2,3]", result?.Replace(" ", string.Empty, StringComparison.Ordinal)); } [Theory] diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs index 9e60502b6..d27c890e6 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs @@ -41,7 +41,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules private readonly DomainId assetId = DomainId.NewGuid(); private readonly RuleEventFormatter sut; - private class FakeContentResolver : IRuleEventFormatter + private sealed class FakeContentResolver : IRuleEventFormatter { public (bool Match, ValueTask) Format(EnrichedEvent @event, object value, string[] path) { @@ -133,7 +133,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules var result = sut.ToEnvelope(@event); - Assert.Contains("MyEventName", result); + Assert.Contains("MyEventName", result, StringComparison.Ordinal); } [Fact] @@ -295,7 +295,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules var result = await sut.FormatAsync(script, @event); - Assert.Equal("{'categories':['ref1','ref2','ref3']}", result?.Replace(" ", string.Empty).Replace("\"", "'")); + Assert.Equal("{'categories':['ref1','ref2','ref3']}", result? + .Replace(" ", string.Empty, StringComparison.Ordinal) + .Replace("\"", "'", StringComparison.Ordinal)); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/MockupHttpHandler.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/MockupHttpHandler.cs index b4025a5b0..f44894483 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/MockupHttpHandler.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/MockupHttpHandler.cs @@ -46,7 +46,8 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting this.response = response; } - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + protected override async Task SendAsync(HttpRequestMessage request, + CancellationToken cancellationToken) { await Task.Delay(1000, cancellationToken); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs index 4381eb878..9e32b2d7b 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs @@ -26,7 +26,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent private readonly DomainId asset2 = DomainId.NewGuid(); private readonly IValidatorsFactory factory; - private class CustomFactory : IValidatorsFactory + private sealed class CustomFactory : IValidatorsFactory { public IEnumerable CreateValueValidators(ValidatorContext context, IField field, ValidatorFactory createFieldValidator) { diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs index 996edfefd..67d23809c 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs @@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent private readonly DomainId ref2 = DomainId.NewGuid(); private readonly IValidatorsFactory factory; - private class CustomFactory : IValidatorsFactory + private sealed class CustomFactory : IValidatorsFactory { private readonly DomainId schemaId; diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/PatternValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/PatternValidatorTests.cs index 26f2a984d..452778f5b 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/PatternValidatorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/PatternValidatorTests.cs @@ -73,7 +73,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators [Fact] public async Task Should_add_timeout_error_if_regex_is_too_slow() { - var sut = new PatternValidator(@"^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$"); + var sut = new PatternValidator(@"^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$", capture: true); await sut.ValidateAsync("https://archiverbx.blob.core.windows.net/static/C:/Users/USR/Documents/Projects/PROJ/static/images/full/1234567890.jpg", errors); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj b/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj index 2df80508e..e8b9e76d6 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj @@ -14,6 +14,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/AExtensions.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/AExtensions.cs index b32b6d26c..36bf4a30a 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/AExtensions.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/AExtensions.cs @@ -18,6 +18,11 @@ namespace Squidex.Domain.Apps.Core.TestHelpers return values == null ? that.IsNull() : that.IsSameSequenceAs(values); } + public static IEnumerable Is(this INegatableArgumentConstraintManager> that, params T[]? values) + { + return values == null ? that.IsNull() : that.IsSameSequenceAs(values); + } + public static HashSet Is(this INegatableArgumentConstraintManager> that, IEnumerable? values) { return values == null ? that.IsNull() : that.Matches(x => x.Intersect(values).Count() == values.Count()); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/AppProviderExtensionsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/AppProviderExtensionsTests.cs index 65a51a367..3c79d9ebf 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/AppProviderExtensionsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/AppProviderExtensionsTests.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Threading; using System.Threading.Tasks; using FakeItEasy; using Squidex.Domain.Apps.Core; @@ -32,7 +33,7 @@ namespace Squidex.Domain.Apps.Entities Assert.Empty(components); - A.CallTo(() => appProvider.GetSchemaAsync(A._, A._, false)) + A.CallTo(() => appProvider.GetSchemaAsync(A._, A._, false, A._)) .MustNotHaveHappened(); } @@ -52,7 +53,7 @@ namespace Squidex.Domain.Apps.Entities Assert.Single(components); Assert.Same(schema.SchemaDef, components[schemaId.Id]); - A.CallTo(() => appProvider.GetSchemaAsync(A._, A._, false)) + A.CallTo(() => appProvider.GetSchemaAsync(A._, A._, false, default)) .MustNotHaveHappened(); } @@ -61,7 +62,7 @@ namespace Squidex.Domain.Apps.Entities { var component = Mocks.Schema(appId, componentId1); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, componentId1.Id, false)) + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, componentId1.Id, false, default)) .Returns(component); var schema = @@ -83,7 +84,7 @@ namespace Squidex.Domain.Apps.Entities { var component = Mocks.Schema(appId, componentId1); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, componentId1.Id, false)) + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, componentId1.Id, false, default)) .Returns(component); var schema = @@ -105,7 +106,7 @@ namespace Squidex.Domain.Apps.Entities { var component = Mocks.Schema(appId, componentId1); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, componentId1.Id, false)) + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, componentId1.Id, false, default)) .Returns(component); var schema = @@ -134,7 +135,7 @@ namespace Squidex.Domain.Apps.Entities SchemaId = componentId1.Id })); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, componentId1.Id, false)) + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, componentId1.Id, false, default)) .Returns(component); var schema = @@ -170,10 +171,10 @@ namespace Squidex.Domain.Apps.Entities SchemaId = componentId2.Id })); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, componentId1.Id, false)) + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, componentId1.Id, false, default)) .Returns(component1); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, componentId2.Id, false)) + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, componentId2.Id, false, default)) .Returns(component2); var schema = diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/AppProviderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/AppProviderTests.cs new file mode 100644 index 000000000..499979968 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/AppProviderTests.cs @@ -0,0 +1,164 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Caching; +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Apps.Indexes; +using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Domain.Apps.Entities.Rules.Indexes; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Indexes; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Security; +using Xunit; + +namespace Squidex.Domain.Apps.Entities +{ + public class AppProviderTests + { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IAppsIndex indexForApps = A.Fake(); + private readonly IRulesIndex indexForRules = A.Fake(); + private readonly ISchemasIndex indexForSchemas = A.Fake(); + private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly NamedId schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); + private readonly IAppEntity app; + private readonly AppProvider sut; + + public AppProviderTests() + { + ct = cts.Token; + + app = Mocks.App(appId); + + sut = new AppProvider(indexForApps, indexForRules, indexForSchemas, new AsyncLocalCache()); + } + + [Fact] + public async Task Should_get_app_with_schema_from_index() + { + var schema = Mocks.Schema(app.NamedId(), schemaId); + + A.CallTo(() => indexForApps.GetAppAsync(app.Id, false, ct)) + .Returns(app); + + A.CallTo(() => indexForSchemas.GetSchemaAsync(app.Id, schema.Id, false, ct)) + .Returns(schema); + + var result = await sut.GetAppWithSchemaAsync(app.Id, schemaId.Id, false, ct); + + Assert.Equal(schema, result.Item2); + } + + [Fact] + public async Task Should_get_apps_from_index() + { + var permissions = new PermissionSet("*"); + + A.CallTo(() => indexForApps.GetAppsForUserAsync("user1", permissions, ct)) + .Returns(new List { app }); + + var result = await sut.GetUserAppsAsync("user1", permissions, ct); + + Assert.Equal(app, result.Single()); + } + + [Fact] + public async Task Should_get_app_from_index() + { + A.CallTo(() => indexForApps.GetAppAsync(app.Id, false, ct)) + .Returns(app); + + var result = await sut.GetAppAsync(app.Id, false, ct); + + Assert.Equal(app, result); + } + + [Fact] + public async Task Should_get_app_by_name_from_index() + { + A.CallTo(() => indexForApps.GetAppAsync(app.Name, false, ct)) + .Returns(app); + + var result = await sut.GetAppAsync(app.Name, false, ct); + + Assert.Equal(app, result); + } + + [Fact] + public async Task Should_get_schema_from_index() + { + var schema = Mocks.Schema(app.NamedId(), schemaId); + + A.CallTo(() => indexForSchemas.GetSchemaAsync(app.Id, schema.Id, false, ct)) + .Returns(schema); + + var result = await sut.GetSchemaAsync(app.Id, schema.Id, false, ct); + + Assert.Equal(schema, result); + } + + [Fact] + public async Task Should_get_schema_by_name_from_index() + { + var schema = Mocks.Schema(app.NamedId(), schemaId); + + A.CallTo(() => indexForSchemas.GetSchemaAsync(app.Id, schemaId.Name, false, ct)) + .Returns(schema); + + var result = await sut.GetSchemaAsync(app.Id, schemaId.Name, false, ct); + + Assert.Equal(schema, result); + } + + [Fact] + public async Task Should_get_schemas_from_index() + { + var schema = Mocks.Schema(app.NamedId(), schemaId); + + A.CallTo(() => indexForSchemas.GetSchemasAsync(app.Id, ct)) + .Returns(new List { schema }); + + var result = await sut.GetSchemasAsync(app.Id, ct); + + Assert.Equal(schema, result.Single()); + } + + [Fact] + public async Task Should_get_rules_from_index() + { + var rule = new RuleEntity(); + + A.CallTo(() => indexForRules.GetRulesAsync(app.Id, ct)) + .Returns(new List { rule }); + + var result = await sut.GetRulesAsync(app.Id, ct); + + Assert.Equal(rule, result.Single()); + } + + [Fact] + public async Task Should_get_rule_from_index() + { + var rule = new RuleEntity { Id = DomainId.NewGuid() }; + + A.CallTo(() => indexForRules.GetRulesAsync(app.Id, ct)) + .Returns(new List { rule }); + + var result = await sut.GetRuleAsync(app.Id, rule.Id, ct); + + Assert.Equal(rule, result); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppEventDeleter.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppEventDeleter.cs new file mode 100644 index 000000000..e683472a8 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppEventDeleter.cs @@ -0,0 +1,51 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public class AppEventDeleterTests + { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IEventStore eventStore = A.Fake(); + private readonly AppEventDeleter sut; + + public AppEventDeleterTests() + { + ct = cts.Token; + + sut = new AppEventDeleter(eventStore); + } + + [Fact] + public void Should_run_last() + { + var order = sut.Order; + + Assert.Equal(int.MaxValue, order); + } + + [Fact] + public async Task Should_remove_events_from_streams() + { + var app = Mocks.App(NamedId.Of(DomainId.NewGuid(), "my-app")); + + await sut.DeleteAppAsync(app, ct); + + A.CallTo(() => eventStore.DeleteAsync($"^[a-zA-Z0-9]-{app.Id}", ct)) + .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 new file mode 100644 index 000000000..8ea959357 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppPermanentDeleterTests.cs @@ -0,0 +1,138 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +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.EventSourcing; +using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.Reflection; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public class AppPermanentDeleterTests + { + private readonly IGrainFactory grainFactory = A.Fake(); + private readonly IDeleter deleter1 = A.Fake(); + private readonly IDeleter deleter2 = A.Fake(); + private readonly TypeNameRegistry typeNameRegistry; + private readonly AppPermanentDeleter sut; + + public AppPermanentDeleterTests() + { + typeNameRegistry = + new TypeNameRegistry() + .Map(typeof(AppCreated)) + .Map(typeof(AppContributorRemoved)) + .Map(typeof(AppDeleted)); + + sut = new AppPermanentDeleter(new[] { deleter1, deleter2 }, grainFactory, typeNameRegistry); + } + + [Fact] + public void Should_return_assets_filter_for_events_filter() + { + IEventConsumer consumer = sut; + + Assert.Equal("^app-", consumer.EventsFilter); + } + + [Fact] + public async Task Should_do_nothing_on_clear() + { + IEventConsumer consumer = sut; + + await consumer.ClearAsync(); + } + + [Fact] + public void Should_return_type_name_for_name() + { + IEventConsumer consumer = sut; + + Assert.Equal(nameof(AppPermanentDeleter), consumer.Name); + } + + [Fact] + public void Should_handle_delete_event() + { + var storedEvent = + new StoredEvent("stream", "1", 1, + new EventData(typeNameRegistry.GetName(), new EnvelopeHeaders(), "payload")); + + Assert.True(sut.Handles(storedEvent)); + } + + [Fact] + public void Should_handle_contributor_event() + { + var storedEvent = + new StoredEvent("stream", "1", 1, + new EventData(typeNameRegistry.GetName(), new EnvelopeHeaders(), "payload")); + + Assert.True(sut.Handles(storedEvent)); + } + + [Fact] + public void Should_not_handle_creation_event() + { + var storedEvent = + new StoredEvent("stream", "1", 1, + new EventData(typeNameRegistry.GetName(), new EnvelopeHeaders(), "payload")); + + Assert.False(sut.Handles(storedEvent)); + } + + [Fact] + public async Task Should_call_deleters_when_contributor_removed() + { + var app = Mocks.App(NamedId.Of(DomainId.NewGuid(), "my-app")); + + await sut.On(Envelope.Create(new AppContributorRemoved + { + AppId = app.NamedId(), + ContributorId = "user1" + })); + + A.CallTo(() => deleter1.DeleteContributorAsync(app.Id, "user1", default)) + .MustHaveHappened(); + + A.CallTo(() => deleter2.DeleteContributorAsync(app.Id, "user1", default)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_call_deleters_when_app_deleted() + { + var app = Mocks.App(NamedId.Of(DomainId.NewGuid(), "my-app")); + + var grain = A.Fake(); + + A.CallTo(() => grain.GetStateAsync()) + .Returns(app.AsJ()); + + A.CallTo(() => grainFactory.GetGrain(app.Id.ToString(), null)) + .Returns(grain); + + await sut.On(Envelope.Create(new AppDeleted + { + AppId = app.NamedId() + })); + + A.CallTo(() => deleter1.DeleteAppAsync(app, default)) + .MustHaveHappened(); + + A.CallTo(() => deleter2.DeleteAppAsync(app, default)) + .MustHaveHappened(); + } + } +} 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 191d80b65..d21151d00 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsTests.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using FakeItEasy; using Orleans; +using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Orleans; @@ -72,5 +73,27 @@ namespace Squidex.Domain.Apps.Entities.Apps A.CallTo(() => grain.RemoveAsync("the.path")) .MustHaveHappened(); } + + [Fact] + public async Task Should_clear_grain_when_app_deleted() + { + var app = Mocks.App(NamedId.Of(DomainId.NewGuid(), "my-app")); + + await ((IDeleter)sut).DeleteAppAsync(app, default); + + A.CallTo(() => grain.ClearAsync()) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_clear_grain_when_contributor_removed() + { + var app = Mocks.App(NamedId.Of(DomainId.NewGuid(), "my-app")); + + await ((IDeleter)sut).DeleteContributorAsync(app.Id, "user1", default); + + A.CallTo(() => grain.ClearAsync()) + .MustHaveHappened(); + } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUsageDeleterTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUsageDeleterTests.cs new file mode 100644 index 000000000..f6abaa742 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUsageDeleterTests.cs @@ -0,0 +1,51 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.UsageTracking; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public class AppUsageDeleterTests + { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IApiUsageTracker usageTracker = A.Fake(); + private readonly AppUsageDeleter sut; + + public AppUsageDeleterTests() + { + ct = cts.Token; + + sut = new AppUsageDeleter(usageTracker); + } + + [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")); + + await sut.DeleteAppAsync(app, ct); + + A.CallTo(() => usageTracker.DeleteAsync(app.Id.ToString(), ct)) + .MustHaveHappened(); + } + } +} 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 c4f2e342d..87ba84fad 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/BackupAppsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/BackupAppsTests.cs @@ -8,13 +8,17 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading; using System.Threading.Tasks; using FakeItEasy; using Squidex.Assets; +using Squidex.Domain.Apps.Core.TestHelpers; +using Squidex.Domain.Apps.Entities.Apps.DomainObject; using Squidex.Domain.Apps.Entities.Apps.Indexes; using Squidex.Domain.Apps.Entities.Backup; using Squidex.Domain.Apps.Events.Apps; using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Json.Objects; using Xunit; @@ -23,7 +27,10 @@ namespace Squidex.Domain.Apps.Entities.Apps { public class BackupAppsTests { - private readonly IAppsIndex index = A.Fake(); + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly Rebuilder rebuilder = A.Fake(); + private readonly IAppsIndex appsIndex = A.Fake(); private readonly IAppUISettings appUISettings = A.Fake(); private readonly IAppImageStore appImageStore = A.Fake(); private readonly DomainId appId = DomainId.NewGuid(); @@ -32,7 +39,9 @@ namespace Squidex.Domain.Apps.Entities.Apps public BackupAppsTests() { - sut = new BackupApps(appImageStore, index, appUISettings); + ct = cts.Token; + + sut = new BackupApps(rebuilder, appImageStore, appsIndex, appUISettings); } [Fact] @@ -48,15 +57,15 @@ namespace Squidex.Domain.Apps.Entities.Apps var context = CreateRestoreContext(); - A.CallTo(() => index.ReserveAsync(appId, appName)) + A.CallTo(() => appsIndex.ReserveAsync(appId, appName, A._)) .Returns("Reservation"); await sut.RestoreEventAsync(Envelope.Create(new AppCreated { Name = appName - }), context); + }), context, ct); - A.CallTo(() => index.ReserveAsync(appId, appName)) + A.CallTo(() => appsIndex.ReserveAsync(appId, appName, A._)) .MustHaveHappened(); } @@ -67,17 +76,20 @@ namespace Squidex.Domain.Apps.Entities.Apps var context = CreateRestoreContext(); - A.CallTo(() => index.ReserveAsync(appId, appName)) + A.CallTo(() => appsIndex.ReserveAsync(appId, appName, ct)) .Returns("Reservation"); await sut.RestoreEventAsync(Envelope.Create(new AppCreated { Name = appName - }), context); + }), context, ct); await sut.CompleteRestoreAsync(context); - A.CallTo(() => index.AddAsync("Reservation")) + A.CallTo(() => appsIndex.RemoveReservationAsync("Reservation", default)) + .MustHaveHappened(); + + A.CallTo(() => rebuilder.InsertManyAsync(A>.That.Is(appId), 1, default)) .MustHaveHappened(); } @@ -88,17 +100,17 @@ namespace Squidex.Domain.Apps.Entities.Apps var context = CreateRestoreContext(); - A.CallTo(() => index.ReserveAsync(appId, appName)) + A.CallTo(() => appsIndex.ReserveAsync(appId, appName, ct)) .Returns("Reservation"); await sut.RestoreEventAsync(Envelope.Create(new AppCreated { Name = appName - }), context); + }), context, ct); await sut.CleanupRestoreErrorAsync(appId); - A.CallTo(() => index.RemoveReservationAsync("Reservation")) + A.CallTo(() => appsIndex.RemoveReservationAsync("Reservation", default)) .MustHaveHappened(); } @@ -109,16 +121,15 @@ namespace Squidex.Domain.Apps.Entities.Apps var context = CreateRestoreContext(); - A.CallTo(() => index.ReserveAsync(appId, appName)) + A.CallTo(() => appsIndex.ReserveAsync(appId, appName, ct)) .Returns(Task.FromResult(null)); - await Assert.ThrowsAsync(() => + var @event = Envelope.Create(new AppCreated { - return sut.RestoreEventAsync(Envelope.Create(new AppCreated - { - Name = appName - }), context); + Name = appName }); + + await Assert.ThrowsAsync(() => sut.RestoreEventAsync(@event, context, ct)); } [Fact] @@ -126,7 +137,7 @@ namespace Squidex.Domain.Apps.Entities.Apps { await sut.CleanupRestoreErrorAsync(appId); - A.CallTo(() => index.RemoveReservationAsync("Reservation")) + A.CallTo(() => appsIndex.RemoveReservationAsync("Reservation", ct)) .MustNotHaveHappened(); } @@ -140,9 +151,9 @@ namespace Squidex.Domain.Apps.Entities.Apps A.CallTo(() => appUISettings.GetAsync(appId, null)) .Returns(settings); - await sut.BackupAsync(context); + await sut.BackupAsync(context, ct); - A.CallTo(() => context.Writer.WriteJsonAsync(A._, settings)) + A.CallTo(() => context.Writer.WriteJsonAsync(A._, settings, ct)) .MustHaveHappened(); } @@ -153,10 +164,10 @@ namespace Squidex.Domain.Apps.Entities.Apps var context = CreateRestoreContext(); - A.CallTo(() => context.Reader.ReadJsonAsync(A._)) + A.CallTo(() => context.Reader.ReadJsonAsync(A._, ct)) .Returns(settings); - await sut.RestoreAsync(context); + await sut.RestoreAsync(context, ct); A.CallTo(() => appUISettings.SetAsync(appId, null, settings)) .MustHaveHappened(); @@ -172,7 +183,7 @@ namespace Squidex.Domain.Apps.Entities.Apps ContributorId = "found" }); - var result = await sut.RestoreEventAsync(@event, context); + var result = await sut.RestoreEventAsync(@event, context, ct); Assert.True(result); Assert.Equal("found_mapped", @event.Payload.ContributorId); @@ -188,7 +199,7 @@ namespace Squidex.Domain.Apps.Entities.Apps ContributorId = "unknown" }); - var result = await sut.RestoreEventAsync(@event, context); + var result = await sut.RestoreEventAsync(@event, context, ct); Assert.False(result); Assert.Equal("unknown", @event.Payload.ContributorId); @@ -204,7 +215,7 @@ namespace Squidex.Domain.Apps.Entities.Apps ContributorId = "found" }); - var result = await sut.RestoreEventAsync(@event, context); + var result = await sut.RestoreEventAsync(@event, context, ct); Assert.True(result); Assert.Equal("found_mapped", @event.Payload.ContributorId); @@ -220,7 +231,7 @@ namespace Squidex.Domain.Apps.Entities.Apps ContributorId = "unknown" }); - var result = await sut.RestoreEventAsync(@event, context); + var result = await sut.RestoreEventAsync(@event, context, ct); Assert.False(result); Assert.Equal("unknown", @event.Payload.ContributorId); @@ -233,13 +244,13 @@ namespace Squidex.Domain.Apps.Entities.Apps var context = CreateBackupContext(); - A.CallTo(() => context.Writer.WriteBlobAsync(A._, A>._)) - .Invokes((string _, Func handler) => handler(imageStream)); + A.CallTo(() => context.Writer.OpenBlobAsync(A._, ct)) + .Returns(imageStream); - A.CallTo(() => appImageStore.DownloadAsync(appId, imageStream, default)) + A.CallTo(() => appImageStore.DownloadAsync(appId, imageStream, ct)) .Throws(new AssetNotFoundException("Image")); - await sut.BackupEventAsync(Envelope.Create(new AppImageUploaded()), context); + await sut.BackupEventAsync(Envelope.Create(new AppImageUploaded()), context, ct); } [Fact] @@ -249,12 +260,12 @@ namespace Squidex.Domain.Apps.Entities.Apps var context = CreateBackupContext(); - A.CallTo(() => context.Writer.WriteBlobAsync(A._, A>._)) - .Invokes((string _, Func handler) => handler(imageStream)); + A.CallTo(() => context.Writer.OpenBlobAsync(A._, ct)) + .Returns(imageStream); - await sut.BackupEventAsync(Envelope.Create(new AppImageUploaded()), context); + await sut.BackupEventAsync(Envelope.Create(new AppImageUploaded()), context, ct); - A.CallTo(() => appImageStore.DownloadAsync(appId, imageStream, default)) + A.CallTo(() => appImageStore.DownloadAsync(appId, imageStream, ct)) .MustHaveHappened(); } @@ -265,12 +276,12 @@ namespace Squidex.Domain.Apps.Entities.Apps var context = CreateRestoreContext(); - A.CallTo(() => context.Reader.ReadBlobAsync(A._, A>._)) - .Invokes((string _, Func handler) => handler(imageStream)); + A.CallTo(() => context.Reader.OpenBlobAsync(A._, ct)) + .Returns(imageStream); - await sut.RestoreEventAsync(Envelope.Create(new AppImageUploaded()), context); + await sut.RestoreEventAsync(Envelope.Create(new AppImageUploaded()), context, ct); - A.CallTo(() => appImageStore.UploadAsync(appId, imageStream, default)) + A.CallTo(() => appImageStore.UploadAsync(appId, imageStream, ct)) .MustHaveHappened(); } @@ -281,55 +292,13 @@ namespace Squidex.Domain.Apps.Entities.Apps var context = CreateRestoreContext(); - A.CallTo(() => context.Reader.ReadBlobAsync(A._, A>._)) - .Invokes((string _, Func handler) => handler(imageStream)); + A.CallTo(() => context.Reader.OpenBlobAsync(A._, ct)) + .Returns(imageStream); - A.CallTo(() => appImageStore.UploadAsync(appId, imageStream, default)) + A.CallTo(() => appImageStore.UploadAsync(appId, imageStream, ct)) .Throws(new AssetAlreadyExistsException("Image")); - await sut.RestoreEventAsync(Envelope.Create(new AppImageUploaded()), context); - } - - [Fact] - public async Task Should_restore_indices_for_all_non_deleted_schemas() - { - var userId1 = "found1"; - var userId2 = "found2"; - var userId3 = "found3"; - var context = CreateRestoreContext(); - - await sut.RestoreEventAsync(Envelope.Create(new AppContributorAssigned - { - ContributorId = userId1 - }), context); - - await sut.RestoreEventAsync(Envelope.Create(new AppContributorAssigned - { - ContributorId = userId2 - }), context); - - await sut.RestoreEventAsync(Envelope.Create(new AppContributorAssigned - { - ContributorId = userId3 - }), context); - - await sut.RestoreEventAsync(Envelope.Create(new AppContributorRemoved - { - ContributorId = userId3 - }), context); - - HashSet? newIndex = null; - - A.CallTo(() => index.RebuildByContributorsAsync(appId, A>._)) - .Invokes(new Action>((_, i) => newIndex = i)); - - await sut.CompleteRestoreAsync(context); - - Assert.Equal(new HashSet - { - "found1_mapped", - "found2_mapped" - }, newIndex); + await sut.RestoreEventAsync(Envelope.Create(new AppImageUploaded()), context, ct); } private BackupContext CreateBackupContext() 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 fe1eb7234..28ddc78a4 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DefaultAppLogStoreTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DefaultAppLogStoreTests.cs @@ -9,8 +9,11 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; +using System.Threading; using System.Threading.Tasks; using FakeItEasy; +using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; using Squidex.Infrastructure.Log; using Xunit; @@ -19,23 +22,47 @@ namespace Squidex.Domain.Apps.Entities.Apps { public class DefaultAppLogStoreTests { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; private readonly IRequestLogStore requestLogStore = A.Fake(); + private readonly DomainId appId = DomainId.NewGuid(); private readonly DefaultAppLogStore sut; public DefaultAppLogStoreTests() { + ct = cts.Token; + sut = new DefaultAppLogStore(requestLogStore); } + [Fact] + public void Should_run_deletion_in_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(appId, "my-app")); + + await ((IDeleter)sut).DeleteAppAsync(app, ct); + + A.CallTo(() => requestLogStore.DeleteAsync($"^[a-z]-{app.Id}", ct)) + .MustNotHaveHappened(); + } + [Fact] public async Task Should_not_forward_request_if_disabled() { A.CallTo(() => requestLogStore.IsEnabled) .Returns(false); - await sut.LogAsync(DomainId.NewGuid(), default); + await sut.LogAsync(appId, default, ct); - A.CallTo(() => requestLogStore.LogAsync(A._)) + A.CallTo(() => requestLogStore.LogAsync(A._, ct)) .MustNotHaveHappened(); } @@ -47,8 +74,8 @@ namespace Squidex.Domain.Apps.Entities.Apps A.CallTo(() => requestLogStore.IsEnabled) .Returns(true); - A.CallTo(() => requestLogStore.LogAsync(A._)) - .Invokes((Request request) => recordedRequest = request); + A.CallTo(() => requestLogStore.LogAsync(A._, ct)) + .Invokes(x => recordedRequest = x.GetArgument(0)!); var request = default(RequestLog); request.Bytes = 1024; @@ -65,7 +92,7 @@ namespace Squidex.Domain.Apps.Entities.Apps request.UserClientId = "frontend"; request.UserId = "user1"; - await sut.LogAsync(DomainId.NewGuid(), request); + await sut.LogAsync(appId, request, ct); Assert.NotNull(recordedRequest); @@ -80,6 +107,8 @@ namespace Squidex.Domain.Apps.Entities.Apps Contains(request.StatusCode, recordedRequest); Contains(request.UserClientId, recordedRequest); Contains(request.UserId, recordedRequest); + + Assert.Equal(appId.ToString(), recordedRequest?.Key); } [Fact] @@ -88,22 +117,18 @@ namespace Squidex.Domain.Apps.Entities.Apps var dateFrom = DateTime.UtcNow.Date.AddDays(-30); var dateTo = DateTime.UtcNow.Date; - var appId = DomainId.NewGuid(); - - A.CallTo(() => requestLogStore.QueryAllAsync(A>._, appId.ToString(), dateFrom, dateTo, default)) - .Invokes(x => + A.CallTo(() => requestLogStore.QueryAllAsync(appId.ToString(), dateFrom, dateTo, ct)) + .Returns(new[] { - var callback = x.GetArgument>(0)!; - - callback(CreateRecord()); - callback(CreateRecord()); - callback(CreateRecord()); - callback(CreateRecord()); - }); + CreateRecord(), + CreateRecord(), + CreateRecord(), + CreateRecord() + }.ToAsyncEnumerable()); var stream = new MemoryStream(); - await sut.ReadLogAsync(appId, dateFrom, dateTo, stream); + await sut.ReadLogAsync(appId, dateFrom, dateTo, stream, ct); stream.Position = 0; 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 05d10e81f..91abc857c 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 @@ -48,7 +48,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject { user = UserMocks.User(contributorId); - A.CallTo(() => userResolver.FindByIdOrEmailAsync(contributorId)) + A.CallTo(() => userResolver.FindByIdOrEmailAsync(contributorId, default)) .Returns(user); A.CallTo(() => appPlansProvider.GetFreePlan()) @@ -74,7 +74,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject } [Fact] - public async Task Command_should_throw_exception_if_app_is_archived() + public async Task Command_should_throw_exception_if_app_is_deleted() { await ExecuteCreateAsync(); await ExecuteArchiveAsync(); @@ -624,9 +624,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject } [Fact] - public async Task ArchiveApp_should_create_events_and_update_archived_flag() + public async Task ArchiveApp_should_create_events_and_update_deleted_flag() { - var command = new ArchiveApp(); + var command = new DeleteApp(); await ExecuteCreateAsync(); @@ -634,11 +634,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject result.ShouldBeEquivalent(None.Value); - Assert.True(sut.Snapshot.IsArchived); + Assert.True(sut.Snapshot.IsDeleted); LastEvents .ShouldHaveSameEvents( - CreateEvent(new AppArchived()) + CreateEvent(new AppDeleted()) ); A.CallTo(() => appPlansBillingManager.ChangePlanAsync(command.Actor.Identifier, AppNamedId, null, A._)) @@ -687,7 +687,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject private Task ExecuteArchiveAsync() { - return PublishAsync(new ArchiveApp()); + return PublishAsync(new DeleteApp()); } private Task PublishIdempotentAsync(AppCommand command) 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 2e90bed16..4c67e628b 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 @@ -33,15 +33,25 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards public GuardAppContributorsTests() { - A.CallTo(() => user1.Id).Returns("1"); - A.CallTo(() => user2.Id).Returns("2"); - A.CallTo(() => user3.Id).Returns("3"); + A.CallTo(() => user1.Id) + .Returns("1"); - A.CallTo(() => users.FindByIdAsync("1")).Returns(user1); - A.CallTo(() => users.FindByIdAsync("2")).Returns(user2); - A.CallTo(() => users.FindByIdAsync("3")).Returns(user3); + A.CallTo(() => user2.Id) + .Returns("2"); - A.CallTo(() => users.FindByIdAsync("notfound")) + A.CallTo(() => user3.Id) + .Returns("3"); + + A.CallTo(() => users.FindByIdAsync("1", default)) + .Returns(user1); + + A.CallTo(() => users.FindByIdAsync("2", default)) + .Returns(user2); + + A.CallTo(() => users.FindByIdAsync("3", default)) + .Returns(user3); + + A.CallTo(() => users.FindByIdAsync("notfound", default)) .Returns(Task.FromResult(null)); A.CallTo(() => appPlan.MaxContributors) 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 f04d2c92d..ffff81326 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 @@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards public GuardAppTests() { - A.CallTo(() => users.FindByIdOrEmailAsync(A._)) + A.CallTo(() => users.FindByIdOrEmailAsync(A._, default)) .Returns(A.Dummy()); A.CallTo(() => appPlans.GetPlan("notfound")) 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 new file mode 100644 index 000000000..856cfa32c --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsCacheGrainTests.cs @@ -0,0 +1,131 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FakeItEasy; +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 IAppRepository appRepository = A.Fake(); + private readonly DomainId appId = DomainId.NewGuid(); + private readonly AppsCacheGrain sut; + + public AppsCacheGrainTests() + { + sut = new AppsCacheGrain(appRepository); + sut.ActivateAsync(appId.ToString()).Wait(); + } + + [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/AppsIndexIntegrationTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexIntegrationTests.cs deleted file mode 100644 index ed28a69a1..000000000 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexIntegrationTests.cs +++ /dev/null @@ -1,265 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using System.Threading.Tasks; -using FakeItEasy; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Orleans; -using Orleans.Hosting; -using Orleans.TestingHost; -using Squidex.Caching; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.Apps.DomainObject; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Orleans; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Apps.Indexes -{ - [Trait("Category", "Dependencies")] - public class AppsIndexIntegrationTests - { - public class GrainEnvironment - { - private AppContributors contributors = AppContributors.Empty; - private long version = EtagVersion.Empty; - - public IGrainFactory GrainFactory { get; } = A.Fake(); - - public NamedId AppId { get; } = NamedId.Of(DomainId.NewGuid(), "my-app"); - - public GrainEnvironment() - { - var indexGrain = A.Fake(); - - A.CallTo(() => indexGrain.GetIdAsync(AppId.Name)) - .Returns(AppId.Id); - - var appGrain = A.Fake(); - - A.CallTo(() => appGrain.GetStateAsync()) - .ReturnsLazily(() => CreateApp().AsJ()); - - A.CallTo(() => GrainFactory.GetGrain(AppId.Id.ToString(), null)) - .Returns(appGrain); - - A.CallTo(() => GrainFactory.GetGrain(SingleGrain.Id, null)) - .Returns(indexGrain); - } - - public void HandleCommand(CreateApp command) - { - version++; - - contributors = contributors.Assign(command.Actor.Identifier, Role.Developer); - } - - public void HandleCommand(AssignContributor command) - { - version++; - - contributors = contributors.Assign(command.ContributorId, Role.Developer); - } - - public void VerifyGrainAccess(int count) - { - A.CallTo(() => GrainFactory.GetGrain(AppId.Id.ToString(), null)) - .MustHaveHappenedANumberOfTimesMatching(x => x == count); - } - - private IAppEntity CreateApp() - { - 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.Contributors) - .Returns(new AppContributors(contributors.ToDictionary())); - - return app; - } - } - - private sealed class Configurator : ISiloConfigurator - { - public void Configure(ISiloBuilder siloBuilder) - { - siloBuilder.AddOrleansPubSub(); - } - } - - [Theory] - [InlineData(3, 100, 400, false)] - [InlineData(3, 100, 202, true)] - public async Task Should_distribute_and_cache_domain_objects(short numSilos, int numRuns, int expectedCounts, bool shouldBreak) - { - var env = new GrainEnvironment(); - - var cluster = - new TestClusterBuilder(numSilos) - .AddSiloBuilderConfigurator() - .Build(); - - await cluster.DeployAsync(); - - try - { - var indexes = GetIndexes(shouldBreak, env, cluster); - - var appId = env.AppId; - - var random = new Random(); - - for (var i = 0; i < numRuns; i++) - { - var contributorId = Guid.NewGuid().ToString(); - var contributorCommand = new AssignContributor { ContributorId = contributorId, AppId = appId }; - - var commandContext = new CommandContext(contributorCommand, A.Fake()); - - var randomIndex = indexes[random.Next(numSilos)]; - - await randomIndex.HandleAsync(commandContext, x => - { - if (x.Command is AssignContributor command) - { - env.HandleCommand(command); - } - - x.Complete(true); - - return Task.CompletedTask; - }); - - foreach (var index in indexes) - { - var appById = await index.GetAppAsync(appId.Id, true); - var appByName = await index.GetAppByNameAsync(appId.Name, true); - - if (index == randomIndex || !shouldBreak || i == 0) - { - Assert.True(appById?.Contributors.ContainsKey(contributorId)); - Assert.True(appByName?.Contributors.ContainsKey(contributorId)); - } - else - { - Assert.False(appById?.Contributors.ContainsKey(contributorId)); - Assert.False(appByName?.Contributors.ContainsKey(contributorId)); - } - } - } - - env.VerifyGrainAccess(expectedCounts); - } - finally - { - await Task.WhenAny(Task.Delay(2000), cluster.StopAllSilosAsync()); - } - } - - [Theory] - [InlineData(3, false)] - public async Task Should_retrieve_new_app(short numSilos, bool shouldBreak) - { - var env = new GrainEnvironment(); - - var cluster = - new TestClusterBuilder(numSilos) - .AddSiloBuilderConfigurator() - .Build(); - - await cluster.DeployAsync(); - - try - { - var indexes = GetIndexes(shouldBreak, env, cluster); - - var appId = env.AppId; - - foreach (var index in indexes) - { - Assert.Null(await index.GetAppAsync(appId.Id, true)); - Assert.Null(await index.GetAppByNameAsync(appId.Name, true)); - } - - var creatorId = Guid.NewGuid().ToString(); - var creatorToken = RefToken.User(creatorId); - var createCommand = new CreateApp { Actor = creatorToken, AppId = appId.Id }; - - var commandContext = new CommandContext(createCommand, A.Fake()); - - var randomIndex = indexes[new Random().Next(3)]; - - await indexes[0].HandleAsync(commandContext, x => - { - if (x.Command is CreateApp command) - { - env.HandleCommand(command); - } - - x.Complete(true); - - return Task.CompletedTask; - }); - - foreach (var index in indexes) - { - var appById = await index.GetAppAsync(appId.Id, true); - var appByName = await index.GetAppByNameAsync(appId.Name, true); - - if (index == randomIndex || !shouldBreak) - { - Assert.True(appById?.Contributors.ContainsKey(creatorId)); - Assert.True(appByName?.Contributors.ContainsKey(creatorId)); - } - else - { - Assert.False(appById?.Contributors.ContainsKey(creatorId)); - Assert.False(appByName?.Contributors.ContainsKey(creatorId)); - } - } - } - finally - { - await Task.WhenAny(Task.Delay(2000), cluster.StopAllSilosAsync()); - } - } - - private static AppsIndex[] GetIndexes(bool shouldBreak, GrainEnvironment env, TestCluster cluster) - { - return cluster.Silos.OfType() - .Select(x => - { - var pubSub = - shouldBreak ? - A.Fake() : - x.SiloHost.Services.GetRequiredService(); - - var cache = - new ReplicatedCache( - new MemoryCache(Options.Create(new MemoryCacheOptions())), - pubSub, - Options.Create(new ReplicatedCacheOptions { Enable = true })); - - return new AppsIndex(env.GrainFactory, cache); - }).ToArray(); - } - } -} 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 c3b02bbea..5942945b5 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 @@ -14,8 +14,10 @@ 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; @@ -28,9 +30,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes public sealed class AppsIndexTests { private readonly IGrainFactory grainFactory = A.Fake(); - private readonly IAppsByNameIndexGrain indexByName = A.Fake(); - private readonly IAppsByUserIndexGrain indexForUser = A.Fake(); - private readonly IAppsByUserIndexGrain indexForClient = A.Fake(); + 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"; @@ -39,20 +40,14 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes public AppsIndexTests() { - A.CallTo(() => grainFactory.GetGrain(SingleGrain.Id, null)) - .Returns(indexByName); + A.CallTo(() => grainFactory.GetGrain(SingleGrain.Id, null)) + .Returns(cache); - A.CallTo(() => grainFactory.GetGrain(userId, null)) - .Returns(indexForUser); - - A.CallTo(() => grainFactory.GetGrain(clientId, null)) - .Returns(indexForClient); - - var cache = + var replicatedCache = new ReplicatedCache(new MemoryCache(Options.Create(new MemoryCacheOptions())), new SimplePubSub(A.Fake>()), Options.Create(new ReplicatedCacheOptions { Enable = true })); - sut = new AppsIndex(grainFactory, cache); + sut = new AppsIndex(appRepository, grainFactory, replicatedCache); } [Fact] @@ -60,7 +55,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes { var (expected, _) = CreateApp(); - A.CallTo(() => indexByName.GetIdsAsync(A.That.IsSameSequenceAs(new[] { appId.Name }))) + 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}")); @@ -73,8 +68,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes { var (expected, _) = CreateApp(); - A.CallTo(() => indexForUser.GetIdsAsync()) - .Returns(new List { appId.Id }); + A.CallTo(() => appRepository.QueryIdsAsync(userId, default)) + .Returns(new Dictionary { [appId.Name] = appId.Id }); var actual = await sut.GetAppsForUserAsync(userId, PermissionSet.Empty); @@ -86,11 +81,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes { var (expected, _) = CreateApp(); - A.CallTo(() => indexByName.GetIdsAsync(A.That.IsSameSequenceAs(new[] { appId.Name }))) + A.CallTo(() => cache.GetAppIdsAsync(A.That.Is(appId.Name))) .Returns(new List { appId.Id }); - A.CallTo(() => indexForUser.GetIdsAsync()) - .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}")); @@ -98,29 +93,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes Assert.Same(expected, actual[0]); } - [Fact] - public async Task Should_resolve_all_apps() - { - var (expected, _) = CreateApp(); - - A.CallTo(() => indexByName.GetIdsAsync()) - .Returns(new List { appId.Id }); - - var actual = await sut.GetAppsAsync(); - - Assert.Same(expected, actual[0]); - } - [Fact] public async Task Should_resolve_app_by_name() { var (expected, _) = CreateApp(); - A.CallTo(() => indexByName.GetIdAsync(appId.Name)) - .Returns(appId.Id); + A.CallTo(() => cache.GetAppIdsAsync(A.That.Is(appId.Name))) + .Returns(new List { appId.Id }); - var actual1 = await sut.GetAppByNameAsync(appId.Name, false); - var actual2 = await sut.GetAppByNameAsync(appId.Name, false); + var actual1 = await sut.GetAppAsync(appId.Name, false); + var actual2 = await sut.GetAppAsync(appId.Name, false); Assert.Same(expected, actual1); Assert.Same(expected, actual2); @@ -128,7 +110,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes A.CallTo(() => grainFactory.GetGrain(appId.Id.ToString(), null)) .MustHaveHappenedTwiceExactly(); - A.CallTo(() => indexByName.GetIdAsync(A._)) + A.CallTo(() => cache.GetAppIdsAsync(A._)) .MustHaveHappenedTwiceExactly(); } @@ -137,11 +119,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes { var (expected, _) = CreateApp(); - A.CallTo(() => indexByName.GetIdAsync(appId.Name)) - .Returns(appId.Id); + A.CallTo(() => cache.GetAppIdsAsync(A.That.Is(appId.Name))) + .Returns(new List { appId.Id }); - var actual1 = await sut.GetAppByNameAsync(appId.Name, true); - var actual2 = await sut.GetAppByNameAsync(appId.Name, true); + var actual1 = await sut.GetAppAsync(appId.Name, true); + var actual2 = await sut.GetAppAsync(appId.Name, true); var actual3 = await sut.GetAppAsync(appId.Id, true); Assert.Same(expected, actual1); @@ -151,7 +133,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes A.CallTo(() => grainFactory.GetGrain(appId.Id.ToString(), null)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => indexByName.GetIdAsync(A._)) + A.CallTo(() => cache.GetAppIdsAsync(A._)) .MustHaveHappenedOnceExactly(); } @@ -169,7 +151,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes A.CallTo(() => grainFactory.GetGrain(appId.Id.ToString(), null)) .MustHaveHappenedTwiceExactly(); - A.CallTo(() => indexByName.GetIdAsync(A._)) + A.CallTo(() => cache.GetAppIdsAsync(A._)) .MustNotHaveHappened(); } @@ -180,7 +162,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes var actual1 = await sut.GetAppAsync(appId.Id, true); var actual2 = await sut.GetAppAsync(appId.Id, true); - var actual3 = await sut.GetAppByNameAsync(appId.Name, true); + var actual3 = await sut.GetAppAsync(appId.Name, true); Assert.Same(expected, actual1); Assert.Same(expected, actual2); @@ -189,12 +171,12 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes A.CallTo(() => grainFactory.GetGrain(appId.Id.ToString(), null)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => indexByName.GetIdAsync(A._)) + A.CallTo(() => cache.GetAppIdsAsync(A._)) .MustNotHaveHappened(); } [Fact] - public async Task Should_return_null_if_app_archived() + public async Task Should_return_null_if_app_deleted() { CreateApp(isArchived: true); @@ -222,7 +204,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes { var token = RandomHash.Simple(); - A.CallTo(() => indexByName.ReserveAsync(appId.Id, appId.Name)) + A.CallTo(() => cache.ReserveAsync(appId.Id, appId.Name)) .Returns(token); var command = Create(appId.Name); @@ -233,43 +215,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes await sut.HandleAsync(context); - A.CallTo(() => indexByName.AddAsync(token)) - .MustHaveHappened(); - - A.CallTo(() => indexByName.RemoveReservationAsync(A._)) - .MustNotHaveHappened(); - - A.CallTo(() => indexForUser.AddAsync(appId.Id)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_also_add_to_user_index_if_app_is_created_by_client() - { - var token = RandomHash.Simple(); - - A.CallTo(() => indexByName.ReserveAsync(appId.Id, appId.Name)) - .Returns(token); - - var command = CreateFromClient(appId.Name); - - var context = - new CommandContext(command, commandBus) - .Complete(); - - await sut.HandleAsync(context); - - A.CallTo(() => indexByName.AddAsync(token)) + A.CallTo(() => cache.AddAsync(appId.Id, appId.Name)) .MustHaveHappened(); - A.CallTo(() => indexByName.RemoveReservationAsync(A._)) - .MustNotHaveHappened(); - - A.CallTo(() => indexForClient.AddAsync(appId.Id)) + A.CallTo(() => cache.RemoveReservationAsync(token)) .MustHaveHappened(); - - A.CallTo(() => indexForUser.AddAsync(appId.Id)) - .MustNotHaveHappened(); } [Fact] @@ -277,7 +227,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes { var token = RandomHash.Simple(); - A.CallTo(() => indexByName.ReserveAsync(appId.Id, appId.Name)) + A.CallTo(() => cache.ReserveAsync(appId.Id, appId.Name)) .Returns(token); var command = CreateFromClient(appId.Name); @@ -287,20 +237,17 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes await sut.HandleAsync(context); - A.CallTo(() => indexByName.AddAsync(token)) + A.CallTo(() => cache.AddAsync(A._, A._)) .MustNotHaveHappened(); - A.CallTo(() => indexByName.RemoveReservationAsync(token)) + A.CallTo(() => cache.RemoveReservationAsync(token)) .MustHaveHappened(); - - A.CallTo(() => indexForUser.AddAsync(appId.Id)) - .MustNotHaveHappened(); } [Fact] - public async Task Should_not_add_to_indexes_if_name_is_taken() + public async Task Should_not_add_to_indexes_if_name_is_reserved() { - A.CallTo(() => indexByName.ReserveAsync(appId.Id, appId.Name)) + A.CallTo(() => cache.ReserveAsync(appId.Id, appId.Name)) .Returns(Task.FromResult(null)); var command = Create(appId.Name); @@ -311,68 +258,36 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes await Assert.ThrowsAsync(() => sut.HandleAsync(context)); - A.CallTo(() => indexByName.AddAsync(A._)) - .MustNotHaveHappened(); - - A.CallTo(() => indexByName.RemoveReservationAsync(A._)) + A.CallTo(() => cache.AddAsync(A._, A._)) .MustNotHaveHappened(); - A.CallTo(() => indexForUser.AddAsync(appId.Id)) + A.CallTo(() => cache.RemoveReservationAsync(A._)) .MustNotHaveHappened(); } [Fact] - public async Task Should_not_add_to_indexes_if_name_is_invalid() + public async Task Should_not_add_to_indexes_if_name_is_taken() { - var command = Create("INVALID"); - - var context = - new CommandContext(command, commandBus) - .Complete(); - - await sut.HandleAsync(context); - - A.CallTo(() => indexByName.ReserveAsync(appId.Id, A._)) - .MustNotHaveHappened(); - - A.CallTo(() => indexByName.RemoveReservationAsync(A._)) - .MustNotHaveHappened(); + var token = RandomHash.Simple(); - A.CallTo(() => indexForUser.AddAsync(appId.Id)) - .MustNotHaveHappened(); - } + A.CallTo(() => cache.ReserveAsync(appId.Id, appId.Name)) + .Returns(token); - [Fact] - public async Task Should_add_app_to_index_if_contributor_assigned() - { - CreateApp(); + A.CallTo(() => cache.GetAppIdsAsync(A.That.Is(appId.Name))) + .Returns(new List { appId.Id }); - var command = new AssignContributor { AppId = appId, ContributorId = userId }; + var command = Create(appId.Name); var context = new CommandContext(command, commandBus) .Complete(); - await sut.HandleAsync(context); - - A.CallTo(() => indexForUser.AddAsync(appId.Id)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_update_index_if_app_is_updated() - { - var (_, appGrain) = CreateApp(); - - var command = new UpdateApp { AppId = appId }; - - var context = - new CommandContext(command, commandBus) - .Complete(); + await Assert.ThrowsAsync(() => sut.HandleAsync(context)); - await sut.HandleAsync(context); + A.CallTo(() => cache.AddAsync(A._, A._)) + .MustNotHaveHappened(); - A.CallTo(() => appGrain.GetStateAsync()) + A.CallTo(() => cache.RemoveReservationAsync(token)) .MustHaveHappened(); } @@ -394,28 +309,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes } [Fact] - public async Task Should_remove_from_user_index_if_contributor_removed() - { - CreateApp(); - - var command = new RemoveContributor { AppId = appId, ContributorId = userId }; - - var context = - new CommandContext(command, commandBus) - .Complete(); - - await sut.HandleAsync(context); - - A.CallTo(() => indexForUser.RemoveAsync(appId.Id)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_remove_app_from_indexes_if_app_gets_archived() + public async Task Should_remove_app_from_indexes_if_app_gets_deleted() { CreateApp(isArchived: true); - var command = new ArchiveApp { AppId = appId }; + var command = new DeleteApp { AppId = appId }; var context = new CommandContext(command, commandBus) @@ -423,75 +321,16 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes await sut.HandleAsync(context); - A.CallTo(() => indexByName.RemoveAsync(appId.Id)) - .MustHaveHappened(); - - A.CallTo(() => indexForUser.RemoveAsync(appId.Id)) - .MustHaveHappenedOnceExactly(); - } - - [Fact] - public async Task Should_also_remove_app_from_client_index_if_created_by_client() - { - CreateApp(fromClient: true); - - var command = new ArchiveApp { AppId = appId }; - - var context = - new CommandContext(command, commandBus) - .Complete(); - - await sut.HandleAsync(context); - - A.CallTo(() => indexByName.RemoveAsync(appId.Id)) - .MustHaveHappened(); - - A.CallTo(() => indexForUser.RemoveAsync(appId.Id)) - .MustHaveHappened(); - - A.CallTo(() => indexForClient.RemoveAsync(appId.Id)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_forward_call_if_rebuilding_for_contributors1() - { - var apps = new HashSet(); - - await sut.RebuildByContributorsAsync(userId, apps); - - A.CallTo(() => indexForUser.RebuildAsync(apps)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_forward_call_if_rebuilding_for_contributors2() - { - var users = new HashSet { userId }; - - await sut.RebuildByContributorsAsync(appId.Id, users); - - A.CallTo(() => indexForUser.AddAsync(appId.Id)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_forward_call_if_rebuilding() - { - var apps = new Dictionary(); - - await sut.RebuildAsync(apps); - - A.CallTo(() => indexByName.RebuildAsync(apps)) + A.CallTo(() => cache.RemoveAsync(appId.Id)) .MustHaveHappened(); } [Fact] public async Task Should_forward_reserveration() { - await sut.AddAsync("token"); + await sut.ReserveAsync(appId.Id, appId.Name); - A.CallTo(() => indexByName.AddAsync("token")) + A.CallTo(() => cache.ReserveAsync(appId.Id, appId.Name)) .MustHaveHappened(); } @@ -500,16 +339,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes { await sut.RemoveReservationAsync("token"); - A.CallTo(() => indexByName.RemoveReservationAsync("token")) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_forward_request_for_ids() - { - await sut.GetIdsAsync(); - - A.CallTo(() => indexByName.GetIdsAsync()) + A.CallTo(() => cache.RemoveReservationAsync("token")) .MustHaveHappened(); } @@ -523,7 +353,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes .Returns(appId.Name); A.CallTo(() => app.Version) .Returns(version); - A.CallTo(() => app.IsArchived) + A.CallTo(() => app.IsDeleted) .Returns(isArchived); A.CallTo(() => app.Contributors) .Returns(AppContributors.Empty.Assign(userId, Role.Owner)); 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 d800811eb..102beb488 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 @@ -37,10 +37,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation A.CallTo(() => notificatíonSender.IsActive) .Returns(true); - A.CallTo(() => userResolver.FindByIdAsync(assignerId)) + A.CallTo(() => userResolver.FindByIdAsync(assignerId, default)) .Returns(assigner); - A.CallTo(() => userResolver.FindByIdAsync(assigneeId)) + A.CallTo(() => userResolver.FindByIdAsync(assigneeId, default)) .Returns(assignee); sut = new InvitationEventConsumer(notificatíonSender, userResolver, log); @@ -110,7 +110,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation { var @event = CreateEvent(RefTokenType.Subject, true); - A.CallTo(() => userResolver.FindByIdAsync(assignerId)) + A.CallTo(() => userResolver.FindByIdAsync(assignerId, default)) .Returns(Task.FromResult(null)); await sut.On(@event); @@ -124,7 +124,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation { var @event = CreateEvent(RefTokenType.Subject, true); - A.CallTo(() => userResolver.FindByIdAsync(assigneeId)) + A.CallTo(() => userResolver.FindByIdAsync(assigneeId, default)) .Returns(Task.FromResult(null)); await sut.On(@event); @@ -163,7 +163,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation private void MustNotResolveUser() { - A.CallTo(() => userResolver.FindByIdAsync(A._)) + A.CallTo(() => userResolver.FindByIdAsync(A._, default)) .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 8d3614266..d6f5d411b 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 @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Threading; using System.Threading.Tasks; using FakeItEasy; using Squidex.Domain.Apps.Core.TestHelpers; @@ -40,7 +41,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation var user = UserMocks.User("123", command.ContributorId); - A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true)) + A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true, default)) .Returns((user, true)); await sut.HandleAsync(context); @@ -48,7 +49,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation Assert.Same(context.Result().App, app); Assert.Equal(user.Id, command.ContributorId); - A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true)) + A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true, default)) .MustHaveHappened(); } @@ -63,7 +64,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation var user = UserMocks.User("123", command.ContributorId); - A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true)) + A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true, default)) .Returns((user, false)); await sut.HandleAsync(context); @@ -71,7 +72,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation Assert.Same(context.Result(), app); Assert.Equal(user.Id, command.ContributorId); - A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true)) + A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true, default)) .MustHaveHappened(); } @@ -86,7 +87,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation await sut.HandleAsync(context); - A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(A._, A._)) + A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(A._, A._, A._)) .MustNotHaveHappened(); } @@ -101,7 +102,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation await sut.HandleAsync(context); - A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(A._, A._)) + 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 8bf1b9d86..0db22ad5c 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 @@ -8,6 +8,7 @@ using System; using System.Linq; using System.Security.Claims; +using System.Threading; using System.Threading.Tasks; using FakeItEasy; using Microsoft.Extensions.Options; @@ -55,7 +56,7 @@ 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)) + A.CallTo(() => userResolver.FindByIdAsync(userId, default)) .Returns(user); var isNextCalled = false; @@ -92,7 +93,7 @@ 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)) + A.CallTo(() => userResolver.FindByIdAsync(userId, default)) .Returns(user); await sut.HandleAsync(commandContext, x => @@ -102,7 +103,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans return Task.CompletedTask; }); - A.CallTo(() => userResolver.SetClaimAsync(userId, SquidexClaimTypes.TotalApps, "6", true)) + A.CallTo(() => userResolver.SetClaimAsync(userId, SquidexClaimTypes.TotalApps, "6", true, default)) .MustHaveHappened(); } @@ -125,7 +126,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans return Task.CompletedTask; }); - A.CallTo(() => userResolver.FindByIdAsync(A._)) + A.CallTo(() => userResolver.FindByIdAsync(A._, A._)) .MustNotHaveHappened(); } @@ -148,7 +149,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans return Task.CompletedTask; }); - A.CallTo(() => userResolver.FindByIdAsync(A._)) + A.CallTo(() => userResolver.FindByIdAsync(A._, A._)) .MustNotHaveHappened(); } @@ -171,7 +172,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans return Task.CompletedTask; }); - A.CallTo(() => userResolver.FindByIdAsync(A._)) + 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 f37fe1967..4d42fe50c 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 @@ -53,7 +53,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans A.CallTo(() => appPlan.BlockingApiCalls) .ReturnsLazily(x => apiCallsBlocking); - A.CallTo(() => usageTracker.GetMonthCallsAsync(appId.Id.ToString(), today, A._)) + A.CallTo(() => usageTracker.GetMonthCallsAsync(appId.Id.ToString(), today, A._, default)) .ReturnsLazily(x => Task.FromResult(apiCallsCurrent)); sut = new UsageGate(appPlansProvider, usageTracker, grainFactory); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/UsageNotifierGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/UsageNotifierGrainTests.cs index 7036283a6..b3bf0bc27 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/UsageNotifierGrainTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/UsageNotifierGrainTests.cs @@ -160,14 +160,14 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans { var user = UserMocks.User(id, email); - A.CallTo(() => userResolver.FindByIdOrEmailAsync(id)) + A.CallTo(() => userResolver.FindByIdOrEmailAsync(id, default)) .Returns(user); return user; } else { - A.CallTo(() => userResolver.FindByIdOrEmailAsync(id)) + A.CallTo(() => userResolver.FindByIdOrEmailAsync(id, default)) .Returns(Task.FromResult(null)); return null; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/RolePermissionsProviderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/RolePermissionsProviderTests.cs index 9ebcc15cb..595ecfc31 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/RolePermissionsProviderTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/RolePermissionsProviderTests.cs @@ -34,7 +34,7 @@ namespace Squidex.Domain.Apps.Entities.Apps [Fact] public async Task Should_provide_all_permissions() { - A.CallTo(() => appProvider.GetSchemasAsync(A._)) + A.CallTo(() => appProvider.GetSchemasAsync(A._, default)) .Returns(new List { Mocks.Schema(appId, NamedId.Of(DomainId.NewGuid(), "schema1")), 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 b87aa5d4a..ecda0aa4f 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetPermanentDeleterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetPermanentDeleterTests.cs @@ -21,11 +21,15 @@ namespace Squidex.Domain.Apps.Entities.Assets { private readonly IAssetFileStore assetFiletore = A.Fake(); private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly TypeNameRegistry typeNameRegistry; private readonly AssetPermanentDeleter sut; public AssetPermanentDeleterTests() { - var typeNameRegistry = new TypeNameRegistry().Map(typeof(AssetDeleted)); + typeNameRegistry = + new TypeNameRegistry() + .Map(typeof(AssetCreated)) + .Map(typeof(AssetDeleted)); sut = new AssetPermanentDeleter(assetFiletore, typeNameRegistry); } @@ -54,6 +58,26 @@ namespace Squidex.Domain.Apps.Entities.Assets Assert.Equal(nameof(AssetPermanentDeleter), consumer.Name); } + [Fact] + public void Should_handle_deletion_event() + { + var storedEvent = + new StoredEvent("stream", "1", 1, + new EventData(typeNameRegistry.GetName(), new EnvelopeHeaders(), "payload")); + + Assert.True(sut.Handles(storedEvent)); + } + + [Fact] + public void Should_not_handle_creation_event() + { + var storedEvent = + new StoredEvent("stream", "1", 1, + new EventData(typeNameRegistry.GetName(), new EnvelopeHeaders(), "payload")); + + Assert.False(sut.Handles(storedEvent)); + } + [Fact] public async Task Should_not_delete_assets_if_event_restored() { @@ -61,21 +85,18 @@ namespace Squidex.Domain.Apps.Entities.Assets await sut.On(Envelope.Create(@event).SetRestored()); - A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, A._, null)) + A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, default)) .MustNotHaveHappened(); } [Fact] - public async Task Should_delete_assets_for_all_versions() + public async Task Should_delete_asset() { var @event = new AssetDeleted { AppId = appId, AssetId = DomainId.NewGuid() }; - await sut.On(Envelope.Create(@event).SetEventStreamNumber(2)); - - A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, 0, null)) - .MustHaveHappened(); + await sut.On(Envelope.Create(@event)); - A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, 1, null)) + A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, default)) .MustHaveHappened(); } @@ -84,13 +105,10 @@ namespace Squidex.Domain.Apps.Entities.Assets { var @event = new AssetDeleted { AppId = appId, AssetId = DomainId.NewGuid() }; - A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, 0, null)) + A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, default)) .Throws(new AssetNotFoundException("fileName")); - await sut.On(Envelope.Create(@event).SetEventStreamNumber(2)); - - A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, 1, null)) - .MustHaveHappened(); + await sut.On(Envelope.Create(@event)); } [Fact] @@ -98,13 +116,10 @@ namespace Squidex.Domain.Apps.Entities.Assets { var @event = new AssetDeleted { AppId = appId, AssetId = DomainId.NewGuid() }; - A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, 0, null)) + A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, default)) .Throws(new InvalidOperationException()); - await Assert.ThrowsAsync(() => sut.On(Envelope.Create(@event).SetEventStreamNumber(2))); - - A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, 1, null)) - .MustNotHaveHappened(); + await Assert.ThrowsAsync(() => sut.On(Envelope.Create(@event))); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetTagsDeleterTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetTagsDeleterTests.cs new file mode 100644 index 000000000..bcf30ec97 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetTagsDeleterTests.cs @@ -0,0 +1,51 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Core.Tags; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public class AssetTagsDeleterTests + { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly ITagService tagService = A.Fake(); + private readonly AssetTagsDeleter sut; + + public AssetTagsDeleterTests() + { + ct = cts.Token; + + sut = new AssetTagsDeleter(tagService); + } + + [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")); + + await sut.DeleteAppAsync(app, ct); + + A.CallTo(() => tagService.ClearAsync(app.Id, TagGroups.Assets)) + .MustHaveHappened(); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetUsageTrackerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetUsageTrackerTests.cs index 5874bfe22..fcdf64932 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetUsageTrackerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetUsageTrackerTests.cs @@ -57,7 +57,7 @@ namespace Squidex.Domain.Apps.Entities.Assets [Fact] public async Task Should_get_total_size_from_summary_date() { - A.CallTo(() => usageTracker.GetAsync($"{appId.Id}_Assets", default, default, null)) + A.CallTo(() => usageTracker.GetAsync($"{appId.Id}_Assets", default, default, null, default)) .Returns(new Counters { ["TotalSize"] = 2048 }); var size = await sut.GetTotalSizeAsync(appId.Id); @@ -73,7 +73,7 @@ namespace Squidex.Domain.Apps.Entities.Assets var dateFrom = new DateTime(2018, 01, 05); var dateTo = dateFrom.AddDays(3); - A.CallTo(() => usageTracker.QueryAsync($"{appId.Id}_Assets", dateFrom, dateTo)) + A.CallTo(() => usageTracker.QueryAsync($"{appId.Id}_Assets", dateFrom, dateTo, default)) .Returns(new Dictionary> { [category] = new List<(DateTime, Counters)> @@ -139,10 +139,10 @@ namespace Squidex.Domain.Apps.Entities.Assets Counters? countersSummary = null; Counters? countersDate = null; - A.CallTo(() => usageTracker.TrackAsync(default, $"{appId.Id}_Assets", null, A._)) + A.CallTo(() => usageTracker.TrackAsync(default, $"{appId.Id}_Assets", null, A._, default)) .Invokes(x => countersSummary = x.GetArgument(3)); - A.CallTo(() => usageTracker.TrackAsync(date, $"{appId.Id}_Assets", null, A._)) + A.CallTo(() => usageTracker.TrackAsync(date, $"{appId.Id}_Assets", null, A._, default)) .Invokes(x => countersDate = x.GetArgument(3)); await sut.On(envelope); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs index e1d02c013..e822a4938 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsFluidExtensionTests.cs @@ -44,7 +44,7 @@ namespace Squidex.Domain.Apps.Entities.Assets new AssetsFluidExtension(services) }; - A.CallTo(() => appProvider.GetAppAsync(appId.Id, false)) + A.CallTo(() => appProvider.GetAppAsync(appId.Id, false, default)) .Returns(Mocks.App(appId)); sut = new FluidTemplateEngine(extensions); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs index 36e04a016..d7d82efed 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetsJintExtensionTests.cs @@ -49,7 +49,7 @@ namespace Squidex.Domain.Apps.Entities.Assets new AssetsJintExtension(services) }; - A.CallTo(() => appProvider.GetAppAsync(appId.Id, false)) + A.CallTo(() => appProvider.GetAppAsync(appId.Id, false, default)) .Returns(Mocks.App(appId)); sut = new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), extensions) 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 cefecdf35..d1ce8c3db 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/BackupAssetsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/BackupAssetsTests.cs @@ -25,6 +25,8 @@ namespace Squidex.Domain.Apps.Entities.Assets { public class BackupAssetsTests { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; private readonly Rebuilder rebuilder = A.Fake(); private readonly IAssetFileStore assetFileStore = A.Fake(); private readonly ITagService tagService = A.Fake(); @@ -34,6 +36,8 @@ namespace Squidex.Domain.Apps.Entities.Assets public BackupAssetsTests() { + ct = cts.Token; + sut = new BackupAssets(rebuilder, assetFileStore, tagService); } @@ -53,9 +57,9 @@ namespace Squidex.Domain.Apps.Entities.Assets A.CallTo(() => tagService.GetExportableTagsAsync(context.AppId, TagGroups.Assets)) .Returns(tags); - await sut.BackupAsync(context); + await sut.BackupAsync(context, ct); - A.CallTo(() => context.Writer.WriteJsonAsync(A._, tags)) + A.CallTo(() => context.Writer.WriteJsonAsync(A._, tags, ct)) .MustHaveHappened(); } @@ -66,10 +70,10 @@ namespace Squidex.Domain.Apps.Entities.Assets var context = CreateRestoreContext(); - A.CallTo(() => context.Reader.ReadJsonAsync(A._)) + A.CallTo(() => context.Reader.ReadJsonAsync(A._, ct)) .Returns(tags); - await sut.RestoreAsync(context); + await sut.RestoreAsync(context, ct); A.CallTo(() => tagService.RebuildTagsAsync(appId.Id, TagGroups.Assets, tags)) .MustHaveHappened(); @@ -114,12 +118,12 @@ namespace Squidex.Domain.Apps.Entities.Assets var context = CreateBackupContext(); - A.CallTo(() => context.Writer.WriteBlobAsync($"{assetId}_{version}.asset", A>._)) - .Invokes((string _, Func handler) => handler(assetStream)); + A.CallTo(() => context.Writer.OpenBlobAsync($"{assetId}_{version}.asset", ct)) + .Returns(assetStream); - await sut.BackupEventAsync(AppEvent(@event), context); + await sut.BackupEventAsync(AppEvent(@event), context, ct); - A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, assetId, version, null, assetStream, default, default)) + A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, assetId, version, null, assetStream, default, ct)) .MustHaveHappened(); } @@ -130,13 +134,13 @@ namespace Squidex.Domain.Apps.Entities.Assets var context = CreateBackupContext(); - A.CallTo(() => context.Writer.WriteBlobAsync($"{assetId}_{version}.asset", A>._)) - .Invokes((string _, Func handler) => handler(assetStream)); + A.CallTo(() => context.Writer.OpenBlobAsync($"{assetId}_{version}.asset", ct)) + .Returns(assetStream); - A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, assetId, version, null, assetStream, default, default)) + A.CallTo(() => assetFileStore.DownloadAsync(appId.Id, assetId, version, null, assetStream, default, ct)) .Throws(new AssetNotFoundException(assetId.ToString())); - await sut.BackupEventAsync(AppEvent(@event), context); + await sut.BackupEventAsync(AppEvent(@event), context, ct); } [Fact] @@ -178,12 +182,12 @@ namespace Squidex.Domain.Apps.Entities.Assets var context = CreateRestoreContext(); - A.CallTo(() => context.Reader.ReadBlobAsync($"{assetId}_{version}.asset", A>._)) - .Invokes((string _, Func handler) => handler(assetStream)); + A.CallTo(() => context.Reader.OpenBlobAsync($"{assetId}_{version}.asset", ct)) + .Returns(assetStream); - await sut.RestoreEventAsync(AppEvent(@event), context); + await sut.RestoreEventAsync(AppEvent(@event), context, ct); - A.CallTo(() => assetFileStore.UploadAsync(appId.Id, assetId, version, null, assetStream, true, default)) + A.CallTo(() => assetFileStore.UploadAsync(appId.Id, assetId, version, null, assetStream, true, ct)) .MustHaveHappened(); } @@ -194,12 +198,12 @@ namespace Squidex.Domain.Apps.Entities.Assets var context = CreateRestoreContext(); - A.CallTo(() => context.Reader.ReadBlobAsync($"{assetId}_{version}.asset", A>._)) + A.CallTo(() => context.Reader.OpenBlobAsync($"{assetId}_{version}.asset", ct)) .Throws(new FileNotFoundException()); - await sut.RestoreEventAsync(AppEvent(@event), context); + await sut.RestoreEventAsync(AppEvent(@event), context, ct); - A.CallTo(() => assetFileStore.UploadAsync(appId.Id, assetId, version, null, assetStream, true, default)) + A.CallTo(() => assetFileStore.UploadAsync(appId.Id, assetId, version, null, assetStream, true, ct)) .MustNotHaveHappened(); } @@ -214,24 +218,24 @@ namespace Squidex.Domain.Apps.Entities.Assets await sut.RestoreEventAsync(AppEvent(new AssetCreated { AssetId = assetId1 - }), context); + }), context, ct); await sut.RestoreEventAsync(AppEvent(new AssetCreated { AssetId = assetId2 - }), context); + }), context, ct); await sut.RestoreEventAsync(AppEvent(new AssetDeleted { AssetId = assetId2 - }), context); + }), context, ct); var rebuildAssets = new HashSet(); - A.CallTo(() => rebuilder.InsertManyAsync(A>._, A._, A._)) - .Invokes((IEnumerable source, int _, CancellationToken _) => rebuildAssets.AddRange(source)); + A.CallTo(() => rebuilder.InsertManyAsync(A>._, A._, ct)) + .Invokes(x => rebuildAssets.AddRange(x.GetArgument>(0)!)); - await sut.RestoreAsync(context); + await sut.RestoreAsync(context, ct); Assert.Equal(new HashSet { @@ -251,24 +255,24 @@ namespace Squidex.Domain.Apps.Entities.Assets await sut.RestoreEventAsync(AppEvent(new AssetFolderCreated { AssetFolderId = assetFolderId1 - }), context); + }), context, ct); await sut.RestoreEventAsync(AppEvent(new AssetFolderCreated { AssetFolderId = assetFolderId2 - }), context); + }), context, ct); await sut.RestoreEventAsync(AppEvent(new AssetFolderDeleted { AssetFolderId = assetFolderId2 - }), context); + }), context, ct); var rebuildAssetFolders = new HashSet(); - A.CallTo(() => rebuilder.InsertManyAsync(A>._, A._, A._)) - .Invokes((IEnumerable source, int _, CancellationToken _) => rebuildAssetFolders.AddRange(source)); + A.CallTo(() => rebuilder.InsertManyAsync(A>._, A._, ct)) + .Invokes(x => rebuildAssetFolders.AddRange(x.GetArgument>(0)!)); - await sut.RestoreAsync(context); + await sut.RestoreAsync(context, ct); Assert.Equal(new HashSet { @@ -291,22 +295,14 @@ namespace Squidex.Domain.Apps.Entities.Assets { @event.AppId = appId; - var envelope = Envelope.Create(@event); - - envelope.SetAggregateId(DomainId.Combine(appId.Id, @event.AssetId)); - - return envelope; + return Envelope.Create(@event).SetAggregateId(DomainId.Combine(appId.Id, @event.AssetId)); } private Envelope AppEvent(AssetFolderEvent @event) { @event.AppId = appId; - var envelope = Envelope.Create(@event); - - envelope.SetAggregateId(DomainId.Combine(appId.Id, @event.AssetFolderId)); - - return envelope; + return Envelope.Create(@event).SetAggregateId(DomainId.Combine(appId.Id, @event.AssetFolderId)); } private IUserMapping CreateUserMapping() diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DefaultAssetFileStoreTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DefaultAssetFileStoreTests.cs index 042c1aa5f..06902501e 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DefaultAssetFileStoreTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DefaultAssetFileStoreTests.cs @@ -7,11 +7,14 @@ using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using FakeItEasy; using Microsoft.Extensions.Options; using Squidex.Assets; +using Squidex.Domain.Apps.Entities.Assets.Repositories; +using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; using Xunit; @@ -19,6 +22,9 @@ namespace Squidex.Domain.Apps.Entities.Assets { public class DefaultAssetFileStoreTests { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IAssetRepository assetRepository = A.Fake(); private readonly IAssetStore assetStore = A.Fake(); private readonly DomainId appId = DomainId.NewGuid(); private readonly DomainId assetId = DomainId.NewGuid(); @@ -28,12 +34,14 @@ namespace Squidex.Domain.Apps.Entities.Assets public DefaultAssetFileStoreTests() { - sut = new DefaultAssetFileStore(assetStore, Options.Create(options)); + ct = cts.Token; + + sut = new DefaultAssetFileStore(assetStore, assetRepository, Options.Create(options)); } public static IEnumerable PathCases() { - yield return new object[] { true, "resize=100", "derived/{appId}/{assetId}_{assetFileVersion}_resize=100" }; + yield return new object[] { true, "resize=100", "{appId}/{assetId}_{assetFileVersion}_resize=100" }; yield return new object[] { true, string.Empty, "{appId}/{assetId}_{assetFileVersion}" }; yield return new object[] { false, "resize=100", "{appId}_{assetId}_{assetFileVersion}_resize=100" }; yield return new object[] { false, string.Empty, "{appId}_{assetId}_{assetFileVersion}" }; @@ -105,9 +113,9 @@ namespace Squidex.Domain.Apps.Entities.Assets { var stream = new MemoryStream(); - await sut.UploadAsync("Temp", stream); + await sut.UploadAsync("Temp", stream, ct); - A.CallTo(() => assetStore.UploadAsync("Temp", stream, false, CancellationToken.None)) + A.CallTo(() => assetStore.UploadAsync("Temp", stream, false, ct)) .MustHaveHappened(); } @@ -121,9 +129,9 @@ namespace Squidex.Domain.Apps.Entities.Assets var stream = new MemoryStream(); - await sut.UploadAsync(appId, assetId, assetFileVersion, suffix, stream); + await sut.UploadAsync(appId, assetId, assetFileVersion, suffix, stream, true, ct); - A.CallTo(() => assetStore.UploadAsync(fullName, stream, true, CancellationToken.None)) + A.CallTo(() => assetStore.UploadAsync(fullName, stream, true, ct)) .MustHaveHappened(); } @@ -137,9 +145,9 @@ namespace Squidex.Domain.Apps.Entities.Assets var stream = new MemoryStream(); - await sut.DownloadAsync(appId, assetId, assetFileVersion, suffix, stream); + await sut.DownloadAsync(appId, assetId, assetFileVersion, suffix, stream, default, ct); - A.CallTo(() => assetStore.DownloadAsync(fullName, stream, default, CancellationToken.None)) + A.CallTo(() => assetStore.DownloadAsync(fullName, stream, default, ct)) .MustHaveHappened(); } @@ -150,12 +158,12 @@ namespace Squidex.Domain.Apps.Entities.Assets var stream = new MemoryStream(); - A.CallTo(() => assetStore.DownloadAsync(A._, stream, default, CancellationToken.None)) + A.CallTo(() => assetStore.DownloadAsync(A._, stream, default, ct)) .Throws(new AssetNotFoundException(assetId.ToString())).Once(); - await Assert.ThrowsAsync(() => sut.DownloadAsync(appId, assetId, assetFileVersion, null, stream)); + await Assert.ThrowsAsync(() => sut.DownloadAsync(appId, assetId, assetFileVersion, null, stream, default, ct)); - A.CallTo(() => assetStore.DownloadAsync(A._, stream, default, CancellationToken.None)) + A.CallTo(() => assetStore.DownloadAsync(A._, stream, default, ct)) .MustHaveHappenedOnceExactly(); } @@ -167,12 +175,12 @@ namespace Squidex.Domain.Apps.Entities.Assets var stream = new MemoryStream(); - A.CallTo(() => assetStore.DownloadAsync(A.That.Matches(x => x != fileName), stream, default, CancellationToken.None)) + A.CallTo(() => assetStore.DownloadAsync(A.That.Matches(x => x != fileName), stream, default, ct)) .Throws(new AssetNotFoundException(assetId.ToString())).Once(); - await sut.DownloadAsync(appId, assetId, assetFileVersion, suffix, stream); + await sut.DownloadAsync(appId, assetId, assetFileVersion, suffix, stream, default, ct); - A.CallTo(() => assetStore.DownloadAsync(fullName, stream, default, CancellationToken.None)) + A.CallTo(() => assetStore.DownloadAsync(fullName, stream, default, ct)) .MustHaveHappened(); } @@ -184,36 +192,81 @@ namespace Squidex.Domain.Apps.Entities.Assets options.FolderPerApp = folderPerApp; - await sut.CopyAsync("Temp", appId, assetId, assetFileVersion, suffix); + await sut.CopyAsync("Temp", appId, assetId, assetFileVersion, suffix, ct); - A.CallTo(() => assetStore.CopyAsync("Temp", fullName, CancellationToken.None)) + A.CallTo(() => assetStore.CopyAsync("Temp", fullName, ct)) .MustHaveHappened(); } [Fact] public async Task Should_delete_temporary_file_from_store() { - await sut.DeleteAsync("Temp"); + await sut.DeleteAsync("Temp", ct); - A.CallTo(() => assetStore.DeleteAsync("Temp")) + A.CallTo(() => assetStore.DeleteAsync("Temp", ct)) .MustHaveHappened(); } - [Theory] - [MemberData(nameof(PathCases))] - public async Task Should_delete_file_from_store(bool folderPerApp, string? suffix, string fileName) + [Fact] + public async Task Should_delete_file_from_store() { - var fullName = GetFullName(fileName); + await sut.DeleteAsync(appId, assetId, ct); - options.FolderPerApp = folderPerApp; + A.CallTo(() => assetStore.DeleteByPrefixAsync($"{appId}_{assetId}", ct)) + .MustHaveHappened(); - await sut.DeleteAsync(appId, assetId, assetFileVersion, suffix); + A.CallTo(() => assetStore.DeleteByPrefixAsync(assetId.ToString(), ct)) + .MustHaveHappened(); + } - A.CallTo(() => assetStore.DeleteAsync(fullName)) + [Fact] + public async Task Should_delete_file_from_store_when_folders_are_used() + { + options.FolderPerApp = true; + + await sut.DeleteAsync(appId, assetId, ct); + + A.CallTo(() => assetStore.DeleteByPrefixAsync($"{appId}/", ct)) .MustHaveHappened(); + } + + [Fact] + public async Task Should_delete_assets_invidually__on_app_deletion() + { + var asset1 = new AssetEntity { Id = DomainId.NewGuid() }; + var asset2 = new AssetEntity { Id = DomainId.NewGuid() }; + + A.CallTo(() => assetRepository.StreamAll(appId, ct)) + .Returns(new[] { asset1, asset2 }.ToAsyncEnumerable()); - A.CallTo(() => assetStore.DeleteAsync(A._)) - .MustHaveHappenedANumberOfTimesMatching(x => x == (folderPerApp ? 1 : 2)); + var app = Mocks.App(NamedId.Of(appId, "my-app")); + + await ((IDeleter)sut).DeleteAppAsync(app, ct); + + A.CallTo(() => assetStore.DeleteByPrefixAsync($"{appId}_{asset1.Id}", ct)) + .MustHaveHappened(); + + A.CallTo(() => assetStore.DeleteByPrefixAsync($"{appId}_{asset2.Id}", ct)) + .MustHaveHappened(); + + A.CallTo(() => assetStore.DeleteByPrefixAsync(asset1.Id.ToString(), ct)) + .MustHaveHappened(); + + A.CallTo(() => assetStore.DeleteByPrefixAsync(asset2.Id.ToString(), ct)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_delete_app_folder_on_app_deletion_when_folders_are_used() + { + options.FolderPerApp = true; + + var app = Mocks.App(NamedId.Of(appId, "my-app")); + + await ((IDeleter)sut).DeleteAppAsync(app, ct); + + A.CallTo(() => assetStore.DeleteByPrefixAsync($"{appId}/", ct)) + .MustHaveHappened(); } private string GetFullName(string fileName) 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 cae17e0fb..06c4f21ae 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 @@ -190,11 +190,11 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject private void AssertAssetHasBeenUploaded(long fileVersion) { - A.CallTo(() => assetFileStore.UploadAsync(A._, A._, CancellationToken.None)) + A.CallTo(() => assetFileStore.UploadAsync(A._, A._, default)) .MustHaveHappened(); - A.CallTo(() => assetFileStore.CopyAsync(A._, AppId, assetId, fileVersion, null, CancellationToken.None)) + A.CallTo(() => assetFileStore.CopyAsync(A._, AppId, assetId, fileVersion, null, default)) .MustHaveHappened(); - A.CallTo(() => assetFileStore.DeleteAsync(A._)) + A.CallTo(() => assetFileStore.DeleteAsync(A._, default)) .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 b7b599039..34f870370 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 @@ -61,7 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject A.CallTo(() => app.AssetScripts) .Returns(scripts); - A.CallTo(() => appProvider.GetAppAsync(AppId, false)) + A.CallTo(() => appProvider.GetAppAsync(AppId, false, default)) .Returns(app); A.CallTo(() => assetQuery.FindAssetFolderAsync(AppId, parentId, A._)) 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 4bbe68106..22906257b 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 @@ -40,7 +40,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject { app = Mocks.App(AppNamedId, Language.DE); - A.CallTo(() => appProvider.GetAppAsync(AppId, false)) + A.CallTo(() => appProvider.GetAppAsync(AppId, false, default)) .Returns(app); A.CallTo(() => assetQuery.FindAssetFolderAsync(AppId, parentId, A._)) 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 71a676347..6dfc37f4b 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 @@ -45,7 +45,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb Task.Run(async () => { - await assetRepository.InitializeAsync(); + await assetRepository.InitializeAsync(default); await mongoDatabase.RunCommandAsync("{ profile : 0 }"); await mongoDatabase.DropCollectionAsync("system.profile"); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupCompatibilityTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupCompatibilityTests.cs index 3b22d4c14..1688112a2 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupCompatibilityTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupCompatibilityTests.cs @@ -21,8 +21,7 @@ namespace Squidex.Domain.Apps.Entities.Backup await writer.WriteVersionAsync(); - A.CallTo(() => writer.WriteJsonAsync(A._, - A.That.Matches(x => x.Major == 5))) + A.CallTo(() => writer.WriteJsonAsync(A._, A.That.Matches(x => x.Major == 5), default)) .MustHaveHappened(); } @@ -31,7 +30,7 @@ namespace Squidex.Domain.Apps.Entities.Backup { var reader = A.Fake(); - A.CallTo(() => reader.ReadJsonAsync(A._)) + A.CallTo(() => reader.ReadJsonAsync(A._, default)) .Returns(new CompatibilityExtensions.FileVersion { Major = 5 }); await reader.CheckCompatibilityAsync(); @@ -42,7 +41,7 @@ namespace Squidex.Domain.Apps.Entities.Backup { var reader = A.Fake(); - A.CallTo(() => reader.ReadJsonAsync(A._)) + A.CallTo(() => reader.ReadJsonAsync(A._, default)) .Returns(new CompatibilityExtensions.FileVersion { Major = 3 }); await Assert.ThrowsAsync(() => reader.CheckCompatibilityAsync()); @@ -53,7 +52,7 @@ namespace Squidex.Domain.Apps.Entities.Backup { var reader = A.Fake(); - A.CallTo(() => reader.ReadJsonAsync(A._)) + A.CallTo(() => reader.ReadJsonAsync(A._, default)) .Throws(new FileNotFoundException()); await reader.CheckCompatibilityAsync(); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupReaderWriterTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupReaderWriterTests.cs index 9ec9f5ccf..28b6c5a7b 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupReaderWriterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupReaderWriterTests.cs @@ -49,10 +49,10 @@ namespace Squidex.Domain.Apps.Entities.Backup { try { - await writer.WriteBlobAsync(file, _ => + await using (var stream = await writer.OpenBlobAsync(file)) { throw new InvalidOperationException(); - }); + } } catch { @@ -120,7 +120,7 @@ namespace Squidex.Domain.Apps.Entities.Backup return Task.CompletedTask; }, async reader => { - await Assert.ThrowsAsync(() => reader.ReadBlobAsync("404", s => Task.CompletedTask)); + await Assert.ThrowsAsync(() => reader.OpenBlobAsync("404")); }); } @@ -180,7 +180,7 @@ namespace Squidex.Domain.Apps.Entities.Backup { var targetEvents = new List<(string Stream, Envelope Event)>(); - await reader.ReadEventsAsync(streamNameResolver, formatter, async @event => + await foreach (var @event in reader.ReadEventsAsync(streamNameResolver, formatter)) { var index = int.Parse(@event.Event.Headers["Index"].ToString()); @@ -200,7 +200,7 @@ namespace Squidex.Domain.Apps.Entities.Backup } targetEvents.Add(@event); - }); + } for (var i = 0; i < targetEvents.Count; i++) { @@ -226,26 +226,26 @@ namespace Squidex.Domain.Apps.Entities.Backup return writer.WriteJsonAsync(file, value); } - private static Task WriteGuidAsync(IBackupWriter writer, string file, Guid value) + private static async Task WriteGuidAsync(IBackupWriter writer, string file, Guid value) { - return writer.WriteBlobAsync(file, async stream => + await using (var stream = await writer.OpenBlobAsync(file)) { await stream.WriteAsync(value.ToByteArray()); - }); + } } private static async Task ReadGuidAsync(IBackupReader reader, string file) { var read = Guid.Empty; - await reader.ReadBlobAsync(file, async stream => + await using (var stream = await reader.OpenBlobAsync(file)) { var buffer = new byte[16]; await stream.ReadAsync(buffer); read = new Guid(buffer); - }); + } return read; } 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 1dfe1bf31..cb0dd0bab 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupServiceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupServiceTests.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using FakeItEasy; using Orleans; using Squidex.Domain.Apps.Entities.Backup.State; +using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; using Squidex.Infrastructure.Orleans; using Xunit; @@ -138,5 +139,19 @@ namespace Squidex.Domain.Apps.Entities.Backup A.CallTo(() => grain.DeleteAsync(backupId)) .MustHaveHappened(); } + + [Fact] + public async Task Should_call_grain_to_clear_backups() + { + var grain = A.Fake(); + + A.CallTo(() => grainFactory.GetGrain(appId.ToString(), null)) + .Returns(grain); + + await ((IDeleter)sut).DeleteAppAsync(Mocks.App(NamedId.Of(appId, "my-app")), default); + + A.CallTo(() => grain.ClearAsync()) + .MustHaveHappened(); + } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/DefaultBackupArchiveStoreTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/DefaultBackupArchiveStoreTests.cs index bcacdfb22..5bda23d74 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/DefaultBackupArchiveStoreTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/DefaultBackupArchiveStoreTests.cs @@ -17,6 +17,8 @@ namespace Squidex.Domain.Apps.Entities.Backup { public class DefaultBackupArchiveStoreTests { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; private readonly IAssetStore assetStore = A.Fake(); private readonly DomainId backupId = DomainId.NewGuid(); private readonly string fileName; @@ -24,6 +26,8 @@ namespace Squidex.Domain.Apps.Entities.Backup public DefaultBackupArchiveStoreTests() { + ct = cts.Token; + fileName = $"{backupId}_0"; sut = new DefaultBackupArchiveStore(assetStore); @@ -34,9 +38,9 @@ namespace Squidex.Domain.Apps.Entities.Backup { var stream = new MemoryStream(); - await sut.UploadAsync(backupId, stream); + await sut.UploadAsync(backupId, stream, ct); - A.CallTo(() => assetStore.UploadAsync(fileName, stream, true, CancellationToken.None)) + A.CallTo(() => assetStore.UploadAsync(fileName, stream, true, ct)) .MustHaveHappened(); } @@ -45,18 +49,18 @@ namespace Squidex.Domain.Apps.Entities.Backup { var stream = new MemoryStream(); - await sut.DownloadAsync(backupId, stream); + await sut.DownloadAsync(backupId, stream, ct); - A.CallTo(() => assetStore.DownloadAsync(fileName, stream, default, CancellationToken.None)) + A.CallTo(() => assetStore.DownloadAsync(fileName, stream, default, ct)) .MustHaveHappened(); } [Fact] public async Task Should_invoke_asset_store_to_delete_archive_using_suffix_for_compatibility() { - await sut.DeleteAsync(backupId); + await sut.DeleteAsync(backupId, ct); - A.CallTo(() => assetStore.DeleteAsync(fileName)) + A.CallTo(() => assetStore.DeleteAsync(fileName, ct)) .MustHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/UserMappingTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/UserMappingTests.cs index 8c6f057f5..221607008 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/UserMappingTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/UserMappingTests.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using FakeItEasy; using Squidex.Domain.Apps.Core.TestHelpers; @@ -18,11 +19,15 @@ namespace Squidex.Domain.Apps.Entities.Backup { public class UserMappingTests { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; private readonly RefToken initiator = Subject("me"); private readonly UserMapping sut; public UserMappingTests() { + ct = cts.Token; + sut = new UserMapping(initiator); } @@ -45,17 +50,17 @@ namespace Squidex.Domain.Apps.Entities.Backup var userResolver = A.Fake(); - A.CallTo(() => userResolver.QueryManyAsync(A.That.Is(user1.Id, user2.Id))) + A.CallTo(() => userResolver.QueryManyAsync(A.That.Is(user1.Id, user2.Id), ct)) .Returns(users); var writer = A.Fake(); Dictionary? storedUsers = null; - A.CallTo(() => writer.WriteJsonAsync(A._, A._)) - .Invokes((string _, object json) => storedUsers = (Dictionary)json); + A.CallTo(() => writer.WriteJsonAsync(A._, A._, ct)) + .Invokes(x => storedUsers = x.GetArgument>(1)); - await sut.StoreAsync(writer, userResolver); + await sut.StoreAsync(writer, userResolver, ct); Assert.Equal(new Dictionary { @@ -74,13 +79,13 @@ namespace Squidex.Domain.Apps.Entities.Backup var userResolver = A.Fake(); - A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user1.Email, false)) + A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user1.Email, false, ct)) .Returns((user1, false)); - A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user2.Email, false)) + A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user2.Email, false, ct)) .Returns((user2, true)); - await sut.RestoreAsync(reader, userResolver); + await sut.RestoreAsync(reader, userResolver, ct); Assert.True(sut.TryMap("1_old", out var mapped1)); Assert.True(sut.TryMap(Subject("2_old"), out var mapped2)); @@ -107,13 +112,13 @@ namespace Squidex.Domain.Apps.Entities.Backup Assert.Same(client, mapped); } - private static IBackupReader SetupReader(params IUser[] users) + private IBackupReader SetupReader(params IUser[] users) { var storedUsers = users.ToDictionary(x => $"{x.Id}_old", x => x.Email); var reader = A.Fake(); - A.CallTo(() => reader.ReadJsonAsync>(A._)) + A.CallTo(() => reader.ReadJsonAsync>(A._, ct)) .Returns(storedUsers); return reader; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentTriggerHandlerTests.cs index 585c9ae90..3a5243945 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentTriggerHandlerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentTriggerHandlerTests.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using FakeItEasy; using Microsoft.Extensions.Caching.Memory; @@ -82,7 +83,7 @@ namespace Squidex.Domain.Apps.Entities.Comments var @event = new CommentCreated { Mentions = userIds }; - A.CallTo(() => userResolver.QueryManyAsync(userIds)) + A.CallTo(() => userResolver.QueryManyAsync(userIds, default)) .Returns(users.ToDictionary(x => x.Id)); var result = await sut.CreateEnrichedEventsAsync(Envelope.Create(@event), ctx, default).ToListAsync(); @@ -127,7 +128,7 @@ namespace Squidex.Domain.Apps.Entities.Comments Assert.Empty(result); - A.CallTo(() => userResolver.QueryManyAsync(A._)) + A.CallTo(() => userResolver.QueryManyAsync(A._, A._)) .MustNotHaveHappened(); } @@ -142,7 +143,7 @@ namespace Squidex.Domain.Apps.Entities.Comments Assert.Empty(result); - A.CallTo(() => userResolver.QueryManyAsync(A._)) + A.CallTo(() => userResolver.QueryManyAsync(A._, A._)) .MustNotHaveHappened(); } 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 f4e81bc49..1aff0d774 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 @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Threading; using System.Threading.Tasks; using FakeItEasy; using Orleans; @@ -31,7 +32,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject public CommentsCommandMiddlewareTests() { - A.CallTo(() => userResolver.FindByIdOrEmailAsync(A._)) + A.CallTo(() => userResolver.FindByIdOrEmailAsync(A._, default)) .Returns(Task.FromResult(null)); sut = new CommentsCommandMiddleware(grainFactory, userResolver); @@ -112,7 +113,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject await sut.HandleAsync(context); - A.CallTo(() => userResolver.FindByIdOrEmailAsync(A._)) + A.CallTo(() => userResolver.FindByIdOrEmailAsync(A._, A._)) .MustNotHaveHappened(); } @@ -128,7 +129,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject await sut.HandleAsync(context); - A.CallTo(() => userResolver.FindByIdOrEmailAsync(A._)) + A.CallTo(() => userResolver.FindByIdOrEmailAsync(A._, A._)) .MustNotHaveHappened(); } @@ -141,7 +142,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject { var user = UserMocks.User(id, email); - A.CallTo(() => userResolver.FindByIdOrEmailAsync(email)) + A.CallTo(() => userResolver.FindByIdOrEmailAsync(email, default)) .Returns(user); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsGrainTests.cs index 1e0ea78dd..7add3e4a1 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsGrainTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsGrainTests.cs @@ -41,7 +41,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject public CommentsGrainTests() { - A.CallTo(() => eventStore.AppendAsync(A._, A._, A._, A>._)) + A.CallTo(() => eventStore.AppendAsync(A._, A._, A._, A>._, default)) .Invokes(x => LastEvents = sut!.GetUncommittedEvents().Select(x => x.To()).ToList()); sut = new CommentsGrain(eventStore, eventDataFormatter); @@ -186,4 +186,4 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject return LastEvents.ElementAt(0).Headers.Timestamp(); } } -} \ No newline at end of file +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BackupContentsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BackupContentsTests.cs index 3e40f0441..eeefad9cf 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BackupContentsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/BackupContentsTests.cs @@ -26,6 +26,8 @@ namespace Squidex.Domain.Apps.Entities.Contents { public class BackupContentsTests { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); private readonly IUrlGenerator urlGenerator = A.Fake(); private readonly Rebuilder rebuilder = A.Fake(); @@ -33,6 +35,8 @@ namespace Squidex.Domain.Apps.Entities.Contents public BackupContentsTests() { + ct = cts.Token; + sut = new BackupContents(rebuilder, urlGenerator); } @@ -63,12 +67,12 @@ namespace Squidex.Domain.Apps.Entities.Contents await sut.BackupEventAsync(Envelope.Create(new AppCreated { Name = appId.Name - }), context); + }), context, ct); A.CallTo(() => writer.WriteJsonAsync(A._, A.That.Matches(x => x.Assets == assetsUrl && - x.AssetsApp == assetsUrlApp))) + x.AssetsApp == assetsUrlApp), ct)) .MustHaveHappened(); } @@ -91,7 +95,7 @@ namespace Squidex.Domain.Apps.Entities.Contents A.CallTo(() => urlGenerator.AssetContentBase(appId.Name)) .Returns(newAssetsUrlApp); - A.CallTo(() => reader.ReadJsonAsync(A._)) + A.CallTo(() => reader.ReadJsonAsync(A._, ct)) .Returns(new BackupContents.Urls { Assets = oldAssetsUrl, @@ -137,12 +141,12 @@ namespace Squidex.Domain.Apps.Entities.Contents await sut.RestoreEventAsync(Envelope.Create(new AppCreated { Name = appId.Name - }), context); + }), context, ct); await sut.RestoreEventAsync(Envelope.Create(new ContentUpdated { Data = data - }), context); + }), context, ct); Assert.Equal(updateData, data); } @@ -165,37 +169,37 @@ namespace Squidex.Domain.Apps.Entities.Contents { ContentId = contentId1, SchemaId = schemaId1 - }), context); + }), context, ct); await sut.RestoreEventAsync(ContentEvent(new ContentCreated { ContentId = contentId2, SchemaId = schemaId1 - }), context); + }), context, ct); await sut.RestoreEventAsync(ContentEvent(new ContentCreated { ContentId = contentId3, SchemaId = schemaId2 - }), context); + }), context, ct); await sut.RestoreEventAsync(ContentEvent(new ContentDeleted { ContentId = contentId2, SchemaId = schemaId1 - }), context); + }), context, ct); await sut.RestoreEventAsync(Envelope.Create(new SchemaDeleted { SchemaId = schemaId2 - }), context); + }), context, ct); var rebuildContents = new HashSet(); - A.CallTo(() => rebuilder.InsertManyAsync(A>._, A._, A._)) - .Invokes((IEnumerable source, int _, CancellationToken _) => rebuildContents.AddRange(source)); + A.CallTo(() => rebuilder.InsertManyAsync(A>._, A._, ct)) + .Invokes(x => rebuildContents.AddRange(x.GetArgument>(0)!)); - await sut.RestoreAsync(context); + await sut.RestoreAsync(context, ct); Assert.Equal(new HashSet { @@ -208,11 +212,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { @event.AppId = appId; - var envelope = Envelope.Create(@event); - - envelope.SetAggregateId(DomainId.Combine(appId.Id, @event.ContentId)); - - return envelope; + return Envelope.Create(@event).SetAggregateId(DomainId.Combine(appId.Id, @event.ContentId)); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentSchedulerGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentSchedulerGrainTests.cs new file mode 100644 index 000000000..5a512b6eb --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentSchedulerGrainTests.cs @@ -0,0 +1,129 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using System.Threading.Tasks; +using FakeItEasy; +using NodaTime; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Log; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public class ContentSchedulerGrainTests + { + 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; + + public ContentSchedulerGrainTests() + { + sut = new ContentSchedulerGrain(contentRepository, commandBus, clock, A.Fake()); + } + + [Fact] + public async Task Should_change_scheduled_items() + { + var now = SystemClock.Instance.GetCurrentInstant(); + + var content1 = new ContentEntity + { + AppId = appId, + Id = DomainId.NewGuid(), + ScheduleJob = new ScheduleJob(DomainId.NewGuid(), Status.Archived, null!, now) + }; + + var content2 = new ContentEntity + { + AppId = appId, + Id = DomainId.NewGuid(), + ScheduleJob = new ScheduleJob(DomainId.NewGuid(), Status.Draft, null!, now) + }; + + A.CallTo(() => clock.GetCurrentInstant()) + .Returns(now); + + A.CallTo(() => contentRepository.QueryScheduledWithoutDataAsync(now, default)) + .Returns(new[] { content1, content2 }.ToAsyncEnumerable()); + + await sut.PublishAsync(); + + A.CallTo(() => commandBus.PublishAsync( + A.That.Matches(x => + x.ContentId == content1.Id && + x.Status == content1.ScheduleJob.Status && + x.StatusJobId == content1.ScheduleJob.Id))) + .MustHaveHappened(); + + A.CallTo(() => commandBus.PublishAsync( + A.That.Matches(x => + x.ContentId == content2.Id && + x.Status == content2.ScheduleJob.Status && + x.StatusJobId == content2.ScheduleJob.Id))) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_not_change_status_if_content_has_no_schedule_job() + { + var now = SystemClock.Instance.GetCurrentInstant(); + + var content1 = new ContentEntity + { + AppId = appId, + Id = DomainId.NewGuid(), + ScheduleJob = null, + }; + + A.CallTo(() => clock.GetCurrentInstant()) + .Returns(now); + + A.CallTo(() => contentRepository.QueryScheduledWithoutDataAsync(now, default)) + .Returns(new[] { content1 }.ToAsyncEnumerable()); + + await sut.PublishAsync(); + + A.CallTo(() => commandBus.PublishAsync(A._)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_reset_job_if_content_not_found_anymore() + { + var now = SystemClock.Instance.GetCurrentInstant(); + + var content1 = new ContentEntity + { + AppId = appId, + Id = DomainId.NewGuid(), + ScheduleJob = new ScheduleJob(DomainId.NewGuid(), Status.Archived, null!, now) + }; + + A.CallTo(() => clock.GetCurrentInstant()) + .Returns(now); + + A.CallTo(() => contentRepository.QueryScheduledWithoutDataAsync(now, default)) + .Returns(new[] { content1 }.ToAsyncEnumerable()); + + A.CallTo(() => commandBus.PublishAsync(A._)) + .Throws(new DomainObjectNotFoundException(content1.Id.ToString())); + + await sut.PublishAsync(); + + A.CallTo(() => contentRepository.ResetScheduledAsync(content1.UniqueId, default)) + .MustHaveHappened(); + } + } +} 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 2a3335b91..2ac6a6a6c 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentsSearchSourceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentsSearchSourceTests.cs @@ -27,6 +27,8 @@ namespace Squidex.Domain.Apps.Entities.Contents { public class ContentsSearchSourceTests { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; private readonly IAppProvider appProvider = A.Fake(); private readonly IUrlGenerator urlGenerator = A.Fake(); private readonly ITextIndex contentIndex = A.Fake(); @@ -39,7 +41,9 @@ namespace Squidex.Domain.Apps.Entities.Contents public ContentsSearchSourceTests() { - A.CallTo(() => appProvider.GetSchemasAsync(appId.Id)) + ct = cts.Token; + + A.CallTo(() => appProvider.GetSchemasAsync(appId.Id, ct)) .Returns(new List { Mocks.Schema(appId, schemaId1), @@ -55,7 +59,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { var content = new ContentEntity { Id = DomainId.NewGuid(), SchemaId = schemaId1 }; - await TestContentAsyc(content, "Content"); + await TestContentAsync(content, "Content"); } [Fact] @@ -80,7 +84,7 @@ namespace Squidex.Domain.Apps.Entities.Contents SchemaId = schemaId1 }; - await TestContentAsyc(content, "hello, world"); + await TestContentAsync(content, "hello, world"); } [Fact] @@ -101,7 +105,7 @@ namespace Squidex.Domain.Apps.Entities.Contents SchemaId = schemaId1 }; - await TestContentAsyc(content, "hello"); + await TestContentAsync(content, "hello"); } [Fact] @@ -122,7 +126,7 @@ namespace Squidex.Domain.Apps.Entities.Contents SchemaId = schemaId1 }; - await TestContentAsyc(content, "hello"); + await TestContentAsync(content, "hello"); } [Fact] @@ -148,7 +152,7 @@ namespace Squidex.Domain.Apps.Entities.Contents SchemaId = schemaId1 }; - await TestContentAsyc(content, "resolved"); + await TestContentAsync(content, "resolved"); } [Fact] @@ -156,11 +160,11 @@ namespace Squidex.Domain.Apps.Entities.Contents { var ctx = ContextWithPermissions(); - var result = await sut.SearchAsync("query", ctx, default); + var result = await sut.SearchAsync("query", ctx, ct); Assert.Empty(result); - A.CallTo(() => contentIndex.SearchAsync(ctx.App, A._, A._)) + A.CallTo(() => contentIndex.SearchAsync(ctx.App, A._, A._, ct)) .MustNotHaveHappened(); } @@ -169,18 +173,18 @@ namespace Squidex.Domain.Apps.Entities.Contents { var ctx = ContextWithPermissions(schemaId1, schemaId2); - A.CallTo(() => contentIndex.SearchAsync(ctx.App, A.That.Matches(x => x.Text == "query~"), ctx.Scope())) + A.CallTo(() => contentIndex.SearchAsync(ctx.App, A.That.Matches(x => x.Text == "query~"), ctx.Scope(), ct)) .Returns(new List()); - var result = await sut.SearchAsync("query", ctx, default); + var result = await sut.SearchAsync("query", ctx, ct); Assert.Empty(result); - A.CallTo(() => contentQuery.QueryAsync(ctx, A._, A._)) + A.CallTo(() => contentQuery.QueryAsync(ctx, A._, ct)) .MustNotHaveHappened(); } - private async Task TestContentAsyc(ContentEntity content, string expectedName) + private async Task TestContentAsync(ContentEntity content, string expectedName) { content.AppId = appId; @@ -188,16 +192,16 @@ namespace Squidex.Domain.Apps.Entities.Contents var ids = new List { content.Id }; - A.CallTo(() => contentIndex.SearchAsync(ctx.App, A.That.Matches(x => x.Text == "query~" && x.Filter != null), ctx.Scope())) + A.CallTo(() => contentIndex.SearchAsync(ctx.App, A.That.Matches(x => x.Text == "query~" && x.Filter != null), ctx.Scope(), ct)) .Returns(ids); - A.CallTo(() => contentQuery.QueryAsync(ctx, A.That.HasIds(ids), A._)) + A.CallTo(() => contentQuery.QueryAsync(ctx, A.That.HasIds(ids), ct)) .Returns(ResultList.CreateFrom(1, content)); A.CallTo(() => urlGenerator.ContentUI(appId, schemaId1, content.Id)) .Returns("content-url"); - var result = await sut.SearchAsync("query", ctx, default); + var result = await sut.SearchAsync("query", ctx, ct); result.Should().BeEquivalentTo( new SearchResults() 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 new file mode 100644 index 000000000..3a15666e4 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterDeleterTests.cs @@ -0,0 +1,51 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +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/DefaultWorkflowsValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultWorkflowsValidatorTests.cs index 1a7fc28e7..12f78f4a1 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultWorkflowsValidatorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultWorkflowsValidatorTests.cs @@ -30,10 +30,10 @@ namespace Squidex.Domain.Apps.Entities.Contents { var schema = Mocks.Schema(appId, schemaId, new Schema(schemaId.Name)); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A._, false)) + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A._, false, default)) .Returns(Task.FromResult(null)); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false)) + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false, default)) .Returns(schema); sut = new DefaultWorkflowsValidator(appProvider); 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 e56718f5a..ecaa595bb 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 @@ -95,10 +95,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject schema = Mocks.Schema(AppNamedId, SchemaNamedId, schemaDef); - A.CallTo(() => appProvider.GetAppAsync(AppName, false)) + A.CallTo(() => appProvider.GetAppAsync(AppName, false, default)) .Returns(app); - A.CallTo(() => appProvider.GetAppWithSchemaAsync(AppId, SchemaId, false)) + A.CallTo(() => appProvider.GetAppWithSchemaAsync(AppId, SchemaId, false, default)) .Returns((app, schema)); A.CallTo(() => scriptEngine.TransformAsync(A._, A._, ScriptOptions(), default)) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs index 42b8dc1e9..0ddeee58c 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs @@ -87,7 +87,7 @@ namespace Squidex.Domain.Apps.Entities.Contents var workflows = Workflows.Empty.Set(workflow).Set(DomainId.NewGuid(), simpleWorkflow); - A.CallTo(() => appProvider.GetAppAsync(appId.Id, false)) + A.CallTo(() => appProvider.GetAppAsync(appId.Id, false, default)) .Returns(app); A.CallTo(() => app.Workflows) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs index 711b4199c..f0ddec990 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs @@ -50,7 +50,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL public GraphQLTestBase() { - A.CallTo(() => userResolver.QueryManyAsync(A._)) + A.CallTo(() => userResolver.QueryManyAsync(A._, default)) .ReturnsLazily(x => { var ids = x.GetArgument(0)!; @@ -107,7 +107,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var appProvider = A.Fake(); - A.CallTo(() => appProvider.GetSchemasAsync(TestApp.Default.Id)) + A.CallTo(() => appProvider.GetSchemasAsync(TestApp.Default.Id, default)) .Returns(schemas.ToList()); var dataLoaderContext = (IDataLoaderContextAccessor)new DataLoaderContextAccessor(); 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 92255dec2..6294976a3 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 @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using FakeItEasy; using LoremNET; @@ -73,7 +74,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb private async Task SetupAsync(MongoContentRepository contentRepository, IMongoDatabase database) { - await contentRepository.InitializeAsync(); + await contentRepository.InitializeAsync(default); await database.RunCommandAsync("{ profile : 0 }"); await database.DropCollectionAsync("system.profile"); @@ -146,7 +147,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb { var appProvider = A.Fake(); - A.CallTo(() => appProvider.GetSchemaAsync(A._, A._, false)) + A.CallTo(() => appProvider.GetSchemaAsync(A._, A._, false, A._)) .ReturnsLazily(x => Task.FromResult(CreateSchema(x.GetArgument(0)!, x.GetArgument(1)!))); return appProvider; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryIntegrationTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryIntegrationTests.cs index a40474731..5ed9469e9 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryIntegrationTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryIntegrationTests.cs @@ -93,7 +93,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb { var time = SystemClock.Instance.GetCurrentInstant(); - await _.ContentRepository.QueryScheduledWithoutDataAsync(time, _ => Task.CompletedTask); + await _.ContentRepository.QueryScheduledWithoutDataAsync(time).ToListAsync(); } [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 0320061f4..7d9db0fc6 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 @@ -21,6 +21,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { public class ContentEnricherTests { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; private readonly IAppProvider appProvider = A.Fake(); private readonly ISchemaEntity schema; private readonly Context requestContext; @@ -43,11 +45,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries public ContentEnricherTests() { + ct = cts.Token; + requestContext = new Context(Mocks.ApiUser(), Mocks.App(appId)); schema = Mocks.Schema(appId, schemaId); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false)) + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false, ct)) .Returns(schema); } @@ -61,18 +65,18 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries var sut = new ContentEnricher(new[] { step1, step2 }, appProvider); - await sut.EnrichAsync(source, requestContext, default); + await sut.EnrichAsync(source, requestContext, ct); - A.CallTo(() => step1.EnrichAsync(requestContext, A._)) + A.CallTo(() => step1.EnrichAsync(requestContext, ct)) .MustHaveHappened(); - A.CallTo(() => step2.EnrichAsync(requestContext, A._)) + A.CallTo(() => step2.EnrichAsync(requestContext, ct)) .MustHaveHappened(); - A.CallTo(() => step1.EnrichAsync(requestContext, A>._, A._, A._)) + A.CallTo(() => step1.EnrichAsync(requestContext, A>._, A._, ct)) .MustNotHaveHappened(); - A.CallTo(() => step2.EnrichAsync(requestContext, A>._, A._, A._)) + A.CallTo(() => step2.EnrichAsync(requestContext, A>._, A._, ct)) .MustNotHaveHappened(); } @@ -86,18 +90,18 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries var sut = new ContentEnricher(new[] { step1, step2 }, appProvider); - await sut.EnrichAsync(source, false, requestContext, default); + await sut.EnrichAsync(source, false, requestContext, ct); - A.CallTo(() => step1.EnrichAsync(requestContext, A._)) + A.CallTo(() => step1.EnrichAsync(requestContext, ct)) .MustHaveHappened(); - A.CallTo(() => step2.EnrichAsync(requestContext, A._)) + A.CallTo(() => step2.EnrichAsync(requestContext, ct)) .MustHaveHappened(); - A.CallTo(() => step1.EnrichAsync(requestContext, A>._, A._, A._)) + A.CallTo(() => step1.EnrichAsync(requestContext, A>._, A._, ct)) .MustHaveHappened(); - A.CallTo(() => step2.EnrichAsync(requestContext, A>._, A._, A._)) + A.CallTo(() => step2.EnrichAsync(requestContext, A>._, A._, ct)) .MustHaveHappened(); } @@ -111,12 +115,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries var sut = new ContentEnricher(new[] { step1, step2 }, appProvider); - await sut.EnrichAsync(source, false, requestContext, default); + await sut.EnrichAsync(source, false, requestContext, ct); Assert.Same(schema, step1.Schema); Assert.Same(schema, step1.Schema); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false)) + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false, ct)) .MustHaveHappenedOnceExactly(); } @@ -127,7 +131,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries var sut = new ContentEnricher(Enumerable.Empty(), appProvider); - var result = await sut.EnrichAsync(source, true, requestContext, default); + var result = await sut.EnrichAsync(source, true, requestContext, ct); Assert.NotSame(source.Data, result.Data); } @@ -139,7 +143,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries var sut = new ContentEnricher(Enumerable.Empty(), appProvider); - var result = await sut.EnrichAsync(source, false, requestContext, default); + var result = await sut.EnrichAsync(source, false, requestContext, ct); Assert.Same(source.Data, result.Data); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs index 22aa0d054..0457b44ce 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryParserTests.cs @@ -129,7 +129,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries [Fact] public async Task Should_convert_full_text_query_to_filter_with_other_filter() { - A.CallTo(() => textIndex.SearchAsync(requestContext.App, A.That.Matches(x => x.Text == "Hello"), requestContext.Scope())) + A.CallTo(() => textIndex.SearchAsync(requestContext.App, A.That.Matches(x => x.Text == "Hello"), requestContext.Scope(), default)) .Returns(new List { DomainId.Create("1"), DomainId.Create("2") }); var query = Q.Empty.WithODataQuery("$search=Hello&$filter=data/firstName/iv eq 'ABC'"); @@ -142,7 +142,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries [Fact] public async Task Should_convert_full_text_query_to_filter() { - A.CallTo(() => textIndex.SearchAsync(requestContext.App, A.That.Matches(x => x.Text == "Hello"), requestContext.Scope())) + A.CallTo(() => textIndex.SearchAsync(requestContext.App, A.That.Matches(x => x.Text == "Hello"), requestContext.Scope(), default)) .Returns(new List { DomainId.Create("1"), DomainId.Create("2") }); var query = Q.Empty.WithODataQuery("$search=Hello"); @@ -155,7 +155,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries [Fact] public async Task Should_convert_full_text_query_to_filter_if_single_id_found() { - A.CallTo(() => textIndex.SearchAsync(requestContext.App, A.That.Matches(x => x.Text == "Hello"), requestContext.Scope())) + A.CallTo(() => textIndex.SearchAsync(requestContext.App, A.That.Matches(x => x.Text == "Hello"), requestContext.Scope(), default)) .Returns(new List { DomainId.Create("1") }); var query = Q.Empty.WithODataQuery("$search=Hello"); @@ -168,7 +168,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries [Fact] public async Task Should_convert_full_text_query_to_filter_if_index_returns_null() { - A.CallTo(() => textIndex.SearchAsync(requestContext.App, A.That.Matches(x => x.Text == "Hello"), requestContext.Scope())) + A.CallTo(() => textIndex.SearchAsync(requestContext.App, A.That.Matches(x => x.Text == "Hello"), requestContext.Scope(), default)) .Returns(Task.FromResult?>(null)); var query = Q.Empty.WithODataQuery("$search=Hello"); @@ -181,7 +181,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries [Fact] public async Task Should_convert_full_text_query_to_filter_if_index_returns_empty() { - A.CallTo(() => textIndex.SearchAsync(requestContext.App, A.That.Matches(x => x.Text == "Hello"), requestContext.Scope())) + A.CallTo(() => textIndex.SearchAsync(requestContext.App, A.That.Matches(x => x.Text == "Hello"), requestContext.Scope(), default)) .Returns(new List()); var query = Q.Empty.WithODataQuery("$search=Hello"); @@ -194,7 +194,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries [Fact] public async Task Should_convert_geo_query_to_filter() { - A.CallTo(() => textIndex.SearchAsync(requestContext.App, new GeoQuery(schemaId.Id, "geo.iv", 10, 20, 30), requestContext.Scope())) + A.CallTo(() => textIndex.SearchAsync(requestContext.App, new GeoQuery(schemaId.Id, "geo.iv", 10, 20, 30), requestContext.Scope(), default)) .Returns(new List { DomainId.Create("1"), DomainId.Create("2") }); var query = Q.Empty.WithODataQuery("$filter=geo.distance(data/geo/iv, geography'POINT(20 10)') lt 30.0"); @@ -207,7 +207,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries [Fact] public async Task Should_convert_geo_query_to_filter_if_single_id_found() { - A.CallTo(() => textIndex.SearchAsync(requestContext.App, new GeoQuery(schemaId.Id, "geo.iv", 10, 20, 30), requestContext.Scope())) + A.CallTo(() => textIndex.SearchAsync(requestContext.App, new GeoQuery(schemaId.Id, "geo.iv", 10, 20, 30), requestContext.Scope(), default)) .Returns(new List { DomainId.Create("1") }); var query = Q.Empty.WithODataQuery("$filter=geo.distance(data/geo/iv, geography'POINT(20 10)') lt 30.0"); @@ -220,7 +220,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries [Fact] public async Task Should_convert_geo_query_to_filter_if_index_returns_null() { - A.CallTo(() => textIndex.SearchAsync(requestContext.App, new GeoQuery(schemaId.Id, "geo.iv", 10, 20, 30), requestContext.Scope())) + A.CallTo(() => textIndex.SearchAsync(requestContext.App, new GeoQuery(schemaId.Id, "geo.iv", 10, 20, 30), requestContext.Scope(), default)) .Returns(Task.FromResult?>(null)); var query = Q.Empty.WithODataQuery("$filter=geo.distance(data/geo/iv, geography'POINT(20 10)') lt 30.0"); @@ -233,7 +233,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries [Fact] public async Task Should_convert_geo_query_to_filter_if_index_returns_empty() { - A.CallTo(() => textIndex.SearchAsync(requestContext.App, new GeoQuery(schemaId.Id, "geo.iv", 10, 20, 30), requestContext.Scope())) + A.CallTo(() => textIndex.SearchAsync(requestContext.App, new GeoQuery(schemaId.Id, "geo.iv", 10, 20, 30), requestContext.Scope(), default)) .Returns(new List()); var query = Q.Empty.WithODataQuery("$filter=geo.distance(data/geo/iv, geography'POINT(20 10)') lt 30.0"); 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 9e75ef113..2c6feb331 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 @@ -28,6 +28,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { public class ContentQueryServiceTests { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; private readonly IAppProvider appProvider = A.Fake(); private readonly IContentEnricher contentEnricher = A.Fake(); private readonly IContentRepository contentRepository = A.Fake(); @@ -41,6 +43,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries public ContentQueryServiceTests() { + ct = cts.Token; + var schemaDef = new Schema(schemaId.Name) .Publish() @@ -50,10 +54,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries SetupEnricher(); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name, A._)) + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name, A._, ct)) .Returns(schema); - A.CallTo(() => appProvider.GetSchemasAsync(appId.Id)) + A.CallTo(() => appProvider.GetSchemasAsync(appId.Id, ct)) .Returns(new List { schema }); A.CallTo(() => queryParser.ParseAsync(A._, A._, A._)) @@ -77,10 +81,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries var requestContext = CreateContext(); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, true)) + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, true, ct)) .Returns(schema); - var result = await sut.GetSchemaOrThrowAsync(requestContext, input); + var result = await sut.GetSchemaOrThrowAsync(requestContext, input, ct); Assert.Equal(schema, result); } @@ -92,10 +96,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries var requestContext = CreateContext(); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name, true)) + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name, true, ct)) .Returns(schema); - var result = await sut.GetSchemaOrThrowAsync(requestContext, input); + var result = await sut.GetSchemaOrThrowAsync(requestContext, input, ct); Assert.Equal(schema, result); } @@ -105,10 +109,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { var requestContext = CreateContext(); - A.CallTo(() => appProvider.GetSchemaAsync(A._, A._, true)) + A.CallTo(() => appProvider.GetSchemaAsync(A._, A._, true, ct)) .Returns((ISchemaEntity?)null); - await Assert.ThrowsAsync(() => sut.GetSchemaOrThrowAsync(requestContext, schemaId.Name)); + await Assert.ThrowsAsync(() => sut.GetSchemaOrThrowAsync(requestContext, schemaId.Name, ct)); } [Fact] @@ -121,7 +125,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries A.CallTo(() => contentRepository.FindContentAsync(requestContext.App, schema, content.Id, A._, A._)) .Returns(CreateContent(DomainId.NewGuid())); - await Assert.ThrowsAsync(() => sut.FindAsync(requestContext, schemaId.Name, content.Id)); + await Assert.ThrowsAsync(() => sut.FindAsync(requestContext, schemaId.Name, content.Id, ct: ct)); } [Fact] @@ -134,7 +138,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries A.CallTo(() => contentRepository.FindContentAsync(requestContext.App, schema, content.Id, A._, A._)) .Returns(null); - Assert.Null(await sut.FindAsync(requestContext, schemaId.Name, content.Id)); + var result = await sut.FindAsync(requestContext, schemaId.Name, content.Id, ct: ct); + + Assert.Null(result); } [Fact] @@ -147,7 +153,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries A.CallTo(() => contentRepository.FindContentAsync(requestContext.App, schema, schema.Id, SearchScope.Published, A._)) .Returns(content); - var result = await sut.FindAsync(requestContext, schemaId.Name, DomainId.Create("_schemaId_")); + var result = await sut.FindAsync(requestContext, schemaId.Name, DomainId.Create("_schemaId_"), ct: ct); AssertContent(content, result); } @@ -166,7 +172,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries A.CallTo(() => contentRepository.FindContentAsync(requestContext.App, schema, content.Id, scope, A._)) .Returns(content); - var result = await sut.FindAsync(requestContext, schemaId.Name, content.Id); + var result = await sut.FindAsync(requestContext, schemaId.Name, content.Id, ct: ct); AssertContent(content, result); } @@ -181,7 +187,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries A.CallTo(() => contentVersionLoader.GetAsync(appId.Id, content.Id, 13)) .Returns(content); - var result = await sut.FindAsync(requestContext, schemaId.Name, content.Id, 13); + var result = await sut.FindAsync(requestContext, schemaId.Name, content.Id, 13, ct); AssertContent(content, result); } @@ -191,7 +197,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { var requestContext = CreateContext(allowSchema: false); - await Assert.ThrowsAsync(() => sut.QueryAsync(requestContext, schemaId.Name, Q.Empty, default)); + await Assert.ThrowsAsync(() => sut.QueryAsync(requestContext, schemaId.Name, Q.Empty, ct)); } [Theory] @@ -211,7 +217,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries A.CallTo(() => contentRepository.QueryAsync(requestContext.App, schema, q, scope, A._)) .Returns(ResultList.CreateFrom(5, content1, content2)); - var result = await sut.QueryAsync(requestContext, schemaId.Name, q, default); + var result = await sut.QueryAsync(requestContext, schemaId.Name, q, ct); Assert.Equal(5, result.Total); @@ -235,10 +241,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries var q = Q.Empty.WithIds(ids); A.CallTo(() => contentRepository.QueryAsync(requestContext.App, - A>.That.Matches(x => x.Count == 1), q, scope, A._)) + A>.That.Matches(x => x.Count == 1), q, scope, + A._)) .Returns(ResultList.Create(5, contents)); - var result = await sut.QueryAsync(requestContext, q, default); + var result = await sut.QueryAsync(requestContext, q, ct); Assert.Equal(5, result.Total); @@ -258,10 +265,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries var q = Q.Empty.WithIds(ids); A.CallTo(() => contentRepository.QueryAsync(requestContext.App, - A>.That.Matches(x => x.Count == 0), q, SearchScope.All, A._)) + A>.That.Matches(x => x.Count == 0), q, SearchScope.All, + A._)) .Returns(ResultList.Create(0, ids.Select(CreateContent))); - var result = await sut.QueryAsync(requestContext, q, default); + var result = await sut.QueryAsync(requestContext, q, ct); Assert.Empty(result); } @@ -271,10 +279,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { var requestContext = CreateContext(permissionId: Permissions.AppContentsReadOwn); - await sut.QueryAsync(requestContext, schemaId.Name, Q.Empty, default); + await sut.QueryAsync(requestContext, schemaId.Name, Q.Empty, ct); A.CallTo(() => contentRepository.QueryAsync(requestContext.App, schema, - A.That.Matches(x => Equals(x.CreatedBy, requestContext.User.Token())), SearchScope.Published, A._)) + A.That.Matches(x => Equals(x.CreatedBy, requestContext.User.Token())), SearchScope.Published, A + ._)) .MustHaveHappened(); } @@ -283,16 +292,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { var requestContext = CreateContext(permissionId: Permissions.AppContentsRead); - await sut.QueryAsync(requestContext, schemaId.Name, Q.Empty, default); + await sut.QueryAsync(requestContext, schemaId.Name, Q.Empty, ct); A.CallTo(() => contentRepository.QueryAsync(requestContext.App, schema, - A.That.Matches(x => x.CreatedBy == null), SearchScope.Published, A._)) + A.That.Matches(x => x.CreatedBy == null), SearchScope.Published, + A._)) .MustHaveHappened(); } private void SetupEnricher() { - A.CallTo(() => contentEnricher.EnrichAsync(A>._, A._, A._)) + A.CallTo(() => contentEnricher.EnrichAsync(A>._, A._, ct)) .ReturnsLazily(x => { var input = x.GetArgument>(0)!; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesFluidExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesFluidExtensionTests.cs index 9fcf9b253..224f32aa7 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesFluidExtensionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesFluidExtensionTests.cs @@ -39,7 +39,7 @@ namespace Squidex.Domain.Apps.Entities.Contents new ReferencesFluidExtension(services) }; - A.CallTo(() => appProvider.GetAppAsync(appId.Id, false)) + A.CallTo(() => appProvider.GetAppAsync(appId.Id, false, default)) .Returns(Mocks.App(appId)); sut = new FluidTemplateEngine(extensions); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesJintExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesJintExtensionTests.cs index 69da3944a..a7f6336ed 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesJintExtensionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ReferencesJintExtensionTests.cs @@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Entities.Contents new ReferencesJintExtension(services) }; - A.CallTo(() => appProvider.GetAppAsync(appId.Id, false)) + A.CallTo(() => appProvider.GetAppAsync(appId.Id, false, default)) .Returns(Mocks.App(appId)); sut = new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), extensions) 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 0090c78e6..4809b7428 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 @@ -6,6 +6,7 @@ // ========================================================================== using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using FakeItEasy; using Squidex.Domain.Apps.Core.TestHelpers; @@ -17,12 +18,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text { public class CachingTextIndexerStateTests { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; private readonly ITextIndexerState inner = A.Fake(); private readonly DomainId contentId = DomainId.NewGuid(); private readonly CachingTextIndexerState sut; public CachingTextIndexerStateTests() { + ct = cts.Token; + sut = new CachingTextIndexerState(inner); } @@ -38,16 +43,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text [contentId] = state }; - A.CallTo(() => inner.GetAsync(A>.That.Is(contentIds))) + A.CallTo(() => inner.GetAsync(A>.That.Is(contentIds), ct)) .Returns(states); - var found1 = await sut.GetAsync(HashSet.Of(contentId)); - var found2 = await sut.GetAsync(HashSet.Of(contentId)); + var found1 = await sut.GetAsync(HashSet.Of(contentId), ct); + var found2 = await sut.GetAsync(HashSet.Of(contentId), ct); Assert.Same(state, found1[contentId]); Assert.Same(state, found2[contentId]); - A.CallTo(() => inner.GetAsync(A>.That.Is(contentIds))) + A.CallTo(() => inner.GetAsync(A>.That.Is(contentIds), ct)) .MustHaveHappenedOnceExactly(); } @@ -56,16 +61,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text { var contentIds = HashSet.Of(contentId); - A.CallTo(() => inner.GetAsync(A>.That.Is(contentIds))) + A.CallTo(() => inner.GetAsync(A>.That.Is(contentIds), ct)) .Returns(new Dictionary()); - var found1 = await sut.GetAsync(HashSet.Of(contentId)); - var found2 = await sut.GetAsync(HashSet.Of(contentId)); + var found1 = await sut.GetAsync(HashSet.Of(contentId), ct); + var found2 = await sut.GetAsync(HashSet.Of(contentId), ct); Assert.Empty(found1); Assert.Empty(found2); - A.CallTo(() => inner.GetAsync(A>.That.Is(contentIds))) + A.CallTo(() => inner.GetAsync(A>.That.Is(contentIds), ct)) .MustHaveHappenedOnceExactly(); } @@ -76,21 +81,18 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text var state = new TextContentState { UniqueContentId = contentId }; - await sut.SetAsync(new List - { - state - }); + await sut.SetAsync(new List { state }, ct); - var found1 = await sut.GetAsync(contentIds); - var found2 = await sut.GetAsync(contentIds); + var found1 = await sut.GetAsync(contentIds, ct); + var found2 = await sut.GetAsync(contentIds, ct); Assert.Same(state, found1[contentId]); Assert.Same(state, found2[contentId]); - A.CallTo(() => inner.SetAsync(A>.That.IsSameSequenceAs(state))) + A.CallTo(() => inner.SetAsync(A>.That.IsSameSequenceAs(state), ct)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => inner.GetAsync(A>._)) + A.CallTo(() => inner.GetAsync(A>._, A._)) .MustNotHaveHappened(); } @@ -104,23 +106,23 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text await sut.SetAsync(new List { state - }); + }, ct); await sut.SetAsync(new List { new TextContentState { UniqueContentId = contentId, IsDeleted = true } - }); + }, ct); - var found1 = await sut.GetAsync(contentIds); - var found2 = await sut.GetAsync(contentIds); + var found1 = await sut.GetAsync(contentIds, ct); + var found2 = await sut.GetAsync(contentIds, ct); Assert.Empty(found1); Assert.Empty(found2); - A.CallTo(() => inner.SetAsync(A>.That.Matches(x => x.Count == 1 && x[0].IsDeleted))) + A.CallTo(() => inner.SetAsync(A>.That.Matches(x => x.Count == 1 && x[0].IsDeleted), ct)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => inner.GetAsync(A>._)) + A.CallTo(() => inner.GetAsync(A>._, ct)) .MustNotHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerTests_Elastic.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerTests_Elastic.cs index 3042627db..abe98770e 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerTests_Elastic.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerTests_Elastic.cs @@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text { var index = new ElasticSearchTextIndex("http://localhost:9200", "squidex", true); - await index.InitializeAsync(); + await index.InitializeAsync(default); return index; } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerTests_Mongo.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerTests_Mongo.cs index 16c96ae83..fd35a392d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerTests_Mongo.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/TextIndexerTests_Mongo.cs @@ -37,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text var index = new MongoTextIndex(database, false); - await index.InitializeAsync(); + await index.InitializeAsync(default); return index; } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/BackupRulesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/BackupRulesTests.cs index abf687323..3cfcced01 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/BackupRulesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/BackupRulesTests.cs @@ -5,14 +5,15 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using FakeItEasy; using Squidex.Domain.Apps.Entities.Backup; -using Squidex.Domain.Apps.Entities.Rules.Indexes; +using Squidex.Domain.Apps.Entities.Rules.DomainObject; using Squidex.Domain.Apps.Events.Rules; using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; using Xunit; @@ -20,12 +21,17 @@ namespace Squidex.Domain.Apps.Entities.Rules { public class BackupRulesTests { - private readonly IRulesIndex index = A.Fake(); + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly Rebuilder rebuilder = A.Fake(); + private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); private readonly BackupRules sut; public BackupRulesTests() { - sut = new BackupRules(index); + ct = cts.Token; + + sut = new BackupRules(rebuilder); } [Fact] @@ -37,46 +43,51 @@ namespace Squidex.Domain.Apps.Entities.Rules [Fact] public async Task Should_restore_indices_for_all_non_deleted_rules() { - var appId = DomainId.NewGuid(); - var ruleId1 = DomainId.NewGuid(); var ruleId2 = DomainId.NewGuid(); var ruleId3 = DomainId.NewGuid(); - var context = new RestoreContext(appId, new UserMapping(RefToken.User("123")), A.Fake(), DomainId.NewGuid()); + var context = new RestoreContext(appId.Id, new UserMapping(RefToken.User("123")), A.Fake(), DomainId.NewGuid()); - await sut.RestoreEventAsync(Envelope.Create(new RuleCreated + await sut.RestoreEventAsync(AppEvent(new RuleCreated { RuleId = ruleId1 - }), context); + }), context, ct); - await sut.RestoreEventAsync(Envelope.Create(new RuleCreated + await sut.RestoreEventAsync(AppEvent(new RuleCreated { RuleId = ruleId2 - }), context); + }), context, ct); - await sut.RestoreEventAsync(Envelope.Create(new RuleCreated + await sut.RestoreEventAsync(AppEvent(new RuleCreated { RuleId = ruleId3 - }), context); + }), context, ct); - await sut.RestoreEventAsync(Envelope.Create(new RuleDeleted + await sut.RestoreEventAsync(AppEvent(new RuleDeleted { RuleId = ruleId3 - }), context); + }), context, ct); - HashSet? newIndex = null; + var rebuildAssets = new HashSet(); - A.CallTo(() => index.RebuildAsync(appId, A>._)) - .Invokes(new Action>((_, i) => newIndex = i)); + A.CallTo(() => rebuilder.InsertManyAsync(A>._, A._, ct)) + .Invokes(x => rebuildAssets.AddRange(x.GetArgument>(0)!)); - await sut.RestoreAsync(context); + await sut.RestoreAsync(context, ct); Assert.Equal(new HashSet { - ruleId1, - ruleId2 - }, newIndex); + DomainId.Combine(appId, ruleId1), + DomainId.Combine(appId, ruleId2) + }, rebuildAssets); + } + + private Envelope AppEvent(RuleEvent @event) + { + @event.AppId = appId; + + return Envelope.Create(@event).SetAggregateId(DomainId.Combine(appId.Id, @event.RuleId)); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/Guards/GuardRuleTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/Guards/GuardRuleTests.cs index 4ef1f85ef..621cabbe4 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/Guards/GuardRuleTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/Guards/GuardRuleTests.cs @@ -34,7 +34,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject.Guards public GuardRuleTests() { - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false)) + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false, default)) .Returns(Mocks.Schema(appId, schemaId)); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/Guards/Triggers/ContentChangedTriggerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/Guards/Triggers/ContentChangedTriggerTests.cs index 357994074..6f9ffc95c 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/Guards/Triggers/ContentChangedTriggerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/Guards/Triggers/ContentChangedTriggerTests.cs @@ -42,14 +42,14 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject.Guards.Triggers new ValidationError("Schema ID is required.", "Schemas") }); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A._, false)) + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A._, false, default)) .MustNotHaveHappened(); } [Fact] public async Task Should_add_error_if_schemas_ids_are_not_valid() { - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false)) + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false, default)) .Returns(Task.FromResult(null)); var trigger = new ContentChangedTriggerV2 @@ -92,7 +92,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject.Guards.Triggers [Fact] public async Task Should_not_add_error_if_schemas_ids_are_valid() { - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A._, false)) + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A._, false, default)) .Returns(Mocks.Schema(appId, schemaId)); var trigger = new ContentChangedTriggerV2 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 7a65fbf09..f4e664e65 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 @@ -50,7 +50,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject { await HandleAsync(new EnableRule(), 12); - A.CallTo(() => ruleEnricher.EnrichAsync(A._, requestContext)) + A.CallTo(() => ruleEnricher.EnrichAsync(A._, requestContext, default)) .MustNotHaveHappened(); } @@ -65,7 +65,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject Assert.Same(result, context.Result()); - A.CallTo(() => ruleEnricher.EnrichAsync(A._, requestContext)) + A.CallTo(() => ruleEnricher.EnrichAsync(A._, requestContext, default)) .MustNotHaveHappened(); } @@ -76,7 +76,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject var enriched = new RuleEntity(); - A.CallTo(() => ruleEnricher.EnrichAsync(result, requestContext)) + A.CallTo(() => ruleEnricher.EnrichAsync(result, requestContext, default)) .Returns(enriched); var context = 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 new file mode 100644 index 000000000..8fa75334b --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesCacheGrainTests.cs @@ -0,0 +1,102 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Entities.Rules.Repositories; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Rules.Indexes +{ + public class RulesCacheGrainTests + { + private readonly IRuleRepository ruleRepository = A.Fake(); + private readonly DomainId appId = DomainId.NewGuid(); + private readonly RulesCacheGrain sut; + + public RulesCacheGrainTests() + { + sut = new RulesCacheGrain(ruleRepository); + sut.ActivateAsync(appId.ToString()).Wait(); + } + + [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 411f3c141..2f6c6626e 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 @@ -22,14 +22,14 @@ namespace Squidex.Domain.Apps.Entities.Rules.Indexes { private readonly IGrainFactory grainFactory = A.Fake(); private readonly ICommandBus commandBus = A.Fake(); - private readonly IRulesByAppIndexGrain index = A.Fake(); + private readonly IRulesCacheGrain cache = 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(index); + A.CallTo(() => grainFactory.GetGrain(appId.Id.ToString(), null)) + .Returns(cache); sut = new RulesIndex(grainFactory); } @@ -39,7 +39,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Indexes { var rule = SetupRule(0); - A.CallTo(() => index.GetIdsAsync()) + A.CallTo(() => cache.GetRuleIdsAsync()) .Returns(new List { rule.Id }); var actual = await sut.GetRulesAsync(appId.Id); @@ -52,7 +52,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Indexes { var rule = SetupRule(-1); - A.CallTo(() => index.GetIdsAsync()) + A.CallTo(() => cache.GetRuleIdsAsync()) .Returns(new List { rule.Id }); var actual = await sut.GetRulesAsync(appId.Id); @@ -65,7 +65,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Indexes { var rule = SetupRule(0, true); - A.CallTo(() => index.GetIdsAsync()) + A.CallTo(() => cache.GetRuleIdsAsync()) .Returns(new List { rule.Id }); var actual = await sut.GetRulesAsync(appId.Id); @@ -86,7 +86,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Indexes await sut.HandleAsync(context); - A.CallTo(() => index.AddAsync(ruleId)) + A.CallTo(() => cache.AddAsync(ruleId)) .MustHaveHappened(); } @@ -103,18 +103,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Indexes await sut.HandleAsync(context); - A.CallTo(() => index.RemoveAsync(rule.Id)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_forward_call_if_rebuilding() - { - var rules = new HashSet(); - - await sut.RebuildAsync(appId.Id, rules); - - A.CallTo(() => index.RebuildAsync(rules)) + A.CallTo(() => cache.RemoveAsync(rule.Id)) .MustHaveHappened(); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleEnricherTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleEnricherTests.cs index 2842e830c..061145985 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleEnricherTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleEnricherTests.cs @@ -20,6 +20,8 @@ namespace Squidex.Domain.Apps.Entities.Rules.Queries { public class RuleEnricherTests { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; private readonly IRuleEventRepository ruleEventRepository = A.Fake(); private readonly IRequestCache requestCache = A.Fake(); private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); @@ -28,6 +30,8 @@ namespace Squidex.Domain.Apps.Entities.Rules.Queries public RuleEnricherTests() { + ct = cts.Token; + requestContext = Context.Anonymous(Mocks.App(appId)); sut = new RuleEnricher(ruleEventRepository, requestCache); @@ -38,7 +42,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Queries { var source = CreateRule(); - var result = await sut.EnrichAsync(source, requestContext); + var result = await sut.EnrichAsync(source, requestContext, ct); Assert.Equal(0, result.NumFailed); Assert.Equal(0, result.NumSucceeded); @@ -65,10 +69,10 @@ namespace Squidex.Domain.Apps.Entities.Rules.Queries LastExecuted = SystemClock.Instance.GetCurrentInstant() }; - A.CallTo(() => ruleEventRepository.QueryStatisticsByAppAsync(appId.Id, A._)) + A.CallTo(() => ruleEventRepository.QueryStatisticsByAppAsync(appId.Id, ct)) .Returns(new List { stats }); - await sut.EnrichAsync(source, requestContext); + await sut.EnrichAsync(source, requestContext, ct); A.CallTo(() => requestCache.AddDependency(source.UniqueId, source.Version)) .MustHaveHappened(); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleQueryServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleQueryServiceTests.cs index 01a0dcd67..e63fd1b41 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleQueryServiceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Queries/RuleQueryServiceTests.cs @@ -6,6 +6,7 @@ // ========================================================================== using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using FakeItEasy; using Squidex.Domain.Apps.Entities.Rules.Indexes; @@ -17,6 +18,8 @@ namespace Squidex.Domain.Apps.Entities.Rules.Queries { public class RuleQueryServiceTests { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; private readonly IRulesIndex rulesIndex = A.Fake(); private readonly IRuleEnricher ruleEnricher = A.Fake(); private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); @@ -25,6 +28,8 @@ namespace Squidex.Domain.Apps.Entities.Rules.Queries public RuleQueryServiceTests() { + ct = cts.Token; + requestContext = Context.Anonymous(Mocks.App(appId)); sut = new RuleQueryService(rulesIndex, ruleEnricher); @@ -43,13 +48,13 @@ namespace Squidex.Domain.Apps.Entities.Rules.Queries new RuleEntity() }; - A.CallTo(() => rulesIndex.GetRulesAsync(appId.Id)) + A.CallTo(() => rulesIndex.GetRulesAsync(appId.Id, ct)) .Returns(original); - A.CallTo(() => ruleEnricher.EnrichAsync(original, requestContext)) + A.CallTo(() => ruleEnricher.EnrichAsync(original, requestContext, ct)) .Returns(enriched); - var result = await sut.QueryAsync(requestContext); + var result = await sut.QueryAsync(requestContext, ct); Assert.Same(enriched, result); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerGrainTests.cs index f2127aa7f..e41493ded 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerGrainTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerGrainTests.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Threading; using System.Threading.Tasks; using FakeItEasy; using NodaTime; @@ -60,7 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Rules { var @event = CreateEvent(1, "MyAction", "{}"); - A.CallTo(() => ruleService.InvokeAsync(A._, A._)) + A.CallTo(() => ruleService.InvokeAsync(A._, A._, default)) .Throws(new InvalidOperationException()); await sut.HandleAsync(@event); @@ -77,7 +78,7 @@ namespace Squidex.Domain.Apps.Entities.Rules var event1 = CreateEvent(1, "MyAction", "{}", id); var event2 = CreateEvent(1, "MyAction", "{}", id); - A.CallTo(() => ruleService.InvokeAsync(A._, A._)) + A.CallTo(() => ruleService.InvokeAsync(A._, A._, default)) .ReturnsLazily(async () => { await Task.Delay(500); @@ -89,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Rules sut.HandleAsync(event1), sut.HandleAsync(event2)); - A.CallTo(() => ruleService.InvokeAsync(A._, A._)) + A.CallTo(() => ruleService.InvokeAsync(A._, A._, default)) .MustHaveHappenedOnceExactly(); } @@ -110,7 +111,7 @@ namespace Squidex.Domain.Apps.Entities.Rules var requestElapsed = TimeSpan.FromMinutes(1); var requestDump = "Dump"; - A.CallTo(() => ruleService.InvokeAsync(@event.Job.ActionName, @event.Job.ActionData)) + A.CallTo(() => ruleService.InvokeAsync(@event.Job.ActionName, @event.Job.ActionData, default)) .Returns((Result.Create(requestDump, result), requestElapsed)); var now = clock.GetCurrentInstant(); @@ -142,7 +143,8 @@ namespace Squidex.Domain.Apps.Entities.Rules x.ExecutionResult == result && x.Finished == now && x.JobNext == nextCall && - x.JobResult == jobResult))) + x.JobResult == jobResult), + A._)) .MustHaveHappened(); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs index 95dd2eadb..f12de352f 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using FakeItEasy; using Microsoft.Extensions.Caching.Memory; @@ -92,7 +93,7 @@ namespace Squidex.Domain.Apps.Entities.Rules await sut.EnqueueAsync(rule.RuleDef, rule.Id, @event); - A.CallTo(() => ruleEventRepository.EnqueueAsync(A._, (Exception?)null)) + A.CallTo(() => ruleEventRepository.EnqueueAsync(A._, (Exception?)null, default)) .MustNotHaveHappened(); } @@ -113,7 +114,7 @@ namespace Squidex.Domain.Apps.Entities.Rules await sut.EnqueueAsync(rule.RuleDef, rule.Id, @event); - A.CallTo(() => ruleEventRepository.EnqueueAsync(A._, (Exception?)null)) + A.CallTo(() => ruleEventRepository.EnqueueAsync(A._, (Exception?)null, default)) .MustNotHaveHappened(); } @@ -134,7 +135,7 @@ namespace Squidex.Domain.Apps.Entities.Rules await sut.EnqueueAsync(rule.RuleDef, rule.Id, @event); - A.CallTo(() => ruleEventRepository.EnqueueAsync(job, (Exception?)null)) + A.CallTo(() => ruleEventRepository.EnqueueAsync(job, (Exception?)null, default)) .MustHaveHappened(); } @@ -152,7 +153,7 @@ namespace Squidex.Domain.Apps.Entities.Rules await sut.On(@event); - A.CallTo(() => ruleEventRepository.EnqueueAsync(job1, (Exception?)null)) + A.CallTo(() => ruleEventRepository.EnqueueAsync(job1, (Exception?)null, default)) .MustHaveHappened(); } @@ -167,7 +168,7 @@ namespace Squidex.Domain.Apps.Entities.Rules await sut.On(@event.SetRestored(true)); - A.CallTo(() => ruleEventRepository.EnqueueAsync(A._, A._)) + A.CallTo(() => ruleEventRepository.EnqueueAsync(A._, A._, default)) .MustNotHaveHappened(); } @@ -176,7 +177,7 @@ namespace Squidex.Domain.Apps.Entities.Rules var rule1 = CreateRule(); var rule2 = CreateRule(); - A.CallTo(() => appProvider.GetRulesAsync(appId.Id)) + A.CallTo(() => appProvider.GetRulesAsync(appId.Id, A._)) .Returns(new List { rule1, rule2 }); A.CallTo(() => ruleService.CreateJobsAsync(@event, MatchingContext(rule1), default)) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/BackupSchemasTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/BackupSchemasTests.cs index 7f8d27bf0..0d9cb07f3 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/BackupSchemasTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/BackupSchemasTests.cs @@ -5,14 +5,16 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using FakeItEasy; using Squidex.Domain.Apps.Entities.Backup; -using Squidex.Domain.Apps.Entities.Schemas.Indexes; +using Squidex.Domain.Apps.Entities.Schemas.DomainObject; +using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events.Schemas; using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; using Xunit; @@ -20,12 +22,17 @@ namespace Squidex.Domain.Apps.Entities.Schemas { public class BackupSchemasTests { - private readonly ISchemasIndex index = A.Fake(); + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly Rebuilder rebuilder = A.Fake(); + private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); private readonly BackupSchemas sut; public BackupSchemasTests() { - sut = new BackupSchemas(index); + ct = cts.Token; + + sut = new BackupSchemas(rebuilder); } [Fact] @@ -37,46 +44,51 @@ namespace Squidex.Domain.Apps.Entities.Schemas [Fact] public async Task Should_restore_indices_for_all_non_deleted_schemas() { - var appId = DomainId.NewGuid(); - var schemaId1 = NamedId.Of(DomainId.NewGuid(), "my-schema1"); var schemaId2 = NamedId.Of(DomainId.NewGuid(), "my-schema2"); var schemaId3 = NamedId.Of(DomainId.NewGuid(), "my-schema3"); - var context = new RestoreContext(appId, new UserMapping(RefToken.User("123")), A.Fake(), DomainId.NewGuid()); + var context = new RestoreContext(appId.Id, new UserMapping(RefToken.User("123")), A.Fake(), DomainId.NewGuid()); - await sut.RestoreEventAsync(Envelope.Create(new SchemaCreated + await sut.RestoreEventAsync(AppEvent(new SchemaCreated { SchemaId = schemaId1 - }), context); + }), context, ct); - await sut.RestoreEventAsync(Envelope.Create(new SchemaCreated + await sut.RestoreEventAsync(AppEvent(new SchemaCreated { SchemaId = schemaId2 - }), context); + }), context, ct); - await sut.RestoreEventAsync(Envelope.Create(new SchemaCreated + await sut.RestoreEventAsync(AppEvent(new SchemaCreated { SchemaId = schemaId3 - }), context); + }), context, ct); - await sut.RestoreEventAsync(Envelope.Create(new SchemaDeleted + await sut.RestoreEventAsync(AppEvent(new SchemaDeleted { SchemaId = schemaId3 - }), context); + }), context, ct); - Dictionary? newIndex = null; + var rebuildContents = new HashSet(); - A.CallTo(() => index.RebuildAsync(appId, A>._)) - .Invokes(new Action>((_, i) => newIndex = i)); + A.CallTo(() => rebuilder.InsertManyAsync(A>._, A._, ct)) + .Invokes(x => rebuildContents.AddRange(x.GetArgument>(0)!)); - await sut.RestoreAsync(context); + await sut.RestoreAsync(context, ct); - Assert.Equal(new Dictionary + Assert.Equal(new HashSet { - [schemaId1.Name] = schemaId1.Id, - [schemaId2.Name] = schemaId2.Id - }, newIndex); + DomainId.Combine(appId, schemaId1.Id), + DomainId.Combine(appId, schemaId2.Id) + }, rebuildContents); + } + + private Envelope AppEvent(SchemaEvent @event) + { + @event.AppId = appId; + + return Envelope.Create(@event).SetAggregateId(DomainId.Combine(appId.Id, @event.SchemaId.Id)); } } } 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 new file mode 100644 index 000000000..b5daf552d --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasCacheGrainTests.cs @@ -0,0 +1,102 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Entities.Schemas.Repositories; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Schemas.Indexes +{ + public class SchemasCacheGrainTests + { + private readonly ISchemaRepository schemaRepository = A.Fake(); + private readonly DomainId appId = DomainId.NewGuid(); + private readonly SchemasCacheGrain sut; + + public SchemasCacheGrainTests() + { + sut = new SchemasCacheGrain(schemaRepository); + sut.ActivateAsync(appId.ToString()).Wait(); + } + + [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/SchemasIndexIntegrationTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexIntegrationTests.cs deleted file mode 100644 index 6154312f1..000000000 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexIntegrationTests.cs +++ /dev/null @@ -1,189 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Linq; -using System.Threading.Tasks; -using FakeItEasy; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Orleans; -using Orleans.Hosting; -using Orleans.TestingHost; -using Squidex.Caching; -using Squidex.Domain.Apps.Core; -using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Domain.Apps.Entities.Schemas.DomainObject; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Orleans; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Schemas.Indexes -{ - [Trait("Category", "Dependencies")] - public class SchemasIndexIntegrationTests - { - public class GrainEnvironment - { - private Schema schema = new Schema("my-schema"); - private long version = EtagVersion.Empty; - - public IGrainFactory GrainFactory { get; } = A.Fake(); - - public NamedId AppId { get; } = NamedId.Of(DomainId.NewGuid(), "my-app"); - - public NamedId SchemaId { get; } = NamedId.Of(DomainId.NewGuid(), "my-schema"); - - public GrainEnvironment() - { - var indexGrain = A.Fake(); - - A.CallTo(() => indexGrain.GetIdAsync(AppId.Name)) - .Returns(AppId.Id); - - var schemaGrain = A.Fake(); - - A.CallTo(() => schemaGrain.GetStateAsync()) - .ReturnsLazily(() => CreateEntity().AsJ()); - - A.CallTo(() => GrainFactory.GetGrain(DomainId.Combine(AppId.Id, SchemaId.Id).ToString(), null)) - .Returns(schemaGrain); - - A.CallTo(() => GrainFactory.GetGrain(SingleGrain.Id, null)) - .Returns(indexGrain); - } - - public void HandleCommand(AddField command) - { - version++; - - schema = schema.AddString(schema.Fields.Count + 1, command.Name, Partitioning.Invariant); - } - - public void VerifyGrainAccess(int count) - { - A.CallTo(() => GrainFactory.GetGrain(DomainId.Combine(AppId.Id, SchemaId.Id).ToString(), null)) - .MustHaveHappenedANumberOfTimesMatching(x => x == count); - } - - private ISchemaEntity CreateEntity() - { - var schemaEntity = A.Fake(); - - A.CallTo(() => schemaEntity.Id) - .Returns(SchemaId.Id); - - A.CallTo(() => schemaEntity.AppId) - .Returns(AppId); - - A.CallTo(() => schemaEntity.Version) - .Returns(version); - - A.CallTo(() => schemaEntity.SchemaDef) - .Returns(schema); - - return schemaEntity; - } - } - - private sealed class Configurator : ISiloConfigurator - { - public void Configure(ISiloBuilder siloBuilder) - { - siloBuilder.AddOrleansPubSub(); - } - } - - [Theory] - [InlineData(3, 100, 400, false)] - [InlineData(3, 100, 202, true)] - public async Task Should_distribute_and_cache_domain_objects(short numSilos, int numRuns, int expectedCounts, bool shouldBreak) - { - var env = new GrainEnvironment(); - - var cluster = - new TestClusterBuilder(numSilos) - .AddSiloBuilderConfigurator() - .Build(); - - await cluster.DeployAsync(); - - try - { - var indexes = - cluster.Silos.OfType() - .Select(x => - { - var pubSub = - shouldBreak ? - A.Fake() : - x.SiloHost.Services.GetRequiredService(); - - var cache = - new ReplicatedCache( - new MemoryCache(Options.Create(new MemoryCacheOptions())), - pubSub, - Options.Create(new ReplicatedCacheOptions { Enable = true })); - - return new SchemasIndex(env.GrainFactory, cache); - }).ToArray(); - - var appId = env.AppId; - - var random = new Random(); - - for (var i = 0; i < numRuns; i++) - { - var fieldName = Guid.NewGuid().ToString(); - var fieldCommand = new AddField { Name = fieldName, SchemaId = env.SchemaId, AppId = env.AppId }; - - var commandContext = new CommandContext(fieldCommand, A.Fake()); - - var randomIndex = indexes[random.Next(numSilos)]; - - await randomIndex.HandleAsync(commandContext, x => - { - if (x.Command is AddField command) - { - env.HandleCommand(command); - } - - x.Complete(true); - - return Task.CompletedTask; - }); - - foreach (var index in indexes) - { - var schemaById = await index.GetSchemaAsync(appId.Id, env.SchemaId.Id, true); - var schemaByName = await index.GetSchemaByNameAsync(appId.Id, env.SchemaId.Name, true); - - if (index == randomIndex || !shouldBreak || i == 0) - { - Assert.True(schemaById?.SchemaDef.FieldsByName.ContainsKey(fieldName)); - Assert.True(schemaByName?.SchemaDef.FieldsByName.ContainsKey(fieldName)); - } - else - { - Assert.False(schemaById?.SchemaDef.FieldsByName.ContainsKey(fieldName)); - Assert.False(schemaByName?.SchemaDef.FieldsByName.ContainsKey(fieldName)); - } - } - } - - env.VerifyGrainAccess(expectedCounts); - } - finally - { - await Task.WhenAny(Task.Delay(2000), cluster.StopAllSilosAsync()); - } - } - } -} 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 61dacd5d3..2703884d3 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 @@ -28,15 +28,15 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes { private readonly IGrainFactory grainFactory = A.Fake(); private readonly ICommandBus commandBus = A.Fake(); - private readonly ISchemasByAppIndexGrain index = 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(index); + A.CallTo(() => grainFactory.GetGrain(appId.Id.ToString(), null)) + .Returns(this.cache); var cache = new ReplicatedCache(new MemoryCache(Options.Create(new MemoryCacheOptions())), new SimplePubSub(A.Fake>()), @@ -50,11 +50,11 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes { var (expected, _) = SetupSchema(); - A.CallTo(() => index.GetIdAsync(schemaId.Name)) + A.CallTo(() => cache.GetSchemaIdAsync(schemaId.Name)) .Returns(schemaId.Id); - var actual1 = await sut.GetSchemaByNameAsync(appId.Id, schemaId.Name, false); - var actual2 = await sut.GetSchemaByNameAsync(appId.Id, schemaId.Name, false); + var actual1 = await sut.GetSchemaAsync(appId.Id, schemaId.Name, false); + var actual2 = await sut.GetSchemaAsync(appId.Id, schemaId.Name, false); Assert.Same(expected, actual1); Assert.Same(expected, actual2); @@ -62,7 +62,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes A.CallTo(() => grainFactory.GetGrain(A._, null)) .MustHaveHappenedTwiceExactly(); - A.CallTo(() => index.GetIdAsync(A._)) + A.CallTo(() => cache.GetSchemaIdAsync(A._)) .MustHaveHappenedTwiceExactly(); } @@ -71,11 +71,11 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes { var (expected, _) = SetupSchema(); - A.CallTo(() => index.GetIdAsync(schemaId.Name)) + A.CallTo(() => cache.GetSchemaIdAsync(schemaId.Name)) .Returns(schemaId.Id); - var actual1 = await sut.GetSchemaByNameAsync(appId.Id, schemaId.Name, true); - var actual2 = await sut.GetSchemaByNameAsync(appId.Id, schemaId.Name, true); + 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); Assert.Same(expected, actual1); @@ -85,7 +85,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes A.CallTo(() => grainFactory.GetGrain(A._, null)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => index.GetIdAsync(A._)) + A.CallTo(() => cache.GetSchemaIdAsync(A._)) .MustHaveHappenedOnceExactly(); } @@ -103,7 +103,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes A.CallTo(() => grainFactory.GetGrain(A._, null)) .MustHaveHappenedTwiceExactly(); - A.CallTo(() => index.GetIdAsync(A._)) + A.CallTo(() => cache.GetSchemaIdAsync(A._)) .MustNotHaveHappened(); } @@ -114,7 +114,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes var actual1 = await sut.GetSchemaAsync(appId.Id, schemaId.Id, true); var actual2 = await sut.GetSchemaAsync(appId.Id, schemaId.Id, true); - var actual3 = await sut.GetSchemaByNameAsync(appId.Id, schemaId.Name, true); + var actual3 = await sut.GetSchemaAsync(appId.Id, schemaId.Name, true); Assert.Same(expected, actual1); Assert.Same(expected, actual2); @@ -123,7 +123,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes A.CallTo(() => grainFactory.GetGrain(A._, null)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => index.GetIdAsync(A._)) + A.CallTo(() => cache.GetSchemaIdAsync(A._)) .MustNotHaveHappened(); } @@ -132,7 +132,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes { var (schema, _) = SetupSchema(); - A.CallTo(() => index.GetIdsAsync()) + A.CallTo(() => cache.GetSchemaIdsAsync()) .Returns(new List { schema.Id }); var actual = await sut.GetSchemasAsync(appId.Id); @@ -145,7 +145,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes { var (schema, _) = SetupSchema(EtagVersion.Empty); - A.CallTo(() => index.GetIdsAsync()) + A.CallTo(() => cache.GetSchemaIdsAsync()) .Returns(new List { schema.Id }); var actual = await sut.GetSchemasAsync(appId.Id); @@ -158,7 +158,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes { var (schema, _) = SetupSchema(0, true); - A.CallTo(() => index.GetIdsAsync()) + A.CallTo(() => cache.GetSchemaIdsAsync()) .Returns(new List { schema.Id }); var actual = await sut.GetSchemasAsync(appId.Id); @@ -171,7 +171,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes { var token = RandomHash.Simple(); - A.CallTo(() => index.ReserveAsync(schemaId.Id, schemaId.Name)) + A.CallTo(() => cache.ReserveAsync(schemaId.Id, schemaId.Name)) .Returns(token); var command = Create(schemaId.Name); @@ -182,11 +182,11 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes await sut.HandleAsync(context); - A.CallTo(() => index.AddAsync(token)) + A.CallTo(() => cache.AddAsync(schemaId.Id, schemaId.Name)) .MustHaveHappened(); - A.CallTo(() => index.RemoveReservationAsync(A._)) - .MustNotHaveHappened(); + A.CallTo(() => cache.RemoveReservationAsync(A._)) + .MustHaveHappened(); } [Fact] @@ -194,7 +194,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes { var token = RandomHash.Simple(); - A.CallTo(() => index.ReserveAsync(schemaId.Id, schemaId.Name)) + A.CallTo(() => cache.ReserveAsync(schemaId.Id, schemaId.Name)) .Returns(token); var command = Create(schemaId.Name); @@ -204,17 +204,17 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes await sut.HandleAsync(context); - A.CallTo(() => index.AddAsync(token)) + A.CallTo(() => cache.AddAsync(A._, A._)) .MustNotHaveHappened(); - A.CallTo(() => index.RemoveReservationAsync(token)) + A.CallTo(() => cache.RemoveReservationAsync(token)) .MustHaveHappened(); } [Fact] - public async Task Should_not_add_to_indexes_if_name_is_taken() + public async Task Should_not_add_to_indexes_if_name_is_reserved() { - A.CallTo(() => index.ReserveAsync(schemaId.Id, schemaId.Name)) + A.CallTo(() => cache.ReserveAsync(schemaId.Id, schemaId.Name)) .Returns(Task.FromResult(null)); var command = Create(schemaId.Name); @@ -225,45 +225,36 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes await Assert.ThrowsAsync(() => sut.HandleAsync(context)); - A.CallTo(() => index.AddAsync(A._)) + A.CallTo(() => cache.AddAsync(A._, A._)) .MustNotHaveHappened(); - A.CallTo(() => index.RemoveReservationAsync(A._)) + A.CallTo(() => cache.RemoveReservationAsync(A._)) .MustNotHaveHappened(); } [Fact] - public async Task Should_not_add_to_indexes_if_name_is_invalid() + public async Task Should_not_add_to_indexes_if_name_is_taken() { - var command = Create("INVALID"); - - var context = - new CommandContext(command, commandBus) - .Complete(); - - await sut.HandleAsync(context); - - A.CallTo(() => index.ReserveAsync(schemaId.Id, A._)) - .MustNotHaveHappened(); + var token = RandomHash.Simple(); - A.CallTo(() => index.RemoveReservationAsync(A._)) - .MustNotHaveHappened(); - } + A.CallTo(() => cache.ReserveAsync(schemaId.Id, schemaId.Name)) + .Returns(Task.FromResult(token)); - [Fact] - public async Task Should_update_index_if_schema_is_updated() - { - var (_, schemaGrain) = SetupSchema(); + A.CallTo(() => cache.GetSchemaIdAsync(schemaId.Name)) + .Returns(schemaId.Id); - var command = new UpdateSchema { SchemaId = schemaId, AppId = appId }; + var command = Create(schemaId.Name); var context = new CommandContext(command, commandBus) .Complete(); - await sut.HandleAsync(context); + await Assert.ThrowsAsync(() => sut.HandleAsync(context)); - A.CallTo(() => schemaGrain.GetStateAsync()) + A.CallTo(() => cache.AddAsync(A._, A._)) + .MustNotHaveHappened(); + + A.CallTo(() => cache.RemoveReservationAsync(token)) .MustHaveHappened(); } @@ -297,18 +288,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes await sut.HandleAsync(context); - A.CallTo(() => index.RemoveAsync(schema.Id)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_forward_call_if_rebuilding() - { - var schemas = new Dictionary(); - - await sut.RebuildAsync(appId.Id, schemas); - - A.CallTo(() => index.RebuildAsync(schemas)) + A.CallTo(() => cache.RemoveAsync(schema.Id)) .MustHaveHappened(); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/MongoDb/SchemasHashFixture.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/MongoDb/SchemasHashFixture.cs index 21288c99d..258d15a42 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/MongoDb/SchemasHashFixture.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/MongoDb/SchemasHashFixture.cs @@ -29,7 +29,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.MongoDb Task.Run(async () => { - await schemasHash.InitializeAsync(); + await schemasHash.InitializeAsync(default); }).Wait(); SchemasHash = schemasHash; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemasSearchSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemasSearchSourceTests.cs index 222834984..42af46b49 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemasSearchSourceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemasSearchSourceTests.cs @@ -41,7 +41,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var schema1 = CreateSchema("schemaA1"); - A.CallTo(() => appProvider.GetSchemasAsync(appId.Id)) + A.CallTo(() => appProvider.GetSchemasAsync(appId.Id, default)) .Returns(new List { schema1 }); A.CallTo(() => urlGenerator.SchemaUI(appId, schema1.NamedId())) @@ -63,7 +63,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var schema1 = CreateSchema("schemaA1", SchemaType.Component); - A.CallTo(() => appProvider.GetSchemasAsync(appId.Id)) + A.CallTo(() => appProvider.GetSchemasAsync(appId.Id, default)) .Returns(new List { schema1 }); A.CallTo(() => urlGenerator.SchemaUI(appId, schema1.NamedId())) @@ -87,7 +87,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var schema2 = CreateSchema("schemaA2"); var schema3 = CreateSchema("schemaB2"); - A.CallTo(() => appProvider.GetSchemasAsync(appId.Id)) + A.CallTo(() => appProvider.GetSchemasAsync(appId.Id, default)) .Returns(new List { schema1, schema2, schema3 }); A.CallTo(() => urlGenerator.SchemaUI(appId, schema1.NamedId())) @@ -117,7 +117,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas var schema1 = CreateSchema("schemaA1", SchemaType.Singleton); - A.CallTo(() => appProvider.GetSchemasAsync(appId.Id)) + A.CallTo(() => appProvider.GetSchemasAsync(appId.Id, default)) .Returns(new List { schema1 }); A.CallTo(() => urlGenerator.SchemaUI(appId, schema1.NamedId())) diff --git a/backend/tests/Squidex.Domain.Users.Tests/DefaultKeyStoreTests.cs b/backend/tests/Squidex.Domain.Users.Tests/DefaultKeyStoreTests.cs index fea09d824..1f78407f4 100644 --- a/backend/tests/Squidex.Domain.Users.Tests/DefaultKeyStoreTests.cs +++ b/backend/tests/Squidex.Domain.Users.Tests/DefaultKeyStoreTests.cs @@ -29,7 +29,7 @@ namespace Squidex.Domain.Users [Fact] public void Should_configure_new_keys() { - A.CallTo(() => store.ReadAsync(A._)) + A.CallTo(() => store.ReadAsync(A._, default)) .Returns((null!, true, 0)); var options = new OpenIddictServerOptions(); @@ -39,14 +39,14 @@ namespace Squidex.Domain.Users Assert.NotEmpty(options.SigningCredentials); Assert.NotEmpty(options.EncryptionCredentials); - A.CallTo(() => store.WriteAsync(A._, A._, 0, 0)) + A.CallTo(() => store.WriteAsync(A._, A._, 0, 0, default)) .MustHaveHappenedOnceExactly(); } [Fact] public void Should_configure_existing_keys() { - A.CallTo(() => store.ReadAsync(A._)) + A.CallTo(() => store.ReadAsync(A._, default)) .Returns((ExistingKey(), true, 0)); var options = new OpenIddictServerOptions(); @@ -56,19 +56,19 @@ namespace Squidex.Domain.Users Assert.NotEmpty(options.SigningCredentials); Assert.NotEmpty(options.EncryptionCredentials); - A.CallTo(() => store.WriteAsync(A._, A._, 0, 0)) + A.CallTo(() => store.WriteAsync(A._, A._, 0, 0, default)) .MustNotHaveHappened(); } [Fact] public void Should_configure_existing_keys_when_initial_setup_failed() { - A.CallTo(() => store.ReadAsync(A._)) + A.CallTo(() => store.ReadAsync(A._, default)) .Returns((null!, true, 0)).Once() .Then .Returns((ExistingKey(), true, 0)); - A.CallTo(() => store.WriteAsync(A._, A._, 0, 0)) + A.CallTo(() => store.WriteAsync(A._, A._, 0, 0, default)) .Throws(new InconsistentStateException(0, 0)); var options = new OpenIddictServerOptions(); @@ -78,7 +78,7 @@ namespace Squidex.Domain.Users Assert.NotEmpty(options.SigningCredentials); Assert.NotEmpty(options.EncryptionCredentials); - A.CallTo(() => store.WriteAsync(A._, A._, 0, 0)) + A.CallTo(() => store.WriteAsync(A._, A._, 0, 0, default)) .MustHaveHappened(); } diff --git a/backend/tests/Squidex.Domain.Users.Tests/DefaultUserResolverTests.cs b/backend/tests/Squidex.Domain.Users.Tests/DefaultUserResolverTests.cs index 64e25e1fb..68ae3981a 100644 --- a/backend/tests/Squidex.Domain.Users.Tests/DefaultUserResolverTests.cs +++ b/backend/tests/Squidex.Domain.Users.Tests/DefaultUserResolverTests.cs @@ -7,6 +7,7 @@ using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; using FakeItEasy; using Microsoft.Extensions.DependencyInjection; @@ -18,21 +19,24 @@ namespace Squidex.Domain.Users { public class DefaultUserResolverTests { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; private readonly IUserService userService = A.Fake(); private readonly DefaultUserResolver sut; public DefaultUserResolverTests() { - var serviceProvider = A.Fake(); + ct = cts.Token; - var scope = A.Fake(); + var serviceProvider = A.Fake(); + var scopeObject = A.Fake(); var scopeFactory = A.Fake(); A.CallTo(() => scopeFactory.CreateScope()) - .Returns(scope); + .Returns(scopeObject); - A.CallTo(() => scope.ServiceProvider) + A.CallTo(() => scopeObject.ServiceProvider) .Returns(serviceProvider); A.CallTo(() => serviceProvider.GetService(typeof(IServiceScopeFactory))) @@ -51,10 +55,10 @@ namespace Squidex.Domain.Users var user = A.Fake(); - A.CallTo(() => userService.CreateAsync(email, A.That.Matches(x => x.Invited == true), false)) + A.CallTo(() => userService.CreateAsync(email, A.That.Matches(x => x.Invited == true), false, ct)) .Returns(user); - var result = await sut.CreateUserIfNotExistsAsync(email, true); + var result = await sut.CreateUserIfNotExistsAsync(email, true, ct); Assert.Equal((user, true), result); } @@ -66,13 +70,13 @@ namespace Squidex.Domain.Users var user = A.Fake(); - A.CallTo(() => userService.CreateAsync(email, A._, false)) + A.CallTo(() => userService.CreateAsync(email, A._, false, ct)) .Throws(new InvalidOperationException()); - A.CallTo(() => userService.FindByEmailAsync(email)) + A.CallTo(() => userService.FindByEmailAsync(email, ct)) .Returns(user); - var result = await sut.CreateUserIfNotExistsAsync(email, true); + var result = await sut.CreateUserIfNotExistsAsync(email, true, ct); Assert.Equal((user, false), result); } @@ -82,10 +86,10 @@ namespace Squidex.Domain.Users { var id = "123"; - await sut.SetClaimAsync(id, "my-claim", "my-value", false); + await sut.SetClaimAsync(id, "my-claim", "my-value", false, ct); A.CallTo(() => userService.UpdateAsync(id, - A.That.Matches(x => x.CustomClaims!.Any(y => y.Type == "my-claim" && y.Value == "my-value")), false)) + A.That.Matches(x => x.CustomClaims!.Any(y => y.Type == "my-claim" && y.Value == "my-value")), false, ct)) .MustHaveHappened(); } @@ -94,10 +98,10 @@ namespace Squidex.Domain.Users { var id = "123"; - await sut.SetClaimAsync(id, "my-claim", "my-value", true); + await sut.SetClaimAsync(id, "my-claim", "my-value", true, ct); A.CallTo(() => userService.UpdateAsync(id, - A.That.Matches(x => x.CustomClaims!.Any(y => y.Type == "my-claim" && y.Value == "my-value")), true)) + A.That.Matches(x => x.CustomClaims!.Any(y => y.Type == "my-claim" && y.Value == "my-value")), true, ct)) .MustHaveHappened(); } @@ -108,10 +112,10 @@ namespace Squidex.Domain.Users var user = A.Fake(); - A.CallTo(() => userService.FindByEmailAsync(id)) + A.CallTo(() => userService.FindByEmailAsync(id, ct)) .Returns(user); - var result = await sut.FindByIdOrEmailAsync(id); + var result = await sut.FindByIdOrEmailAsync(id, ct); Assert.Equal(user, result); } @@ -123,10 +127,10 @@ namespace Squidex.Domain.Users var user = A.Fake(); - A.CallTo(() => userService.FindByIdAsync(id)) + A.CallTo(() => userService.FindByIdAsync(id, ct)) .Returns(user); - var result = await sut.FindByIdOrEmailAsync(id); + var result = await sut.FindByIdOrEmailAsync(id, ct); Assert.Equal(user, result); } @@ -138,10 +142,10 @@ namespace Squidex.Domain.Users var user = A.Fake(); - A.CallTo(() => userService.FindByIdAsync(id)) + A.CallTo(() => userService.FindByIdAsync(id, ct)) .Returns(user); - var result = await sut.FindByIdOrEmailAsync(id); + var result = await sut.FindByIdOrEmailAsync(id, ct); Assert.Equal(user, result); } @@ -153,10 +157,10 @@ namespace Squidex.Domain.Users var users = ResultList.CreateFrom(0, A.Fake()); - A.CallTo(() => userService.QueryAsync(email, 10, 0)) + A.CallTo(() => userService.QueryAsync(email, 10, 0, ct)) .Returns(users); - var result = await sut.QueryByEmailAsync(email); + var result = await sut.QueryByEmailAsync(email, ct); Assert.Single(result); } @@ -168,10 +172,10 @@ namespace Squidex.Domain.Users var users = ResultList.CreateFrom(0, A.Fake()); - A.CallTo(() => userService.QueryAsync(ids)) + A.CallTo(() => userService.QueryAsync(ids, ct)) .Returns(users); - var result = await sut.QueryManyAsync(ids); + var result = await sut.QueryManyAsync(ids, ct); Assert.Single(result); } @@ -181,10 +185,10 @@ namespace Squidex.Domain.Users { var users = ResultList.CreateFrom(0, A.Fake()); - A.CallTo(() => userService.QueryAsync(null, int.MaxValue, 0)) + A.CallTo(() => userService.QueryAsync(null, int.MaxValue, 0, ct)) .Returns(users); - var result = await sut.QueryAllAsync(); + var result = await sut.QueryAllAsync(ct); Assert.Single(result); } diff --git a/backend/tests/Squidex.Domain.Users.Tests/DefaultUserServiceTests.cs b/backend/tests/Squidex.Domain.Users.Tests/DefaultUserServiceTests.cs index 01cb2212f..edd34f5e6 100644 --- a/backend/tests/Squidex.Domain.Users.Tests/DefaultUserServiceTests.cs +++ b/backend/tests/Squidex.Domain.Users.Tests/DefaultUserServiceTests.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; @@ -583,7 +584,7 @@ namespace Squidex.Domain.Users for (var i = 0; i < numCurrentUsers; i++) { - users.Add(CreatePendingUser(i.ToString())); + users.Add(CreatePendingUser(i.ToString(CultureInfo.InvariantCulture))); } A.CallTo(() => userManager.Users) diff --git a/backend/tests/Squidex.Domain.Users.Tests/DefaultXmlRepositoryTests.cs b/backend/tests/Squidex.Domain.Users.Tests/DefaultXmlRepositoryTests.cs index 816c9e737..d216a9c96 100644 --- a/backend/tests/Squidex.Domain.Users.Tests/DefaultXmlRepositoryTests.cs +++ b/backend/tests/Squidex.Domain.Users.Tests/DefaultXmlRepositoryTests.cs @@ -5,9 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; -using System.Threading; -using System.Threading.Tasks; +using System.Linq; using System.Xml.Linq; using FakeItEasy; using Squidex.Infrastructure; @@ -29,19 +27,18 @@ namespace Squidex.Domain.Users [Fact] public void Should_read_from_store() { - A.CallTo(() => store.ReadAllAsync(A>._, A._)) - .Invokes((Func callback, CancellationToken _) => + A.CallTo(() => store.ReadAllAsync(default)) + .Returns(new[] { - callback(new DefaultXmlRepository.State + (new DefaultXmlRepository.State { Xml = new XElement("xml").ToString() - }, 0); - - callback(new DefaultXmlRepository.State + }, 0L), + (new DefaultXmlRepository.State { Xml = new XElement("xml").ToString() - }, 0); - }); + }, 0L) + }.ToAsyncEnumerable()); var xml = sut.GetAllElements(); @@ -55,7 +52,7 @@ namespace Squidex.Domain.Users sut.StoreElement(xml, "name"); - A.CallTo(() => store.WriteAsync(DomainId.Create("name"), A._, A._, 0)) + A.CallTo(() => store.WriteAsync(DomainId.Create("name"), A._, A._, 0, default)) .MustHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj b/backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj index 5982d4cf6..408b848d4 100644 --- a/backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj +++ b/backend/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj @@ -15,6 +15,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectTests.cs index bc6d982db..6ffe8be2c 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectTests.cs @@ -508,7 +508,10 @@ namespace Squidex.Infrastructure.Commands var @events = new List>(); A.CallTo(() => persistence.WriteEventsAsync(A>>._)) - .Invokes(c => @events.AddRange(c.GetArgument>>(0)!)); + .Invokes(args => + { + @events.AddRange(args.GetArgument>>(0)!); + }); var eventsPersistence = A.Fake>(); diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EventStoreTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EventStoreTests.cs index 5fc46a495..7824ae4d4 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EventStoreTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EventStoreTests.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -50,7 +51,9 @@ namespace Squidex.Infrastructure.EventSourcing protected EventStoreTests() { +#pragma warning disable MA0056 // Do not call overridable members in constructor sut = new Lazy(CreateStore); +#pragma warning restore MA0056 // Do not call overridable members in constructor } public abstract T CreateStore(); @@ -359,6 +362,26 @@ namespace Squidex.Infrastructure.EventSourcing } } + [Fact] + public async Task Should_delete_by_filter() + { + var streamName = $"test-{Guid.NewGuid()}"; + + var events = new[] + { + CreateEventData(1), + CreateEventData(2) + }; + + await Sut.AppendAsync(Guid.NewGuid(), streamName, events); + + await Sut.DeleteAsync($"^{streamName.Substring(0, 10)}"); + + var readEvents = await QueryAsync(streamName); + + Assert.Empty(readEvents); + } + [Fact] public async Task Should_delete_stream() { @@ -386,7 +409,7 @@ namespace Squidex.Infrastructure.EventSourcing private static EventData CreateEventData(int i) { - return new EventData($"Type{i}", new EnvelopeHeaders(), i.ToString()); + return new EventData($"Type{i}", new EnvelopeHeaders(), i.ToString(CultureInfo.InvariantCulture)); } private async Task?> QueryAllAsync(string? streamFilter = null, string? position = null) diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreFixture.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreFixture.cs index 646feb734..88b972279 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreFixture.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoEventStoreFixture.cs @@ -30,7 +30,7 @@ namespace Squidex.Infrastructure.EventSourcing BsonJsonConvention.Register(JsonSerializer.Create(TestUtils.DefaultSettings())); EventStore = new MongoEventStore(mongoDatabase, notifier); - EventStore.InitializeAsync().Wait(); + EventStore.InitializeAsync(default).Wait(); } public void Cleanup() diff --git a/backend/tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ReadOnlyCollectionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ReadOnlyCollectionTests.cs index 278f2bdfe..f237a14b6 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ReadOnlyCollectionTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Json/Newtonsoft/ReadOnlyCollectionTests.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; using Newtonsoft.Json; using Xunit; @@ -39,7 +40,7 @@ namespace Squidex.Infrastructure.Json.Newtonsoft var serialized = JsonConvert.DeserializeObject>>(json)!; - Assert.DoesNotContain("$type", json); + Assert.DoesNotContain("$type", json, StringComparison.Ordinal); Assert.Equal(2, serialized.Values.Count); } @@ -64,7 +65,7 @@ namespace Squidex.Infrastructure.Json.Newtonsoft var serialized = JsonConvert.DeserializeObject>>(json)!; - Assert.DoesNotContain("$type", json); + Assert.DoesNotContain("$type", json, StringComparison.Ordinal); Assert.Equal(2, serialized.Values.Count); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/LanguagesInitializerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/LanguagesInitializerTests.cs index e83ee7c60..6fc2105f2 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/LanguagesInitializerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/LanguagesInitializerTests.cs @@ -23,7 +23,7 @@ namespace Squidex.Infrastructure var sut = new LanguagesInitializer(options); - await sut.InitializeAsync(); + await sut.InitializeAsync(default); Assert.Equal("English (Norwegian)", Language.GetLanguage("en-NO").EnglishName); } @@ -38,7 +38,7 @@ namespace Squidex.Infrastructure var sut = new LanguagesInitializer(options); - await sut.InitializeAsync(); + await sut.InitializeAsync(default); Assert.False(Language.TryGetLanguage("en-Error", out _)); } @@ -53,7 +53,7 @@ namespace Squidex.Infrastructure var sut = new LanguagesInitializer(options); - await sut.InitializeAsync(); + await sut.InitializeAsync(default); Assert.Equal("German", Language.GetLanguage("de").EnglishName); } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Log/BackgroundRequestLogStoreTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Log/BackgroundRequestLogStoreTests.cs index 8fe3c8338..13dacb51c 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Log/BackgroundRequestLogStoreTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Log/BackgroundRequestLogStoreTests.cs @@ -5,8 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; +using System.Threading; using System.Threading.Tasks; using FakeItEasy; using Microsoft.Extensions.Options; @@ -17,15 +20,22 @@ namespace Squidex.Infrastructure.Log { public class BackgroundRequestLogStoreTests { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; private readonly IRequestLogRepository requestLogRepository = A.Fake(); private readonly RequestLogStoreOptions options = new RequestLogStoreOptions(); private readonly BackgroundRequestLogStore sut; public BackgroundRequestLogStoreTests() { + ct = cts.Token; + options.StoreEnabled = true; - sut = new BackgroundRequestLogStore(Options.Create(options), requestLogRepository, A.Fake()); + sut = new BackgroundRequestLogStore(Options.Create(options), requestLogRepository, A.Fake()) + { + ForceWrite = true + }; } [Theory] @@ -39,40 +49,89 @@ namespace Squidex.Infrastructure.Log } [Fact] - public async Task Should_not_if_disabled() + public async Task Should_forward_delete_call() + { + await sut.DeleteAsync("my-key", ct); + + A.CallTo(() => requestLogRepository.DeleteAsync("my-key", ct)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_not_log_if_disabled() { options.StoreEnabled = false; for (var i = 0; i < 2500; i++) { - await sut.LogAsync(new Request { Key = i.ToString() }); + await sut.LogAsync(new Request { Key = i.ToString(CultureInfo.InvariantCulture) }, ct); } sut.Next(); sut.Dispose(); - A.CallTo(() => requestLogRepository.InsertManyAsync(A>._)) + // Wait for the timer to not trigger. + await Task.Delay(500, ct); + + A.CallTo(() => requestLogRepository.InsertManyAsync(A>._, A._)) .MustNotHaveHappened(); } [Fact] - public async Task Should_log_in_batches() + public async Task Should_provide_results_from_repository() + { + var key = "my-key"; + + var dateFrom = DateTime.Today; + var dateTo = dateFrom.AddDays(4); + + A.CallTo(() => requestLogRepository.QueryAllAsync(key, dateFrom, dateTo, ct)) + .Returns(AsyncEnumerable.Repeat(new Request { Key = key }, 1)); + + var results = await sut.QueryAllAsync(key, dateFrom, dateTo, ct).ToListAsync(ct); + + Assert.NotEmpty(results); + } + + [Fact] + public async Task Should_not_provide_results_from_repository_if_disabled() + { + options.StoreEnabled = false; + + var key = "my-key"; + + var dateFrom = DateTime.Today; + var dateTo = dateFrom.AddDays(4); + + var results = await sut.QueryAllAsync(key, dateFrom, dateTo, ct).ToListAsync(ct); + + Assert.Empty(results); + + A.CallTo(() => requestLogRepository.QueryAllAsync(key, dateFrom, dateTo, ct)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_write_logs_in_batches() { for (var i = 0; i < 2500; i++) { - await sut.LogAsync(new Request { Key = i.ToString() }); + await sut.LogAsync(new Request { Key = i.ToString(CultureInfo.InvariantCulture) }, ct); } sut.Next(); sut.Dispose(); - A.CallTo(() => requestLogRepository.InsertManyAsync(Batch("0", "999"))) + // Wait for the timer to trigger. + await Task.Delay(500, ct); + + A.CallTo(() => requestLogRepository.InsertManyAsync(Batch("0", "999"), A._)) .MustHaveHappened(); - A.CallTo(() => requestLogRepository.InsertManyAsync(Batch("1000", "1999"))) + A.CallTo(() => requestLogRepository.InsertManyAsync(Batch("1000", "1999"), A._)) .MustHaveHappened(); - A.CallTo(() => requestLogRepository.InsertManyAsync(Batch("2000", "2499"))) + A.CallTo(() => requestLogRepository.InsertManyAsync(Batch("2000", "2499"), A._)) .MustHaveHappened(); } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs index 1afca3044..ac29e873c 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs @@ -18,6 +18,8 @@ namespace Squidex.Infrastructure.Migrations { public class MigratorTests { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; private readonly IMigrationStatus status = A.Fake(); private readonly IMigrationPath path = A.Fake(); private readonly ISemanticLog log = A.Fake(); @@ -29,12 +31,14 @@ namespace Squidex.Infrastructure.Migrations private int version; private bool isLocked; - public Task GetVersionAsync() + public Task GetVersionAsync( + CancellationToken ct = default) { return Task.FromResult(version); } - public Task TryLockAsync() + public Task TryLockAsync( + CancellationToken ct = default) { var lockAcquired = false; @@ -51,7 +55,8 @@ namespace Squidex.Infrastructure.Migrations return Task.FromResult(lockAcquired); } - public Task CompleteAsync(int newVersion) + public Task CompleteAsync(int newVersion, + CancellationToken ct = default) { lock (lockObject) { @@ -61,7 +66,8 @@ namespace Squidex.Infrastructure.Migrations return Task.CompletedTask; } - public Task UnlockAsync() + public Task UnlockAsync( + CancellationToken ct = default) { lock (lockObject) { @@ -74,6 +80,8 @@ namespace Squidex.Infrastructure.Migrations public MigratorTests() { + ct = cts.Token; + A.CallTo(() => path.GetNext(A._)) .ReturnsLazily((int version) => { @@ -89,8 +97,11 @@ namespace Squidex.Infrastructure.Migrations return (newVersion, migrations.Select(x => x.Migration)); }); - A.CallTo(() => status.GetVersionAsync()).Returns(0); - A.CallTo(() => status.TryLockAsync()).Returns(true); + A.CallTo(() => status.GetVersionAsync(ct)) + .Returns(0); + + A.CallTo(() => status.TryLockAsync(ct)) + .Returns(true); } [Fact] @@ -102,27 +113,27 @@ namespace Squidex.Infrastructure.Migrations var sut = new Migrator(status, path, log); - await sut.MigrateAsync(); + await sut.MigrateAsync(ct); - A.CallTo(() => migrator_0_1.UpdateAsync(A._)) + A.CallTo(() => migrator_0_1.UpdateAsync(ct)) .MustHaveHappened(); - A.CallTo(() => migrator_1_2.UpdateAsync(A._)) + A.CallTo(() => migrator_1_2.UpdateAsync(ct)) .MustHaveHappened(); - A.CallTo(() => migrator_2_3.UpdateAsync(A._)) + A.CallTo(() => migrator_2_3.UpdateAsync(ct)) .MustHaveHappened(); - A.CallTo(() => status.CompleteAsync(1)) + A.CallTo(() => status.CompleteAsync(1, ct)) .MustNotHaveHappened(); - A.CallTo(() => status.CompleteAsync(2)) + A.CallTo(() => status.CompleteAsync(2, ct)) .MustNotHaveHappened(); - A.CallTo(() => status.CompleteAsync(3)) + A.CallTo(() => status.CompleteAsync(3, ct)) .MustHaveHappened(); - A.CallTo(() => status.UnlockAsync()) + A.CallTo(() => status.UnlockAsync(default)) .MustHaveHappened(); } @@ -135,27 +146,27 @@ namespace Squidex.Infrastructure.Migrations var sut = new Migrator(status, path, log); - await sut.MigrateAsync(); + await sut.MigrateAsync(ct); - A.CallTo(() => migrator_0_1.UpdateAsync(A._)) + A.CallTo(() => migrator_0_1.UpdateAsync(ct)) .MustHaveHappened(); - A.CallTo(() => migrator_1_2.UpdateAsync(A._)) + A.CallTo(() => migrator_1_2.UpdateAsync(ct)) .MustHaveHappened(); - A.CallTo(() => migrator_2_3.UpdateAsync(A._)) + A.CallTo(() => migrator_2_3.UpdateAsync(ct)) .MustHaveHappened(); - A.CallTo(() => status.CompleteAsync(1)) + A.CallTo(() => status.CompleteAsync(1, ct)) .MustHaveHappened(); - A.CallTo(() => status.CompleteAsync(2)) + A.CallTo(() => status.CompleteAsync(2, ct)) .MustHaveHappened(); - A.CallTo(() => status.CompleteAsync(3)) + A.CallTo(() => status.CompleteAsync(3, ct)) .MustHaveHappened(); - A.CallTo(() => status.UnlockAsync()) + A.CallTo(() => status.UnlockAsync(A._)) .MustHaveHappened(); } @@ -168,9 +179,10 @@ namespace Squidex.Infrastructure.Migrations var sut = new Migrator(status, path, log); - A.CallTo(() => migrator_1_2.UpdateAsync(A._)).Throws(new ArgumentException()); + A.CallTo(() => migrator_1_2.UpdateAsync(ct)) + .Throws(new InvalidOperationException()); - await Assert.ThrowsAsync(() => sut.MigrateAsync()); + await Assert.ThrowsAsync(() => sut.MigrateAsync(ct)); A.CallTo(() => migrator_0_1.UpdateAsync(A._)) .MustHaveHappened(); @@ -181,16 +193,16 @@ namespace Squidex.Infrastructure.Migrations A.CallTo(() => migrator_2_3.UpdateAsync(A._)) .MustNotHaveHappened(); - A.CallTo(() => status.CompleteAsync(1)) + A.CallTo(() => status.CompleteAsync(1, A._)) .MustNotHaveHappened(); - A.CallTo(() => status.CompleteAsync(2)) + A.CallTo(() => status.CompleteAsync(2, A._)) .MustNotHaveHappened(); - A.CallTo(() => status.CompleteAsync(3)) + A.CallTo(() => status.CompleteAsync(3, A._)) .MustNotHaveHappened(); - A.CallTo(() => status.UnlockAsync()) + A.CallTo(() => status.UnlockAsync(default)) .MustHaveHappened(); } @@ -202,17 +214,17 @@ namespace Squidex.Infrastructure.Migrations var ex = new InvalidOperationException(); - A.CallTo(() => migrator_0_1.UpdateAsync(A._)) + A.CallTo(() => migrator_0_1.UpdateAsync(ct)) .Throws(ex); var sut = new Migrator(status, path, log); - await Assert.ThrowsAsync(() => sut.MigrateAsync()); + await Assert.ThrowsAsync(() => sut.MigrateAsync(ct)); A.CallTo(() => log.Log(SemanticLogLevel.Fatal, ex, A._!)) .MustHaveHappened(); - A.CallTo(() => migrator_1_2.UpdateAsync(A._)) + A.CallTo(() => migrator_1_2.UpdateAsync(ct)) .MustNotHaveHappened(); } @@ -224,12 +236,12 @@ namespace Squidex.Infrastructure.Migrations var sut = new Migrator(new InMemoryStatus(), path, log) { LockWaitMs = 2 }; - await Task.WhenAll(Enumerable.Repeat(0, 10).Select(x => Task.Run(() => sut.MigrateAsync()))); + await Task.WhenAll(Enumerable.Repeat(0, 10).Select(x => Task.Run(() => sut.MigrateAsync(ct), ct))); - A.CallTo(() => migrator_0_1.UpdateAsync(A._)) + A.CallTo(() => migrator_0_1.UpdateAsync(ct)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => migrator_1_2.UpdateAsync(A._)) + A.CallTo(() => migrator_1_2.UpdateAsync(ct)) .MustHaveHappenedOnceExactly(); } diff --git a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/MongoQueryTests.cs b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/MongoQueryTests.cs index 8f5e5b7ae..15d14c962 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/MongoDb/MongoQueryTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/MongoDb/MongoQueryTests.cs @@ -354,7 +354,7 @@ namespace Squidex.Infrastructure.MongoDb private static string Cleanup(string filter, object? arg = null) { - return filter.Replace('\'', '"').Replace("[value]", arg?.ToString()); + return filter.Replace('\'', '"').Replace("[value]", arg?.ToString(), StringComparison.Ordinal); } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Orleans/ActivationLimiterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/ActivationLimiterTests.cs index 4c25409ba..4bae040ec 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Orleans/ActivationLimiterTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Orleans/ActivationLimiterTests.cs @@ -20,7 +20,7 @@ namespace Squidex.Infrastructure.Orleans private readonly IGrainRuntime grainRuntime = A.Fake(); private readonly ActivationLimiter sut; - private class MyGrain : GrainBase + private sealed class MyGrain : GrainBase { public MyGrain(IGrainIdentity identity, IGrainRuntime runtime, IActivationLimit limit) : base(identity, runtime) diff --git a/backend/tests/Squidex.Infrastructure.Tests/Orleans/BootstrapTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/BootstrapTests.cs index e10996ba9..6f43b4472 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Orleans/BootstrapTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Orleans/BootstrapTests.cs @@ -32,7 +32,7 @@ namespace Squidex.Infrastructure.Orleans [Fact] public async Task Should_activate_grain_on_run() { - await sut.StartAsync(); + await sut.StartAsync(default); A.CallTo(() => grain.ActivateAsync()) .MustHaveHappened(); @@ -44,7 +44,7 @@ namespace Squidex.Infrastructure.Orleans A.CallTo(() => grain.ActivateAsync()) .Throws(new InvalidOperationException()); - await Assert.ThrowsAsync(() => sut.StartAsync()); + await Assert.ThrowsAsync(() => sut.StartAsync(default)); } [Fact] @@ -53,7 +53,7 @@ namespace Squidex.Infrastructure.Orleans A.CallTo(() => grain.ActivateAsync()) .Throws(new OrleansException()).Once(); - await sut.StartAsync(); + await sut.StartAsync(default); A.CallTo(() => grain.ActivateAsync()) .MustHaveHappened(2, Times.Exactly); @@ -65,7 +65,7 @@ namespace Squidex.Infrastructure.Orleans A.CallTo(() => grain.ActivateAsync()) .Throws(new OrleansException()); - await Assert.ThrowsAsync(() => sut.StartAsync()); + await Assert.ThrowsAsync(() => sut.StartAsync(default)); A.CallTo(() => grain.ActivateAsync()) .MustHaveHappened(10, Times.Exactly); diff --git a/backend/tests/Squidex.Infrastructure.Tests/Orleans/ExceptionWrapperFilterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/ExceptionWrapperFilterTests.cs index 97119fa40..2ead1754f 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Orleans/ExceptionWrapperFilterTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Orleans/ExceptionWrapperFilterTests.cs @@ -95,7 +95,7 @@ namespace Squidex.Infrastructure.Orleans var ex = await Assert.ThrowsAnyAsync(() => sut.Invoke(context)); Assert.Equal(original.GetType(), ex.ExceptionType); - Assert.Contains(original.Message, ex.Message); + Assert.Contains(original.Message, ex.Message, StringComparison.OrdinalIgnoreCase); } [Fact] @@ -104,10 +104,10 @@ namespace Squidex.Infrastructure.Orleans var original = new InvalidException("My Message"); var source = new OrleansWrapperException(original, original.GetType()); - var result = source.SerializeAndDeserializeBinary(); + var serialized = source.SerializeAndDeserializeBinary(); - Assert.Equal(result.ExceptionType, source.ExceptionType); - Assert.Equal(result.Message, source.Message); + Assert.Equal(serialized.ExceptionType, source.ExceptionType); + Assert.Equal(serialized.Message, source.Message); } [Fact, Trait("Category", "Dependencies")] diff --git a/backend/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/IdsIndexGrainTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/IdsIndexGrainTests.cs deleted file mode 100644 index 05ea874d9..000000000 --- a/backend/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/IdsIndexGrainTests.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.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using Xunit; - -namespace Squidex.Infrastructure.Orleans.Indexes -{ - public class IdsIndexGrainTests - { - private readonly IGrainState> grainState = A.Fake>>(); - private readonly DomainId id1 = DomainId.NewGuid(); - private readonly DomainId id2 = DomainId.NewGuid(); - private readonly IdsIndexGrain, DomainId> sut; - - public IdsIndexGrainTests() - { - A.CallTo(() => grainState.ClearAsync()) - .Invokes(() => grainState.Value = new IdsIndexState()); - - sut = new IdsIndexGrain, DomainId>(grainState); - } - - [Fact] - public async Task Should_add_id_to_index() - { - await sut.AddAsync(id1); - await sut.AddAsync(id2); - - var result = await sut.GetIdsAsync(); - - Assert.Equal(new List { id1, id2 }, result); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappenedTwiceExactly(); - } - - [Fact] - public async Task Should_provide_number_of_entries() - { - await sut.AddAsync(id1); - await sut.AddAsync(id2); - - var count = await sut.CountAsync(); - - Assert.Equal(2, count); - } - - [Fact] - public async Task Should_clear_all_entries() - { - await sut.AddAsync(id1); - await sut.AddAsync(id2); - - await sut.ClearAsync(); - - var count = await sut.CountAsync(); - - Assert.Equal(0, count); - } - - [Fact] - public async Task Should_remove_id_from_index() - { - await sut.AddAsync(id1); - await sut.AddAsync(id2); - await sut.RemoveAsync(id1); - - var result = await sut.GetIdsAsync(); - - Assert.Equal(new List { id2 }, result); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappenedTwiceOrMore(); - } - - [Fact] - public async Task Should_replace__ids_on_rebuild() - { - var state = new HashSet - { - id1, - id2 - }; - - await sut.RebuildAsync(state); - - var result = await sut.GetIdsAsync(); - - Assert.Equal(new List { id1, id2 }, result); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappened(); - } - } -} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/UniqueNameGrainTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/UniqueNameGrainTests.cs new file mode 100644 index 000000000..419c023e9 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/UniqueNameGrainTests.cs @@ -0,0 +1,63 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Xunit; + +namespace Squidex.Infrastructure.Orleans.Indexes +{ + public class UniqueNameGrainTests + { + private readonly UniqueNameGrain sut; + + public UniqueNameGrainTests() + { + sut = new UniqueNameGrain(); + } + + [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/Indexes/UniqueNameIndexGrainTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/UniqueNameIndexGrainTests.cs deleted file mode 100644 index 1e97f61cf..000000000 --- a/backend/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/UniqueNameIndexGrainTests.cs +++ /dev/null @@ -1,197 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FakeItEasy; -using Xunit; - -namespace Squidex.Infrastructure.Orleans.Indexes -{ - public class UniqueNameIndexGrainTests - { - private readonly IGrainState> grainState = A.Fake>>(); - private readonly NamedId id1 = NamedId.Of(Guid.NewGuid(), "my-name1"); - private readonly NamedId id2 = NamedId.Of(Guid.NewGuid(), "my-name2"); - private readonly UniqueNameIndexGrain, Guid> sut; - - public UniqueNameIndexGrainTests() - { - A.CallTo(() => grainState.ClearAsync()) - .Invokes(() => grainState.Value = new UniqueNameIndexState()); - - sut = new UniqueNameIndexGrain, Guid>(grainState); - } - - [Fact] - public async Task Should_not_write_to_state_for_reservation() - { - await sut.ReserveAsync(id1.Id, id1.Name); - - A.CallTo(() => grainState.WriteAsync()) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_add_to_index_if_reservation_token_acquired() - { - await AddAsync(id1); - - var result = await sut.GetIdAsync(id1.Name); - - Assert.Equal(id1.Id, result); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_not_make_reservation_if_name_already_reserved() - { - await sut.ReserveAsync(id1.Id, id1.Name); - - var newToken = await sut.ReserveAsync(id1.Id, id1.Name); - - Assert.Null(newToken); - } - - [Fact] - public async Task Should_not_make_reservation_if_name_taken() - { - await AddAsync(id1); - - var newToken = await sut.ReserveAsync(id1.Id, id1.Name); - - Assert.Null(newToken); - } - - [Fact] - public async Task Should_provide_number_of_entries() - { - await AddAsync(id1); - await AddAsync(id2); - - var count = await sut.CountAsync(); - - Assert.Equal(2, count); - } - - [Fact] - public async Task Should_clear_all_entries() - { - await AddAsync(id1); - await AddAsync(id2); - - await sut.ClearAsync(); - - var count = await sut.CountAsync(); - - Assert.Equal(0, count); - } - - [Fact] - public async Task Should_make_reservation_after_reservation_removed() - { - var token = await sut.ReserveAsync(id1.Id, id1.Name); - - await sut.RemoveReservationAsync(token!); - - var newToken = await sut.ReserveAsync(id1.Id, id1.Name); - - Assert.NotNull(newToken); - } - - [Fact] - public async Task Should_make_reservation_after_id_removed() - { - await AddAsync(id1); - - await sut.RemoveAsync(id1.Id); - - var newToken = await sut.ReserveAsync(id1.Id, id1.Name); - - Assert.NotNull(newToken); - } - - [Fact] - public async Task Should_remove_id_from_index() - { - await AddAsync(id1); - - await sut.RemoveAsync(id1.Id); - - var result = await sut.GetIdAsync(id1.Name); - - Assert.Equal(Guid.Empty, result); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappenedTwiceExactly(); - } - - [Fact] - public async Task Should_not_write_to_state_if_nothing_removed() - { - await sut.RemoveAsync(id1.Id); - - A.CallTo(() => grainState.WriteAsync()) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_ignore_error_if_removing_reservation_with_Invalid_token() - { - await sut.RemoveReservationAsync(null); - } - - [Fact] - public async Task Should_ignore_error_if_completing_reservation_with_Invalid_token() - { - await sut.AddAsync(null!); - } - - [Fact] - public async Task Should_replace_ids_on_rebuild() - { - var state = new Dictionary - { - [id1.Name] = id1.Id, - [id2.Name] = id2.Id - }; - - await sut.RebuildAsync(state); - - Assert.Equal(id1.Id, await sut.GetIdAsync(id1.Name)); - Assert.Equal(id2.Id, await sut.GetIdAsync(id2.Name)); - - var result = await sut.GetIdsAsync(); - - Assert.Equal(new List { id1.Id, id2.Id }, result); - - A.CallTo(() => grainState.WriteAsync()) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_provide_multiple_ids_by_names() - { - await AddAsync(id1); - await AddAsync(id2); - - var result = await sut.GetIdsAsync(new[] { id1.Name, id2.Name, "not-found" }); - - Assert.Equal(new List { id1.Id, id2.Id }, result); - } - - private async Task AddAsync(NamedId id) - { - var token = await sut.ReserveAsync(id.Id, id.Name); - - await sut.AddAsync(token!); - } - } -} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Orleans/JsonExternalSerializerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/JsonExternalSerializerTests.cs index 54de2a657..e1c283947 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Orleans/JsonExternalSerializerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Orleans/JsonExternalSerializerTests.cs @@ -13,6 +13,8 @@ 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 @@ -85,7 +87,7 @@ namespace Squidex.Infrastructure.Orleans var reader = A.Fake(); A.CallTo(() => reader.ReadByteArray(A._, A._, A._)) - .Invokes(new Action((b, o, l) => buffer.Read(b, o, l))); + .Invokes(new Action((array, offset, length) => buffer.Read(array, offset, length))); A.CallTo(() => reader.CurrentPosition) .ReturnsLazily(x => (int)buffer.Position); A.CallTo(() => reader.Length) diff --git a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromJsonTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromJsonTests.cs index dd6b943b5..3c59a2549 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromJsonTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Queries/QueryFromJsonTests.cs @@ -615,16 +615,16 @@ namespace Squidex.Infrastructure.Queries $"{field}" }; - foreach (var f in fields) + foreach (var fieldName in fields) { foreach (var (_, op, output) in AllOps.Where(x => opFilter(x.Operator))) { var expected = output - .Replace("$FIELD", f) - .Replace("$VALUE", valueString); + .Replace("$FIELD", fieldName, StringComparison.Ordinal) + .Replace("$VALUE", valueString, StringComparison.Ordinal); - yield return new[] { f, op, value, expected }; + yield return new[] { fieldName, op, value, expected }; } } } @@ -638,16 +638,16 @@ namespace Squidex.Infrastructure.Queries $"json.nested.{field}" }; - foreach (var f in fields) + foreach (var fieldName in fields) { foreach (var (_, op, output) in AllOps.Where(x => opFilter(x.Operator))) { var expected = output - .Replace("$FIELD", f) - .Replace("$VALUE", valueString); + .Replace("$FIELD", fieldName, StringComparison.Ordinal) + .Replace("$VALUE", valueString, StringComparison.Ordinal); - yield return new[] { f, op, value, expected }; + yield return new[] { fieldName, op, value, expected }; } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Reflection/ReflectionExtensionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Reflection/ReflectionExtensionTests.cs index c121d2220..0c320fbde 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Reflection/ReflectionExtensionTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Reflection/ReflectionExtensionTests.cs @@ -28,7 +28,7 @@ namespace Squidex.Infrastructure.Reflection string Sub2Prop { get; set; } } - private class MyMain + private sealed class MyMain { public string MainProp { get; set; } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj b/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj index a6d9075c5..23c822ece 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj +++ b/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj @@ -15,6 +15,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/backend/tests/Squidex.Infrastructure.Tests/States/PersistenceBatchTests.cs b/backend/tests/Squidex.Infrastructure.Tests/States/PersistenceBatchTests.cs index a7a53af4b..fe8b39617 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/States/PersistenceBatchTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/States/PersistenceBatchTests.cs @@ -6,7 +6,9 @@ // ========================================================================== using System.Collections.Generic; +using System.Globalization; using System.Linq; +using System.Threading; using System.Threading.Tasks; using FakeItEasy; using Squidex.Infrastructure.EventSourcing; @@ -113,16 +115,16 @@ namespace Squidex.Infrastructure.States await persistence1.WriteSnapshotAsync(12); await persistence2.WriteSnapshotAsync(12); - A.CallTo(() => snapshotStore.WriteAsync(A._, A._, A._, A._)) + A.CallTo(() => snapshotStore.WriteAsync(A._, A._, A._, A._, A._)) .MustNotHaveHappened(); - A.CallTo(() => snapshotStore.WriteManyAsync(A>._)) + A.CallTo(() => snapshotStore.WriteManyAsync(A>._, A._)) .MustNotHaveHappened(); await bulk.CommitAsync(); await bulk.DisposeAsync(); - A.CallTo(() => snapshotStore.WriteManyAsync(A>.That.Matches(x => x.Count() == 2))) + A.CallTo(() => snapshotStore.WriteManyAsync(A>.That.Matches(x => x.Count() == 2), A._)) .MustHaveHappenedOnceExactly(); } @@ -145,16 +147,16 @@ namespace Squidex.Infrastructure.States await persistence1_1.WriteSnapshotAsync(12); await persistence1_2.WriteSnapshotAsync(12); - A.CallTo(() => snapshotStore.WriteAsync(A._, A._, A._, A._)) + A.CallTo(() => snapshotStore.WriteAsync(A._, A._, A._, A._, A._)) .MustNotHaveHappened(); - A.CallTo(() => snapshotStore.WriteManyAsync(A>._)) + A.CallTo(() => snapshotStore.WriteManyAsync(A>._, A._)) .MustNotHaveHappened(); await bulk.CommitAsync(); await bulk.DisposeAsync(); - A.CallTo(() => snapshotStore.WriteManyAsync(A>.That.Matches(x => x.Count() == 1))) + A.CallTo(() => snapshotStore.WriteManyAsync(A>.That.Matches(x => x.Count() == 1), A._)) .MustHaveHappenedOnceExactly(); } @@ -174,16 +176,16 @@ namespace Squidex.Infrastructure.States await persistence1.WriteSnapshotAsync(12); await persistence1.WriteSnapshotAsync(13); - A.CallTo(() => snapshotStore.WriteAsync(A._, A._, A._, A._)) + A.CallTo(() => snapshotStore.WriteAsync(A._, A._, A._, A._, A._)) .MustNotHaveHappened(); - A.CallTo(() => snapshotStore.WriteManyAsync(A>._)) + A.CallTo(() => snapshotStore.WriteManyAsync(A>._, A._)) .MustNotHaveHappened(); await bulk.CommitAsync(); await bulk.DisposeAsync(); - A.CallTo(() => snapshotStore.WriteManyAsync(A>.That.Matches(x => x.Count() == 1))) + A.CallTo(() => snapshotStore.WriteManyAsync(A>.That.Matches(x => x.Count() == 1), A._)) .MustHaveHappenedOnceExactly(); } @@ -200,7 +202,7 @@ namespace Squidex.Infrastructure.States foreach (var @event in stream) { var eventData = new EventData("Type", new EnvelopeHeaders(), "Payload"); - var eventStored = new StoredEvent(id.ToString(), i.ToString(), i, eventData); + var eventStored = new StoredEvent(id.ToString(), i.ToString(CultureInfo.InvariantCulture), i, eventData); storedStream.Add(eventStored); @@ -218,7 +220,7 @@ namespace Squidex.Infrastructure.States var streamNames = streams.Keys.Select(x => x.ToString()).ToArray(); - A.CallTo(() => eventStore.QueryManyAsync(A>.That.IsSameSequenceAs(streamNames))) + A.CallTo(() => eventStore.QueryManyAsync(A>.That.IsSameSequenceAs(streamNames), A._)) .Returns(storedStreams); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs b/backend/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs index e9aa8e833..5f08f6eaa 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs @@ -7,7 +7,9 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; +using System.Threading; using System.Threading.Tasks; using FakeItEasy; using Squidex.Infrastructure.EventSourcing; @@ -70,7 +72,7 @@ namespace Squidex.Infrastructure.States { var storedEvent = new StoredEvent("1", "1", 0, new EventData("Type", new EnvelopeHeaders(), "Payload")); - A.CallTo(() => eventStore.QueryAsync(key.ToString(), 0)) + A.CallTo(() => eventStore.QueryAsync(key.ToString(), 0, A._)) .Returns(new List { storedEvent }); A.CallTo(() => eventDataFormatter.ParseIfKnown(storedEvent)) @@ -88,7 +90,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_read_read_from_snapshot_store() { - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key, A._)) .Returns(("2", true, 2L)); SetupEventStore(3, 2); @@ -101,14 +103,14 @@ namespace Squidex.Infrastructure.States Assert.False(persistence.IsSnapshotStale); - A.CallTo(() => eventStore.QueryAsync(key.ToString(), 3)) + A.CallTo(() => eventStore.QueryAsync(key.ToString(), 3, A._)) .MustHaveHappened(); } [Fact] public async Task Should_mark_as_stale_if_snapshot_old_than_events() { - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key, A._)) .Returns(("2", true, 1L)); SetupEventStore(3, 2, 2); @@ -121,14 +123,14 @@ namespace Squidex.Infrastructure.States Assert.True(persistence.IsSnapshotStale); - A.CallTo(() => eventStore.QueryAsync(key.ToString(), 2)) + A.CallTo(() => eventStore.QueryAsync(key.ToString(), 2, A._)) .MustHaveHappened(); } [Fact] public async Task Should_throw_exception_if_events_are_older_than_snapshot() { - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key, A._)) .Returns(("2", true, 2L)); SetupEventStore(3, 0, 3); @@ -143,7 +145,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_throw_exception_if_events_have_gaps_to_snapshot() { - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key, A._)) .Returns(("2", true, 2L)); SetupEventStore(3, 4, 3); @@ -180,7 +182,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_throw_exception_if_other_version_found_from_snapshot() { - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key, A._)) .Returns(("2", true, 2L)); SetupEventStore(0); @@ -217,12 +219,12 @@ namespace Squidex.Infrastructure.States await persistence.WriteEventAsync(Envelope.Create(new MyEvent())); await persistence.WriteEventAsync(Envelope.Create(new MyEvent())); - A.CallTo(() => eventStore.AppendAsync(A._, key.ToString(), 2, A>.That.Matches(x => x.Count == 1))) + A.CallTo(() => eventStore.AppendAsync(A._, key.ToString(), 2, A>.That.Matches(x => x.Count == 1), A._)) .MustHaveHappened(); - A.CallTo(() => eventStore.AppendAsync(A._, key.ToString(), 3, A>.That.Matches(x => x.Count == 1))) + A.CallTo(() => eventStore.AppendAsync(A._, key.ToString(), 3, A>.That.Matches(x => x.Count == 1), A._)) .MustHaveHappened(); - A.CallTo(() => snapshotStore.WriteAsync(A._, A._, A._, A._)) + A.CallTo(() => snapshotStore.WriteAsync(A._, A._, A._, A._, A._)) .MustNotHaveHappened(); } @@ -233,14 +235,14 @@ namespace Squidex.Infrastructure.States await persistence.WriteEventAsync(Envelope.Create(new MyEvent())); - A.CallTo(() => eventStore.AppendAsync(A._, key.ToString(), EtagVersion.Empty, A>.That.Matches(x => x.Count == 1))) + A.CallTo(() => eventStore.AppendAsync(A._, key.ToString(), EtagVersion.Empty, A>.That.Matches(x => x.Count == 1), A._)) .MustHaveHappened(); } [Fact] public async Task Should_write_snapshot_to_store() { - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key, A._)) .Returns(("2", true, 2L)); SetupEventStore(3); @@ -257,16 +259,16 @@ namespace Squidex.Infrastructure.States await persistence.WriteEventAsync(Envelope.Create(new MyEvent())); await persistence.WriteSnapshotAsync("5"); - A.CallTo(() => snapshotStore.WriteAsync(key, "4", 2, 3)) + A.CallTo(() => snapshotStore.WriteAsync(key, "4", 2, 3, A._)) .MustHaveHappened(); - A.CallTo(() => snapshotStore.WriteAsync(key, "5", 3, 4)) + A.CallTo(() => snapshotStore.WriteAsync(key, "5", 3, 4, A._)) .MustHaveHappened(); } [Fact] public async Task Should_write_snapshot_to_store_if_not_read_before() { - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key, A._)) .Returns((null!, true, EtagVersion.Empty)); SetupEventStore(3); @@ -283,16 +285,16 @@ namespace Squidex.Infrastructure.States await persistence.WriteEventAsync(Envelope.Create(new MyEvent())); await persistence.WriteSnapshotAsync("5"); - A.CallTo(() => snapshotStore.WriteAsync(key, "4", 2, 3)) + A.CallTo(() => snapshotStore.WriteAsync(key, "4", 2, 3, A._)) .MustHaveHappened(); - A.CallTo(() => snapshotStore.WriteAsync(key, "5", 3, 4)) + A.CallTo(() => snapshotStore.WriteAsync(key, "5", 3, 4, A._)) .MustHaveHappened(); } [Fact] public async Task Should_not_write_snapshot_to_store_if_not_changed() { - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key, A._)) .Returns(("0", true, 2)); SetupEventStore(3); @@ -305,7 +307,7 @@ namespace Squidex.Infrastructure.States await persistence.WriteSnapshotAsync("4"); - A.CallTo(() => snapshotStore.WriteAsync(key, A._, A._, A._)) + A.CallTo(() => snapshotStore.WriteAsync(key, A._, A._, A._, A._)) .MustNotHaveHappened(); } @@ -319,7 +321,7 @@ namespace Squidex.Infrastructure.States await persistence.ReadAsync(); - A.CallTo(() => eventStore.AppendAsync(A._, key.ToString(), 2, A>.That.Matches(x => x.Count == 1))) + A.CallTo(() => eventStore.AppendAsync(A._, key.ToString(), 2, A>.That.Matches(x => x.Count == 1), A._)) .Throws(new WrongEventVersionException(1, 1)); await Assert.ThrowsAsync(() => persistence.WriteEventAsync(Envelope.Create(new MyEvent()))); @@ -332,10 +334,10 @@ namespace Squidex.Infrastructure.States await persistence.DeleteAsync(); - A.CallTo(() => eventStore.DeleteStreamAsync(key.ToString())) + A.CallTo(() => eventStore.DeleteStreamAsync(key.ToString(), A._)) .MustHaveHappened(); - A.CallTo(() => snapshotStore.RemoveAsync(key)) + A.CallTo(() => snapshotStore.RemoveAsync(key, A._)) .MustNotHaveHappened(); } @@ -346,10 +348,10 @@ namespace Squidex.Infrastructure.States await persistence.DeleteAsync(); - A.CallTo(() => eventStore.DeleteStreamAsync(key.ToString())) + A.CallTo(() => eventStore.DeleteStreamAsync(key.ToString(), A._)) .MustHaveHappened(); - A.CallTo(() => snapshotStore.RemoveAsync(key)) + A.CallTo(() => snapshotStore.RemoveAsync(key, A._)) .MustHaveHappened(); } @@ -372,7 +374,7 @@ namespace Squidex.Infrastructure.States foreach (var @event in events) { var eventData = new EventData("Type", new EnvelopeHeaders(), "Payload"); - var eventStored = new StoredEvent(key.ToString(), i.ToString(), i, eventData); + var eventStored = new StoredEvent(key.ToString(), i.ToString(CultureInfo.InvariantCulture), i, eventData); eventsStored.Add(eventStored); @@ -385,8 +387,8 @@ namespace Squidex.Infrastructure.States i++; } - A.CallTo(() => eventStore.QueryAsync(key.ToString(), readPosition)) + A.CallTo(() => eventStore.QueryAsync(key.ToString(), readPosition, A._)) .Returns(eventsStored); } } -} \ No newline at end of file +} diff --git a/backend/tests/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs b/backend/tests/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs index 0a85f7251..9a67bbecb 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Threading; using System.Threading.Tasks; using FakeItEasy; using Squidex.Infrastructure.EventSourcing; @@ -30,7 +31,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_read_from_store() { - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key, A._)) .Returns((20, true, 10)); var persistedState = Save.Snapshot(0); @@ -45,7 +46,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_not_read_from_store_if_not_valid() { - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key, A._)) .Returns((20, false, 10)); var persistedState = Save.Snapshot(0); @@ -60,7 +61,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_return_empty_version_if_version_negative() { - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key, A._)) .Returns((20, true, -10)); var persistedState = Save.Snapshot(0); @@ -74,7 +75,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_set_to_empty_if_store_returns_not_found() { - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key, A._)) .Returns((20, true, EtagVersion.Empty)); var persistedState = Save.Snapshot(0); @@ -89,7 +90,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_throw_exception_if_not_found_and_version_expected() { - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key, A._)) .Returns((123, true, EtagVersion.Empty)); var persistedState = Save.Snapshot(0); @@ -101,7 +102,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_throw_exception_if_other_version_found() { - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key, A._)) .Returns((123, true, 2)); var persistedState = Save.Snapshot(0); @@ -113,7 +114,7 @@ namespace Squidex.Infrastructure.States [Fact] public async Task Should_write_to_store_with_previous_version() { - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key, A._)) .Returns((20, true, 10)); var persistedState = Save.Snapshot(0); @@ -126,7 +127,7 @@ namespace Squidex.Infrastructure.States await persistence.WriteSnapshotAsync(100); - A.CallTo(() => snapshotStore.WriteAsync(key, 100, 10, 11)) + A.CallTo(() => snapshotStore.WriteAsync(key, 100, 10, 11, A._)) .MustHaveHappened(); } @@ -137,17 +138,17 @@ namespace Squidex.Infrastructure.States await persistence.WriteSnapshotAsync(100); - A.CallTo(() => snapshotStore.WriteAsync(key, 100, EtagVersion.Empty, 0)) + A.CallTo(() => snapshotStore.WriteAsync(key, 100, EtagVersion.Empty, 0, A._)) .MustHaveHappened(); } [Fact] public async Task Should_not_wrap_exception_if_writing_to_store_with_previous_version() { - A.CallTo(() => snapshotStore.ReadAsync(key)) + A.CallTo(() => snapshotStore.ReadAsync(key, A._)) .Returns((20, true, 10)); - A.CallTo(() => snapshotStore.WriteAsync(key, 100, 10, 11)) + A.CallTo(() => snapshotStore.WriteAsync(key, 100, 10, 11, A._)) .Throws(new InconsistentStateException(1, 1, new InvalidOperationException())); var persistedState = Save.Snapshot(0); @@ -166,10 +167,10 @@ namespace Squidex.Infrastructure.States await persistence.DeleteAsync(); - A.CallTo(() => eventStore.DeleteStreamAsync(A._)) + A.CallTo(() => eventStore.DeleteStreamAsync(A._, A._)) .MustNotHaveHappened(); - A.CallTo(() => snapshotStore.RemoveAsync(key)) + A.CallTo(() => snapshotStore.RemoveAsync(key, A._)) .MustHaveHappened(); } @@ -178,8 +179,8 @@ namespace Squidex.Infrastructure.States { await sut.ClearSnapshotsAsync(); - A.CallTo(() => snapshotStore.ClearAsync()) + A.CallTo(() => snapshotStore.ClearAsync(A._)) .MustHaveHappened(); } } -} \ No newline at end of file +} diff --git a/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/ApiUsageTrackerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/ApiUsageTrackerTests.cs index 1acb5b2af..fccd12345 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/ApiUsageTrackerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/ApiUsageTrackerTests.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using FakeItEasy; using FluentAssertions; @@ -16,6 +17,8 @@ namespace Squidex.Infrastructure.UsageTracking { public class ApiUsageTrackerTests { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; private readonly IUsageTracker usageTracker = A.Fake(); private readonly string key = Guid.NewGuid().ToString(); private readonly string category = Guid.NewGuid().ToString(); @@ -24,21 +27,32 @@ namespace Squidex.Infrastructure.UsageTracking public ApiUsageTrackerTests() { + ct = cts.Token; + sut = new ApiUsageTracker(usageTracker); } + [Fact] + public async Task Should_forward_delete_call() + { + await sut.DeleteAsync(key, ct); + + A.CallTo(() => usageTracker.DeleteAsync($"{key}_API", A._)) + .MustHaveHappened(); + } + [Fact] public async Task Should_track_usage() { Counters? measuredCounters = null; - A.CallTo(() => usageTracker.TrackAsync(date, $"{key}_API", null, A.Ignored)) + A.CallTo(() => usageTracker.TrackAsync(date, $"{key}_API", null, A.Ignored, ct)) .Invokes(args => { measuredCounters = args.GetArgument(3)!; }); - await sut.TrackAsync(date, key, null, 4, 120, 1024); + await sut.TrackAsync(date, key, null, 4, 120, 1024, ct); measuredCounters.Should().BeEquivalentTo(new Counters { @@ -56,10 +70,10 @@ namespace Squidex.Infrastructure.UsageTracking [ApiUsageTracker.CounterTotalCalls] = 4 }; - A.CallTo(() => usageTracker.GetForMonthAsync($"{key}_API", date, category)) + A.CallTo(() => usageTracker.GetForMonthAsync($"{key}_API", date, category, ct)) .Returns(counters); - var result = await sut.GetMonthCallsAsync(key, date, category); + var result = await sut.GetMonthCallsAsync(key, date, category, ct); Assert.Equal(4, result); } @@ -72,10 +86,10 @@ namespace Squidex.Infrastructure.UsageTracking [ApiUsageTracker.CounterTotalBytes] = 14 }; - A.CallTo(() => usageTracker.GetForMonthAsync($"{key}_API", date, category)) + A.CallTo(() => usageTracker.GetForMonthAsync($"{key}_API", date, category, ct)) .Returns(counters); - var result = await sut.GetMonthBytesAsync(key, date, category); + var result = await sut.GetMonthBytesAsync(key, date, category, ct); Assert.Equal(14, result); } @@ -112,13 +126,13 @@ namespace Squidex.Infrastructure.UsageTracking [ApiUsageTracker.CounterTotalBytes] = 400 }; - A.CallTo(() => usageTracker.GetForMonthAsync($"{key}_API", DateTime.Today, null)) + A.CallTo(() => usageTracker.GetForMonthAsync($"{key}_API", DateTime.Today, null, ct)) .Returns(forMonth); - A.CallTo(() => usageTracker.QueryAsync($"{key}_API", dateFrom, dateTo)) + A.CallTo(() => usageTracker.QueryAsync($"{key}_API", dateFrom, dateTo, ct)) .Returns(counters); - var (summary, stats) = await sut.QueryAsync(key, dateFrom, dateTo); + var (summary, stats) = await sut.QueryAsync(key, dateFrom, dateTo, ct); stats.Should().BeEquivalentTo(new Dictionary> { @@ -153,4 +167,4 @@ namespace Squidex.Infrastructure.UsageTracking }; } } -} \ No newline at end of file +} diff --git a/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs index 6bb264509..949d917c0 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using FakeItEasy; using FluentAssertions; @@ -17,6 +18,8 @@ namespace Squidex.Infrastructure.UsageTracking { public class BackgroundUsageTrackerTests { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; private readonly IUsageRepository usageStore = A.Fake(); private readonly ISemanticLog log = A.Fake(); private readonly string key = Guid.NewGuid().ToString(); @@ -25,7 +28,12 @@ namespace Squidex.Infrastructure.UsageTracking public BackgroundUsageTrackerTests() { - sut = new BackgroundUsageTracker(usageStore, log); + ct = cts.Token; + + sut = new BackgroundUsageTracker(usageStore, log) + { + ForceWrite = true + }; } [Fact] @@ -33,7 +41,7 @@ namespace Squidex.Infrastructure.UsageTracking { sut.Dispose(); - await Assert.ThrowsAsync(() => sut.TrackAsync(date, key, "category1", new Counters())); + await Assert.ThrowsAsync(() => sut.TrackAsync(date, key, "category1", new Counters(), ct)); } [Fact] @@ -41,7 +49,7 @@ namespace Squidex.Infrastructure.UsageTracking { sut.Dispose(); - await Assert.ThrowsAsync(() => sut.QueryAsync(key, date, date.AddDays(1))); + await Assert.ThrowsAsync(() => sut.QueryAsync(key, date, date.AddDays(1), ct)); } [Fact] @@ -49,7 +57,7 @@ namespace Squidex.Infrastructure.UsageTracking { sut.Dispose(); - await Assert.ThrowsAsync(() => sut.GetForMonthAsync(key, date, null)); + await Assert.ThrowsAsync(() => sut.GetForMonthAsync(key, date, null, ct)); } [Fact] @@ -57,7 +65,16 @@ namespace Squidex.Infrastructure.UsageTracking { sut.Dispose(); - await Assert.ThrowsAsync(() => sut.GetAsync(key, date, date, null)); + await Assert.ThrowsAsync(() => sut.GetAsync(key, date, date, null, ct)); + } + + [Fact] + public async Task Should_forward_delete_call() + { + await sut.DeleteAsync(key, ct); + + A.CallTo(() => usageStore.DeleteAsync(key, ct)) + .MustHaveHappened(); } [Fact] @@ -74,11 +91,11 @@ namespace Squidex.Infrastructure.UsageTracking new StoredUsage("category2", date.AddDays(7), Counters(b: 22)) }; - A.CallTo(() => usageStore.QueryAsync(key, dateFrom, dateTo)) + A.CallTo(() => usageStore.QueryAsync(key, dateFrom, dateTo, ct)) .Returns(originalData); - var result1 = await sut.GetForMonthAsync(key, date, null); - var result2 = await sut.GetForMonthAsync(key, date, "category2"); + var result1 = await sut.GetForMonthAsync(key, date, null, ct); + var result2 = await sut.GetForMonthAsync(key, date, "category2", ct); Assert.Equal(38, result1["A"]); Assert.Equal(55, result1["B"]); @@ -100,11 +117,11 @@ namespace Squidex.Infrastructure.UsageTracking new StoredUsage("category2", date.AddDays(7), Counters(b: 22)) }; - A.CallTo(() => usageStore.QueryAsync(key, dateFrom, dateTo)) + A.CallTo(() => usageStore.QueryAsync(key, dateFrom, dateTo, ct)) .Returns(originalData); - var result1 = await sut.GetAsync(key, dateFrom, dateTo, null); - var result2 = await sut.GetAsync(key, dateFrom, dateTo, "category2"); + var result1 = await sut.GetAsync(key, dateFrom, dateTo, null, ct); + var result2 = await sut.GetAsync(key, dateFrom, dateTo, "category2", ct); Assert.Equal(38, result1["A"]); Assert.Equal(55, result1["B"]); @@ -118,10 +135,10 @@ namespace Squidex.Infrastructure.UsageTracking var dateFrom = date; var dateTo = dateFrom.AddDays(4); - A.CallTo(() => usageStore.QueryAsync(key, dateFrom, dateTo)) + A.CallTo(() => usageStore.QueryAsync(key, dateFrom, dateTo, ct)) .Returns(new List()); - var result = await sut.QueryAsync(key, dateFrom, dateTo); + var result = await sut.QueryAsync(key, dateFrom, dateTo, ct); var expected = new Dictionary> { @@ -153,10 +170,10 @@ namespace Squidex.Infrastructure.UsageTracking new StoredUsage(null, dateFrom.AddDays(2), Counters(a: 11, b: 14)) }; - A.CallTo(() => usageStore.QueryAsync(key, dateFrom, dateTo)) + A.CallTo(() => usageStore.QueryAsync(key, dateFrom, dateTo, ct)) .Returns(originalData); - var result = await sut.QueryAsync(key, dateFrom, dateTo); + var result = await sut.QueryAsync(key, dateFrom, dateTo, ct); var expected = new Dictionary> { @@ -182,31 +199,37 @@ namespace Squidex.Infrastructure.UsageTracking } [Fact] - public async Task Should_aggregate_and_store_on_dispose() + public async Task Should_write_usage_in_batches() { var key1 = Guid.NewGuid().ToString(); var key2 = Guid.NewGuid().ToString(); var key3 = Guid.NewGuid().ToString(); - await sut.TrackAsync(date, key1, "my-category", Counters(a: 1, b: 1000)); + await sut.TrackAsync(date, key1, "my-category", Counters(a: 1, b: 1000), ct); - await sut.TrackAsync(date, key2, "my-category", Counters(a: 1.0, b: 2000)); - await sut.TrackAsync(date, key2, "my-category", Counters(a: 0.5, b: 3000)); + await sut.TrackAsync(date, key2, "my-category", Counters(a: 1.0, b: 2000), ct); + await sut.TrackAsync(date, key2, "my-category", Counters(a: 0.5, b: 3000), ct); - await sut.TrackAsync(date, key3, "my-category", Counters(a: 0.3, b: 4000)); - await sut.TrackAsync(date, key3, "my-category", Counters(a: 0.1, b: 5000)); + await sut.TrackAsync(date, key3, "my-category", Counters(a: 0.3, b: 4000), ct); + await sut.TrackAsync(date, key3, "my-category", Counters(a: 0.1, b: 5000), ct); - await sut.TrackAsync(date, key3, null, Counters(a: 0.5, b: 2000)); - await sut.TrackAsync(date, key3, null, Counters(a: 0.5, b: 6000)); + await sut.TrackAsync(date, key3, null, Counters(a: 0.5, b: 2000), ct); + await sut.TrackAsync(date, key3, null, Counters(a: 0.5, b: 6000), ct); UsageUpdate[]? updates = null; - A.CallTo(() => usageStore.TrackUsagesAsync(A._)) - .Invokes((UsageUpdate[] u) => updates = u); + A.CallTo(() => usageStore.TrackUsagesAsync(A._, A._)) + .Invokes(args => + { + updates = args.GetArgument(0)!; + }); sut.Next(); sut.Dispose(); + // Wait for the timer to trigger. + await Task.Delay(500, ct); + updates.Should().BeEquivalentTo(new[] { new UsageUpdate(date, key1, "my-category", Counters(a: 1.0, b: 1000)), @@ -215,7 +238,7 @@ namespace Squidex.Infrastructure.UsageTracking new UsageUpdate(date, key3, "*", Counters(1, 8000)) }, o => o.ComparingByMembers()); - A.CallTo(() => usageStore.TrackUsagesAsync(A._)) + A.CallTo(() => usageStore.TrackUsagesAsync(A._, A._)) .MustHaveHappened(); } diff --git a/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/CachingUsageTrackerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/CachingUsageTrackerTests.cs index a407911b1..899616799 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/CachingUsageTrackerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/UsageTracking/CachingUsageTrackerTests.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Threading; using System.Threading.Tasks; using FakeItEasy; using Microsoft.Extensions.Caching.Memory; @@ -16,6 +17,8 @@ namespace Squidex.Infrastructure.UsageTracking { public class CachingUsageTrackerTests { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; private readonly MemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); private readonly string key = Guid.NewGuid().ToString(); private readonly string category = Guid.NewGuid().ToString(); @@ -25,17 +28,28 @@ namespace Squidex.Infrastructure.UsageTracking public CachingUsageTrackerTests() { + ct = cts.Token; + sut = new CachingUsageTracker(inner, cache); } + [Fact] + public async Task Should_forward_delete_call() + { + await sut.DeleteAsync(key, ct); + + A.CallTo(() => inner.DeleteAsync(key, ct)) + .MustHaveHappened(); + } + [Fact] public async Task Should_forward_track_call() { var counters = new Counters(); - await sut.TrackAsync(date, key, "my-category", counters); + await sut.TrackAsync(date, key, "my-category", counters, ct); - A.CallTo(() => inner.TrackAsync(date, key, "my-category", counters)) + A.CallTo(() => inner.TrackAsync(date, key, "my-category", counters, ct)) .MustHaveHappened(); } @@ -45,9 +59,9 @@ namespace Squidex.Infrastructure.UsageTracking var dateFrom = date; var dateTo = dateFrom.AddDays(10); - await sut.QueryAsync(key, dateFrom, dateTo); + await sut.QueryAsync(key, dateFrom, dateTo, ct); - A.CallTo(() => inner.QueryAsync(key, dateFrom, dateTo)) + A.CallTo(() => inner.QueryAsync(key, dateFrom, dateTo, ct)) .MustHaveHappened(); } @@ -56,16 +70,16 @@ namespace Squidex.Infrastructure.UsageTracking { var counters = new Counters(); - A.CallTo(() => inner.GetForMonthAsync(key, date, category)) + A.CallTo(() => inner.GetForMonthAsync(key, date, category, ct)) .Returns(counters); - var result1 = await sut.GetForMonthAsync(key, date, category); - var result2 = await sut.GetForMonthAsync(key, date, category); + var result1 = await sut.GetForMonthAsync(key, date, category, ct); + var result2 = await sut.GetForMonthAsync(key, date, category, ct); Assert.Same(counters, result1); Assert.Same(counters, result2); - A.CallTo(() => inner.GetForMonthAsync(key, DateTime.Today, category)) + A.CallTo(() => inner.GetForMonthAsync(key, DateTime.Today, category, ct)) .MustHaveHappenedOnceExactly(); } @@ -77,16 +91,16 @@ namespace Squidex.Infrastructure.UsageTracking var dateFrom = date; var dateTo = dateFrom.AddDays(10); - A.CallTo(() => inner.GetAsync(key, dateFrom, dateTo, category)) + A.CallTo(() => inner.GetAsync(key, dateFrom, dateTo, category, ct)) .Returns(counters); - var result1 = await sut.GetAsync(key, dateFrom, dateTo, category); - var result2 = await sut.GetAsync(key, dateFrom, dateTo, category); + var result1 = await sut.GetAsync(key, dateFrom, dateTo, category, ct); + var result2 = await sut.GetAsync(key, dateFrom, dateTo, category, ct); Assert.Same(counters, result1); Assert.Same(counters, result2); - A.CallTo(() => inner.GetAsync(key, dateFrom, dateTo, category)) + A.CallTo(() => inner.GetAsync(key, dateFrom, dateTo, category, ct)) .MustHaveHappenedOnceExactly(); } @@ -96,12 +110,12 @@ namespace Squidex.Infrastructure.UsageTracking var dateFrom = date; var dateTo = dateFrom.AddDays(10); - var result1 = await sut.QueryAsync(key, dateFrom, dateTo); - var result2 = await sut.QueryAsync(key, dateFrom, dateTo); + var result1 = await sut.QueryAsync(key, dateFrom, dateTo, ct); + var result2 = await sut.QueryAsync(key, dateFrom, dateTo, ct); Assert.NotSame(result2, result1); - A.CallTo(() => inner.QueryAsync(key, dateFrom, dateTo)) + A.CallTo(() => inner.QueryAsync(key, dateFrom, dateTo, ct)) .MustHaveHappenedTwiceOrMore(); } } diff --git a/backend/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs b/backend/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs index 5423987be..005d9a2a8 100644 --- a/backend/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs +++ b/backend/tests/Squidex.Web.Tests/ApiExceptionFilterAttributeTests.cs @@ -21,6 +21,8 @@ using Squidex.Infrastructure.Validation; using Squidex.Log; using Xunit; +#pragma warning disable MA0015 // Specify the parameter name in ArgumentException + namespace Squidex.Web { public class ApiExceptionFilterAttributeTests diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs index c91afc099..cbfc2a3f9 100644 --- a/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs +++ b/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs @@ -66,7 +66,7 @@ namespace Squidex.Web.Pipeline { SetupUser(); - A.CallTo(() => appProvider.GetAppAsync(appName, false)) + A.CallTo(() => appProvider.GetAppAsync(appName, false, httpContext.RequestAborted)) .Returns(Task.FromResult(null)); await sut.OnActionExecutionAsync(actionExecutingContext, next); @@ -82,7 +82,7 @@ namespace Squidex.Web.Pipeline var app = CreateApp(appName); - A.CallTo(() => appProvider.GetAppAsync(appName, false)) + A.CallTo(() => appProvider.GetAppAsync(appName, false, httpContext.RequestAborted)) .Returns(app); await sut.OnActionExecutionAsync(actionExecutingContext, next); @@ -101,7 +101,7 @@ namespace Squidex.Web.Pipeline user.AddClaim(new Claim(OpenIdClaims.Subject, "user1")); user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.my-app")); - A.CallTo(() => appProvider.GetAppAsync(appName, true)) + A.CallTo(() => appProvider.GetAppAsync(appName, true, httpContext.RequestAborted)) .Returns(app); await sut.OnActionExecutionAsync(actionExecutingContext, next); @@ -124,7 +124,7 @@ namespace Squidex.Web.Pipeline user.AddClaim(new Claim(OpenIdClaims.Subject, "user1")); - A.CallTo(() => appProvider.GetAppAsync(appName, true)) + A.CallTo(() => appProvider.GetAppAsync(appName, true, httpContext.RequestAborted)) .Returns(app); await sut.OnActionExecutionAsync(actionExecutingContext, next); @@ -148,7 +148,7 @@ namespace Squidex.Web.Pipeline user.AddClaim(new Claim(OpenIdClaims.Subject, "user1")); user.AddClaim(new Claim(OpenIdClaims.ClientId, DefaultClients.Frontend)); - A.CallTo(() => appProvider.GetAppAsync(appName, false)) + A.CallTo(() => appProvider.GetAppAsync(appName, false, httpContext.RequestAborted)) .Returns(app); await sut.OnActionExecutionAsync(actionExecutingContext, next); @@ -171,7 +171,7 @@ namespace Squidex.Web.Pipeline var app = CreateApp(appName, appClient: "client1"); - A.CallTo(() => appProvider.GetAppAsync(appName, true)) + A.CallTo(() => appProvider.GetAppAsync(appName, true, httpContext.RequestAborted)) .Returns(app); await sut.OnActionExecutionAsync(actionExecutingContext, next); @@ -188,7 +188,7 @@ namespace Squidex.Web.Pipeline var app = CreateApp(appName, appClient: "client1", allowAnonymous: true); - A.CallTo(() => appProvider.GetAppAsync(appName, true)) + A.CallTo(() => appProvider.GetAppAsync(appName, true, httpContext.RequestAborted)) .Returns(app); await sut.OnActionExecutionAsync(actionExecutingContext, next); @@ -211,7 +211,7 @@ namespace Squidex.Web.Pipeline actionContext.ActionDescriptor.EndpointMetadata.Add(new AllowAnonymousAttribute()); - A.CallTo(() => appProvider.GetAppAsync(appName, true)) + A.CallTo(() => appProvider.GetAppAsync(appName, true, httpContext.RequestAborted)) .Returns(app); await sut.OnActionExecutionAsync(actionExecutingContext, next); @@ -231,7 +231,7 @@ namespace Squidex.Web.Pipeline var app = CreateApp(appName); - A.CallTo(() => appProvider.GetAppAsync(appName, false)) + A.CallTo(() => appProvider.GetAppAsync(appName, false, httpContext.RequestAborted)) .Returns(app); await sut.OnActionExecutionAsync(actionExecutingContext, next); @@ -249,7 +249,7 @@ namespace Squidex.Web.Pipeline var app = CreateApp(appName, appClient: "client1"); - A.CallTo(() => appProvider.GetAppAsync(appName, false)) + A.CallTo(() => appProvider.GetAppAsync(appName, false, httpContext.RequestAborted)) .Returns(app); await sut.OnActionExecutionAsync(actionExecutingContext, next); @@ -267,7 +267,7 @@ namespace Squidex.Web.Pipeline Assert.True(isNextCalled); - A.CallTo(() => appProvider.GetAppAsync(A._, false)) + A.CallTo(() => appProvider.GetAppAsync(A._, false, httpContext.RequestAborted)) .MustNotHaveHappened(); } diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/CachingKeysMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/CachingKeysMiddlewareTests.cs index a521badf5..b4eaa296c 100644 --- a/backend/tests/Squidex.Web.Tests/Pipeline/CachingKeysMiddlewareTests.cs +++ b/backend/tests/Squidex.Web.Tests/Pipeline/CachingKeysMiddlewareTests.cs @@ -55,7 +55,7 @@ namespace Squidex.Web.Pipeline { foreach (var (state, callback) in callbacks) { - callback(state).Wait(); + callback(state).Wait(httpContext.RequestAborted); } }); @@ -322,7 +322,7 @@ namespace Squidex.Web.Pipeline action?.Invoke(); - await httpContext.Response.StartAsync(); + await httpContext.Response.StartAsync(httpContext.RequestAborted); } } } diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/SchemaResolverTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/SchemaResolverTests.cs index 3d78e59c0..aff9970f8 100644 --- a/backend/tests/Squidex.Web.Tests/Pipeline/SchemaResolverTests.cs +++ b/backend/tests/Squidex.Web.Tests/Pipeline/SchemaResolverTests.cs @@ -70,7 +70,7 @@ namespace Squidex.Web.Pipeline var schema = CreateSchema(false); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A._, true)) + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A._, true, httpContext.RequestAborted)) .Returns(schema); await sut.OnActionExecutionAsync(actionExecutingContext, next); @@ -85,7 +85,7 @@ namespace Squidex.Web.Pipeline var schema = CreateSchema(false); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A._, true)) + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A._, true, httpContext.RequestAborted)) .Returns(schema); await sut.OnActionExecutionAsync(actionExecutingContext, next); @@ -98,7 +98,7 @@ namespace Squidex.Web.Pipeline { actionContext.RouteData.Values["schema"] = schemaId.Id.ToString(); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A._, true)) + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, A._, true, httpContext.RequestAborted)) .Returns(Task.FromResult(null)); await sut.OnActionExecutionAsync(actionExecutingContext, next); @@ -113,7 +113,7 @@ namespace Squidex.Web.Pipeline var schema = CreateSchema(true); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, true)) + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, true, httpContext.RequestAborted)) .Returns(schema); await sut.OnActionExecutionAsync(actionExecutingContext, next); @@ -130,7 +130,7 @@ namespace Squidex.Web.Pipeline var schema = CreateSchema(true); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false)) + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Id, false, httpContext.RequestAborted)) .Returns(schema); await sut.OnActionExecutionAsync(actionExecutingContext, next); @@ -145,7 +145,7 @@ namespace Squidex.Web.Pipeline var schema = CreateSchema(true); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name, true)) + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name, true, httpContext.RequestAborted)) .Returns(schema); await sut.OnActionExecutionAsync(actionExecutingContext, next); @@ -162,7 +162,7 @@ namespace Squidex.Web.Pipeline var schema = CreateSchema(true); - A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name, false)) + A.CallTo(() => appProvider.GetSchemaAsync(appId.Id, schemaId.Name, false, httpContext.RequestAborted)) .Returns(schema); await sut.OnActionExecutionAsync(actionExecutingContext, next); @@ -180,7 +180,7 @@ namespace Squidex.Web.Pipeline Assert.True(isNextCalled); - A.CallTo(() => appProvider.GetAppAsync(A._, false)) + A.CallTo(() => appProvider.GetAppAsync(A._, false, httpContext.RequestAborted)) .MustNotHaveHappened(); } @@ -191,7 +191,7 @@ namespace Squidex.Web.Pipeline Assert.True(isNextCalled); - A.CallTo(() => appProvider.GetAppAsync(A._, false)) + A.CallTo(() => appProvider.GetAppAsync(A._, false, httpContext.RequestAborted)) .MustNotHaveHappened(); } diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/UsageMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/UsageMiddlewareTests.cs index 940658ff5..bb82fcccd 100644 --- a/backend/tests/Squidex.Web.Tests/Pipeline/UsageMiddlewareTests.cs +++ b/backend/tests/Squidex.Web.Tests/Pipeline/UsageMiddlewareTests.cs @@ -55,7 +55,7 @@ namespace Squidex.Web.Pipeline var date = instant.ToDateTimeUtc().Date; - A.CallTo(() => usageTracker.TrackAsync(date, A._, A._, A._, A._, A._)) + A.CallTo(() => usageTracker.TrackAsync(date, A._, A._, A._, A._, A._, default)) .MustNotHaveHappened(); } @@ -73,7 +73,7 @@ namespace Squidex.Web.Pipeline var date = instant.ToDateTimeUtc().Date; - A.CallTo(() => usageTracker.TrackAsync(date, A._, A._, A._, A._, A._)) + A.CallTo(() => usageTracker.TrackAsync(date, A._, A._, A._, A._, A._, default)) .MustNotHaveHappened(); } @@ -89,7 +89,7 @@ namespace Squidex.Web.Pipeline var date = instant.ToDateTimeUtc().Date; - A.CallTo(() => usageTracker.TrackAsync(date, A._, A._, 13, A._, A._)) + A.CallTo(() => usageTracker.TrackAsync(date, A._, A._, 13, A._, A._, default)) .MustHaveHappened(); } @@ -106,7 +106,7 @@ namespace Squidex.Web.Pipeline var date = instant.ToDateTimeUtc().Date; - A.CallTo(() => usageTracker.TrackAsync(date, A._, A._, 13, A._, 1024)) + A.CallTo(() => usageTracker.TrackAsync(date, A._, A._, 13, A._, 1024, default)) .MustHaveHappened(); } @@ -118,7 +118,7 @@ namespace Squidex.Web.Pipeline await sut.InvokeAsync(httpContext, async x => { - await x.Response.BodyWriter.WriteAsync(Encoding.Default.GetBytes("Hello World")); + await x.Response.BodyWriter.WriteAsync(Encoding.Default.GetBytes("Hello World"), httpContext.RequestAborted); await next(x); }); @@ -127,7 +127,7 @@ namespace Squidex.Web.Pipeline var date = instant.ToDateTimeUtc().Date; - A.CallTo(() => usageTracker.TrackAsync(date, A._, A._, 13, A._, 11)) + A.CallTo(() => usageTracker.TrackAsync(date, A._, A._, 13, A._, 11, default)) .MustHaveHappened(); } @@ -139,7 +139,7 @@ namespace Squidex.Web.Pipeline await sut.InvokeAsync(httpContext, async x => { - await x.Response.Body.WriteAsync(Encoding.Default.GetBytes("Hello World")); + await x.Response.Body.WriteAsync(Encoding.Default.GetBytes("Hello World"), httpContext.RequestAborted); await next(x); }); @@ -148,7 +148,7 @@ namespace Squidex.Web.Pipeline var date = instant.ToDateTimeUtc().Date; - A.CallTo(() => usageTracker.TrackAsync(date, A._, A._, 13, A._, 11)) + A.CallTo(() => usageTracker.TrackAsync(date, A._, A._, 13, A._, 11, default)) .MustHaveHappened(); } @@ -161,11 +161,11 @@ namespace Squidex.Web.Pipeline var tempFileName = Path.GetTempFileName(); try { - await File.WriteAllTextAsync(tempFileName, "Hello World"); + await File.WriteAllTextAsync(tempFileName, "Hello World", httpContext.RequestAborted); await sut.InvokeAsync(httpContext, async x => { - await x.Response.SendFileAsync(tempFileName, 0, new FileInfo(tempFileName).Length); + await x.Response.SendFileAsync(tempFileName, 0, new FileInfo(tempFileName).Length, httpContext.RequestAborted); await next(x); }); @@ -179,7 +179,7 @@ namespace Squidex.Web.Pipeline var date = instant.ToDateTimeUtc().Date; - A.CallTo(() => usageTracker.TrackAsync(date, A._, A._, 13, A._, 11)) + A.CallTo(() => usageTracker.TrackAsync(date, A._, A._, 13, A._, 11, default)) .MustHaveHappened(); } @@ -195,7 +195,7 @@ namespace Squidex.Web.Pipeline var date = instant.ToDateTimeUtc().Date; - A.CallTo(() => usageTracker.TrackAsync(date, A._, A._, A._, A._, A._)) + A.CallTo(() => usageTracker.TrackAsync(date, A._, A._, A._, A._, A._, default)) .MustNotHaveHappened(); } @@ -215,7 +215,8 @@ namespace Squidex.Web.Pipeline x.Timestamp == instant && x.RequestMethod == "GET" && x.RequestPath == "/my-path" && - x.Costs == 0))) + x.Costs == 0), + default)) .MustHaveHappened(); } } diff --git a/backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj b/backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj index 7b11adf67..020a5a89d 100644 --- a/backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj +++ b/backend/tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj @@ -14,6 +14,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/frontend/app/features/settings/pages/more/more-page.component.html b/frontend/app/features/settings/pages/more/more-page.component.html index 2e531e309..9d5fa51b7 100644 --- a/frontend/app/features/settings/pages/more/more-page.component.html +++ b/frontend/app/features/settings/pages/more/more-page.component.html @@ -87,18 +87,16 @@
-
{{ 'apps.archive' | sqxTranslate }}
+
{{ 'apps.delete' | sqxTranslate }}
- - {{ 'apps.archiveWarning' | sqxTranslate }} - + {{ 'apps.deleteWarning' | sqxTranslate }}
diff --git a/frontend/app/features/settings/pages/more/more-page.component.ts b/frontend/app/features/settings/pages/more/more-page.component.ts index 4d90fd67c..2861af820 100644 --- a/frontend/app/features/settings/pages/more/more-page.component.ts +++ b/frontend/app/features/settings/pages/more/more-page.component.ts @@ -104,7 +104,7 @@ export class MorePageComponent extends ResourceOwner implements OnInit { this.appsState.removeImage(this.app); } - public archiveApp() { + public deleteApp() { if (!this.isDeletable) { return; } diff --git a/frontend/app/shared/services/apps.service.spec.ts b/frontend/app/shared/services/apps.service.spec.ts index eee524fee..a9413933e 100644 --- a/frontend/app/shared/services/apps.service.spec.ts +++ b/frontend/app/shared/services/apps.service.spec.ts @@ -299,7 +299,7 @@ describe('AppsService', () => { req.flush({}); })); - it('should make delete request to archive app', + it('should make delete request to delete app', inject([AppsService, HttpTestingController], (appsService: AppsService, httpMock: HttpTestingController) => { const resource: Resource = { _links: {