From 285781b6005d01c5006904e333758d8aa0c02d2d Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle Date: Wed, 4 May 2022 10:49:31 +0200 Subject: [PATCH 01/57] Run code cleanup --- ...abaseServerMessengerNotificationHandler.cs | 79 +- .../Cache/DefaultRepositoryCachePolicy.cs | 398 +-- .../Cache/DistributedCacheBinder_Handlers.cs | 487 ++- .../Cache/DistributedCacheExtensions.cs | 451 +-- .../Cache/FullDataSetRepositoryCachePolicy.cs | 285 +- .../Cache/RepositoryCachePolicyBase.cs | 99 +- .../SingleItemsOnlyRepositoryCachePolicy.cs | 44 +- .../Configuration/JsonConfigManipulator.cs | 450 +-- .../Configuration/NCronTabParser.cs | 26 +- .../UmbracoBuilder.Collections.cs | 38 +- .../UmbracoBuilder.CoreServices.cs | 583 ++-- .../UmbracoBuilder.DistributedCache.cs | 166 +- .../UmbracoBuilder.Examine.cs | 89 +- .../UmbracoBuilder.FileSystems.cs | 85 +- .../UmbracoBuilder.Installer.cs | 50 +- .../UmbracoBuilder.MappingProfiles.cs | 59 +- .../UmbracoBuilder.Repositories.cs | 120 +- .../UmbracoBuilder.Services.cs | 157 +- .../UmbracoBuilder.TelemetryProviders.cs | 31 +- .../UmbracoBuilder.Uniques.cs | 369 ++- .../UmbracoBuilder.WebAssets.cs | 14 +- .../Deploy/IGridCellValueConnector.cs | 68 +- ...faultDistributedLockingMechanismFactory.cs | 17 +- .../Events/MigrationEventArgs.cs | 159 +- .../RelateOnTrashNotificationHandler.cs | 237 +- .../Examine/BaseValueSetBuilder.cs | 110 +- .../Examine/ContentIndexPopulator.cs | 252 +- .../Examine/ContentValueSetBuilder.cs | 214 +- .../Examine/ContentValueSetValidator.cs | 257 +- .../Examine/ExamineExtensions.cs | 139 +- .../Examine/ExamineIndexModel.cs | 27 +- .../Examine/ExamineIndexRebuilder.cs | 287 +- .../Examine/ExamineSearcherModel.cs | 18 +- .../Examine/ExamineUmbracoIndexingHandler.cs | 697 ++-- .../Examine/GenericIndexDiagnostics.cs | 89 +- .../Examine/IBackOfficeExamineSearcher.cs | 24 +- .../Examine/IContentValueSetBuilder.cs | 15 +- .../Examine/IContentValueSetValidator.cs | 43 +- .../Examine/IIndexDiagnostics.cs | 40 +- .../Examine/IIndexDiagnosticsFactory.cs | 16 +- .../Examine/IIndexPopulator.cs | 29 +- .../Examine/IIndexRebuilder.cs | 16 +- .../IPublishedContentValueSetBuilder.cs | 13 +- .../Examine/IUmbracoContentIndex.cs | 13 +- .../Examine/IUmbracoIndex.cs | 36 +- .../Examine/IUmbracoIndexConfig.cs | 14 +- .../Examine/IUmbracoMemberIndex.cs | 8 +- .../Examine/IUmbracoTreeSearcherFields.cs | 49 +- .../Examine/IValueSetBuilder.cs | 27 +- .../Examine/IndexDiagnosticsFactory.cs | 24 +- .../Examine/IndexPopulator.cs | 66 +- .../Examine/IndexTypes.cs | 52 +- .../Examine/MediaIndexPopulator.cs | 111 +- .../Examine/MediaValueSetBuilder.cs | 121 +- .../Examine/MemberIndexPopulator.cs | 57 +- .../Examine/MemberValueSetBuilder.cs | 71 +- .../Examine/MemberValueSetValidator.cs | 44 +- .../Examine/NoopBackOfficeExamineSearcher.cs | 18 +- .../Examine/PublishedContentIndexPopulator.cs | 31 +- .../Examine/RebuildOnStartupHandler.cs | 104 +- .../Examine/UmbracoExamineExtensions.cs | 222 +- .../Examine/UmbracoExamineFieldNames.cs | 42 +- .../UmbracoFieldDefinitionCollection.cs | 145 +- .../Examine/UmbracoIndexConfig.cs | 44 +- .../Examine/ValueSetValidator.cs | 158 +- .../Extensions/EmailMessageExtensions.cs | 178 +- .../InfrastuctureTypeLoaderExtensions.cs | 25 +- .../InstanceIdentifiableExtensions.cs | 20 +- .../MediaPicker3ConfigurationExtensions.cs | 59 +- .../Extensions/ObjectJsonExtensions.cs | 68 +- .../Extensions/ScopeExtensions.cs | 24 +- .../HealthChecks/MarkdownToHtmlConverter.cs | 45 +- .../HostedServices/BackgroundTaskQueue.cs | 59 +- .../HostedServices/ContentVersionCleanup.cs | 142 +- .../HostedServices/HealthCheckNotifier.cs | 203 +- .../HostedServices/IBackgroundTaskQueue.cs | 33 +- .../HostedServices/KeepAlive.cs | 154 +- .../HostedServices/LogScrubber.cs | 133 +- .../HostedServices/QueuedHostedService.cs | 77 +- .../RecurringHostedServiceBase.cs | 197 +- .../HostedServices/ReportSiteTask.cs | 117 +- .../HostedServices/ScheduledPublishing.cs | 206 +- .../InstructionProcessTask.cs | 90 +- .../ServerRegistration/TouchServerTask.cs | 137 +- .../HostedServices/TempFileCleanup.cs | 145 +- .../IPublishedContentQuery.cs | 225 +- .../IPublishedContentQueryAccessor.cs | 37 +- .../Install/FilePermissionHelper.cs | 399 +-- .../Install/InstallHelper.cs | 150 +- .../Install/InstallStepCollection.cs | 93 +- .../InstallSteps/CompleteInstallStep.cs | 40 +- .../InstallSteps/DatabaseConfigureStep.cs | 123 +- .../InstallSteps/DatabaseInstallStep.cs | 71 +- .../InstallSteps/DatabaseUpgradeStep.cs | 124 +- .../Install/InstallSteps/NewInstallStep.cs | 361 +- .../Install/PackageMigrationRunner.cs | 163 +- .../Install/UnattendedInstaller.cs | 204 +- .../Install/UnattendedUpgrader.cs | 159 +- .../Logging/MessageTemplates.cs | 73 +- .../Enrichers/HttpRequestIdEnricher.cs | 62 +- .../Enrichers/HttpRequestNumberEnricher.cs | 66 +- .../Enrichers/HttpSessionIdEnricher.cs | 60 +- .../Enrichers/Log4NetLevelMapperEnricher.cs | 82 +- .../Enrichers/ThreadAbortExceptionEnricher.cs | 142 +- .../Logging/Serilog/LoggerConfigExtensions.cs | 419 +-- .../Logging/Serilog/SerilogLogger.cs | 328 +- .../Serilog/UmbracoFileConfiguration.cs | 61 +- .../Logging/Viewer/CountingFilter.cs | 76 +- .../Logging/Viewer/ErrorCounterFilter.cs | 21 +- .../Logging/Viewer/ExpressionFilter.cs | 105 +- .../Logging/Viewer/ILogFilter.cs | 9 +- .../Logging/Viewer/ILogLevelLoader.cs | 23 +- .../Logging/Viewer/ILogViewer.cs | 116 +- .../Logging/Viewer/ILogViewerConfig.cs | 14 +- .../Logging/Viewer/LogLevelCounts.cs | 17 +- .../Logging/Viewer/LogLevelLoader.cs | 51 +- .../Logging/Viewer/LogMessage.cs | 59 +- .../Logging/Viewer/LogTemplate.cs | 11 +- .../Logging/Viewer/LogTimePeriod.cs | 19 +- .../Logging/Viewer/LogViewerConfig.cs | 71 +- .../Logging/Viewer/MessageTemplateFilter.cs | 38 +- .../Logging/Viewer/SavedLogSearch.cs | 13 +- .../Viewer/SerilogExpressionsFunctions.cs | 16 +- .../Logging/Viewer/SerilogJsonLogViewer.cs | 195 +- .../Viewer/SerilogLegacyNameResolver.cs | 54 +- .../Viewer/SerilogLogViewerSourceBase.cs | 235 +- .../Macros/MacroTagParser.cs | 336 +- .../Mail/EmailSender.cs | 242 +- .../Manifest/DashboardAccessRuleConverter.cs | 66 +- .../Manifest/DataEditorConverter.cs | 316 +- .../Manifest/ManifestParser.cs | 415 +-- .../Manifest/ValueValidatorConverter.cs | 39 +- .../Mapping/UmbracoMapper.cs | 828 ++--- .../Media/ImageSharpDimensionExtractor.cs | 93 +- .../Migrations/ExecutedMigrationPlan.cs | 23 +- .../Expressions/Alter/AlterBuilder.cs | 30 +- .../Expressions/AlterColumnExpression.cs | 32 +- .../AlterDefaultConstraintExpression.cs | 33 +- .../Alter/Expressions/AlterTableExpression.cs | 19 +- .../Expressions/Alter/IAlterBuilder.cs | 17 +- .../Alter/Table/AlterTableBuilder.cs | 363 +- .../Alter/Table/IAlterTableBuilder.cs | 25 +- .../Table/IAlterTableColumnOptionBuilder.cs | 9 +- ...bleColumnOptionForeignKeyCascadeBuilder.cs | 10 +- .../Table/IAlterTableColumnTypeBuilder.cs | 6 +- .../Expressions/Common/ExecutableBuilder.cs | 18 +- .../Expressions/CreateColumnExpression.cs | 33 +- .../Expressions/CreateForeignKeyExpression.cs | 30 +- .../Expressions/CreateIndexExpression.cs | 31 +- .../Common/IColumnOptionBuilder.cs | 58 +- .../Expressions/Common/IColumnTypeBuilder.cs | 65 +- .../Expressions/Common/IExecutableBuilder.cs | 15 +- .../Common/IForeignKeyCascadeBuilder.cs | 35 +- .../Create/Column/CreateColumnBuilder.cs | 290 +- .../Column/ICreateColumnOnTableBuilder.cs | 15 +- .../Column/ICreateColumnOptionBuilder.cs | 9 +- ...ateColumnOptionForeignKeyCascadeBuilder.cs | 8 +- .../Create/Column/ICreateColumnTypeBuilder.cs | 6 +- .../Constraint/CreateConstraintBuilder.cs | 49 +- .../ICreateConstraintColumnsBuilder.cs | 23 +- .../ICreateConstraintOnTableBuilder.cs | 15 +- .../Expressions/Create/CreateBuilder.cs | 222 +- .../Expressions/CreateConstraintExpression.cs | 56 +- .../Expressions/CreateTableExpression.cs | 32 +- .../ForeignKey/CreateForeignKeyBuilder.cs | 136 +- .../ICreateForeignKeyCascadeBuilder.cs | 13 +- .../ICreateForeignKeyForeignColumnBuilder.cs | 11 +- .../ICreateForeignKeyFromTableBuilder.cs | 9 +- .../ICreateForeignKeyPrimaryColumnBuilder.cs | 11 +- .../ICreateForeignKeyToTableBuilder.cs | 9 +- .../Expressions/Create/ICreateBuilder.cs | 172 +- .../Create/Index/CreateIndexBuilder.cs | 135 +- .../Index/ICreateIndexColumnOptionsBuilder.cs | 13 +- .../Index/ICreateIndexForTableBuilder.cs | 9 +- .../Index/ICreateIndexOnColumnBuilder.cs | 23 +- .../Index/ICreateIndexOptionsBuilder.cs | 13 +- .../CreateKeysAndIndexesBuilder.cs | 83 +- .../Create/Table/CreateTableBuilder.cs | 371 +-- .../Create/Table/CreateTableOfDtoBuilder.cs | 60 +- .../Table/ICreateTableColumnAsTypeBuilder.cs | 6 +- .../Table/ICreateTableColumnOptionBuilder.cs | 10 +- ...bleColumnOptionForeignKeyCascadeBuilder.cs | 10 +- .../Table/ICreateTableWithColumnBuilder.cs | 9 +- .../Delete/Column/DeleteColumnBuilder.cs | 36 +- .../Delete/Column/IDeleteColumnBuilder.cs | 25 +- .../Constraint/DeleteConstraintBuilder.cs | 24 +- .../Constraint/IDeleteConstraintBuilder.cs | 17 +- .../Delete/Data/DeleteDataBuilder.cs | 75 +- .../Delete/Data/IDeleteDataBuilder.cs | 33 +- .../DeleteDefaultConstraintBuilder.cs | 55 +- ...IDeleteDefaultConstraintOnColumnBuilder.cs | 17 +- .../IDeleteDefaultConstraintOnTableBuilder.cs | 17 +- .../Expressions/Delete/DeleteBuilder.cs | 173 +- .../Expressions/DeleteColumnExpression.cs | 38 +- .../Expressions/DeleteConstraintExpression.cs | 27 +- .../Expressions/DeleteDataExpression.cs | 50 +- .../DeleteDefaultConstraintExpression.cs | 36 +- .../Expressions/DeleteForeignKeyExpression.cs | 43 +- .../Expressions/DeleteIndexExpression.cs | 35 +- .../Expressions/DeleteTableExpression.cs | 22 +- .../ForeignKey/DeleteForeignKeyBuilder.cs | 107 +- .../IDeleteForeignKeyForeignColumnBuilder.cs | 25 +- .../IDeleteForeignKeyFromTableBuilder.cs | 17 +- .../IDeleteForeignKeyOnTableBuilder.cs | 17 +- .../IDeleteForeignKeyPrimaryColumnBuilder.cs | 25 +- .../IDeleteForeignKeyToTableBuilder.cs | 17 +- .../Expressions/Delete/IDeleteBuilder.cs | 109 +- .../Delete/Index/DeleteIndexBuilder.cs | 28 +- .../Index/IDeleteIndexForTableBuilder.cs | 17 +- .../DeleteKeysAndIndexesBuilder.cs | 150 +- .../Expressions/Execute/ExecuteBuilder.cs | 56 +- .../ExecuteSqlStatementExpression.cs | 26 +- .../Expressions/Execute/IExecuteBuilder.cs | 27 +- .../Expressions/ExpressionBuilderBase.cs | 30 +- .../ExpressionBuilderBaseOfNext.cs | 395 ++- .../Migrations/Expressions/IFluentBuilder.cs | 6 +- .../Expressions/InsertDataExpression.cs | 94 +- .../Expressions/Insert/IInsertBuilder.cs | 17 +- .../Expressions/Insert/IInsertIntoBuilder.cs | 25 +- .../Expressions/Insert/InsertBuilder.cs | 30 +- .../Expressions/Insert/InsertIntoBuilder.cs | 66 +- .../Rename/Column/IRenameColumnBuilder.cs | 17 +- .../Rename/Column/IRenameColumnToBuilder.cs | 17 +- .../Rename/Column/RenameColumnBuilder.cs | 40 +- .../Expressions/RenameColumnExpression.cs | 25 +- .../Expressions/RenameTableExpression.cs | 43 +- .../Expressions/Rename/IRenameBuilder.cs | 25 +- .../Expressions/Rename/RenameBuilder.cs | 36 +- .../Rename/Table/IRenameTableBuilder.cs | 17 +- .../Rename/Table/RenameTableBuilder.cs | 28 +- .../Expressions/UpdateDataExpression.cs | 50 +- .../Expressions/Update/IUpdateBuilder.cs | 17 +- .../Expressions/Update/IUpdateTableBuilder.cs | 17 +- .../Expressions/Update/IUpdateWhereBuilder.cs | 25 +- .../Expressions/Update/UpdateBuilder.cs | 24 +- .../Expressions/Update/UpdateDataBuilder.cs | 72 +- .../Migrations/IMigrationBuilder.cs | 9 +- .../Migrations/IMigrationContext.cs | 79 +- .../Migrations/IMigrationExpression.cs | 15 +- .../Migrations/IMigrationPlanExecutor.cs | 10 +- .../IncompleteMigrationExpressionException.cs | 87 +- .../Migrations/Install/DatabaseBuilder.cs | 609 ++-- .../Migrations/Install/DatabaseDataCreator.cs | 2948 +++++++++++----- .../Install/DatabaseSchemaCreator.cs | 911 ++--- .../Install/DatabaseSchemaCreatorFactory.cs | 75 +- .../Install/DatabaseSchemaResult.cs | 165 +- .../Migrations/MergeBuilder.cs | 143 +- .../Migrations/MigrationBase.cs | 210 +- .../Migrations/MigrationBuilder.cs | 21 +- .../Migrations/MigrationContext.cs | 88 +- .../Migrations/MigrationExpressionBase.cs | 237 +- .../Migrations/MigrationPlan.cs | 684 ++-- .../Migrations/MigrationPlanExecutor.cs | 178 +- .../Migrations/NoopMigration.cs | 17 +- .../DatabaseSchemaCreatedNotification.cs | 12 +- .../DatabaseSchemaCreatingNotification.cs | 9 +- .../MigrationPlansExecutedNotification.cs | 22 +- .../PostMigrations/ClearCsrfCookies.cs | 29 +- .../DeleteLogViewerQueryFile.cs | 47 +- .../IPublishedSnapshotRebuilder.cs | 29 +- .../PublishedSnapshotRebuilder.cs | 40 +- .../RebuildPublishedSnapshot.cs | 29 +- .../Upgrade/Common/CreateKeysAndIndexes.cs | 29 +- .../Upgrade/Common/DeleteKeysAndIndexes.cs | 99 +- .../Migrations/Upgrade/UmbracoPlan.cs | 499 ++- .../Migrations/Upgrade/Upgrader.cs | 120 +- .../V_10_0_0/AddMemberPropertiesAsColumns.cs | 112 +- .../Upgrade/V_8_0_0/AddContentNuTable.cs | 23 +- .../V_8_0_0/AddContentTypeIsElementColumn.cs | 15 +- .../Upgrade/V_8_0_0/AddLockObjects.cs | 63 +- .../Upgrade/V_8_0_0/AddLogTableColumns.cs | 25 +- .../V_8_0_0/AddPackagesSectionAccess.cs | 28 +- .../Upgrade/V_8_0_0/AddTypedLabels.cs | 244 +- .../Upgrade/V_8_0_0/AddVariationTables1A.cs | 69 +- .../Upgrade/V_8_0_0/AddVariationTables2.cs | 20 +- .../V_8_0_0/ContentVariationMigration.cs | 102 +- .../ConvertRelatedLinksToMultiUrlPicker.cs | 211 +- .../Upgrade/V_8_0_0/DataTypeMigration.cs | 220 +- .../ContentPickerPreValueMigrator.cs | 29 +- .../DataTypes/DecimalPreValueMigrator.cs | 25 +- .../DataTypes/DefaultPreValueMigrator.cs | 61 +- .../DropDownFlexiblePreValueMigrator.cs | 41 +- .../V_8_0_0/DataTypes/IPreValueMigrator.cs | 57 +- .../DataTypes/ListViewPreValueMigrator.cs | 35 +- .../MarkdownEditorPreValueMigrator.cs | 23 +- .../DataTypes/MediaPickerPreValueMigrator.cs | 52 +- .../NestedContentPreValueMigrator.cs | 45 +- .../Upgrade/V_8_0_0/DataTypes/PreValueDto.cs | 26 +- .../V_8_0_0/DataTypes/PreValueMigratorBase.cs | 26 +- .../DataTypes/PreValueMigratorCollection.cs | 33 +- .../PreValueMigratorCollectionBuilder.cs | 10 +- .../DataTypes/RenamingPreValueMigrator.cs | 32 +- .../DataTypes/RichTextPreValueMigrator.cs | 26 +- .../UmbracoSliderPreValueMigrator.cs | 35 +- .../DataTypes/ValueListPreValueMigrator.cs | 42 +- .../DropDownPropertyEditorsMigration.cs | 199 +- .../Upgrade/V_8_0_0/DropMigrationsTable.cs | 18 +- .../Upgrade/V_8_0_0/DropPreValueTable.cs | 18 +- .../Upgrade/V_8_0_0/DropTaskTables.cs | 25 +- .../V_8_0_0/DropTemplateDesignColumn.cs | 18 +- .../Upgrade/V_8_0_0/DropXmlTables.cs | 25 +- .../Upgrade/V_8_0_0/FallbackLanguage.cs | 35 +- .../V_8_0_0/FixLanguageIsoCodeLength.cs | 31 +- .../Upgrade/V_8_0_0/LanguageColumns.cs | 23 +- .../Upgrade/V_8_0_0/MakeRedirectUrlVariant.cs | 17 +- .../Upgrade/V_8_0_0/MakeTagsVariant.cs | 17 +- .../MergeDateAndDateTimePropertyEditor.cs | 132 +- .../V_8_0_0/Models/ContentTypeDto80.cs | 111 +- .../V_8_0_0/Models/PropertyDataDto80.cs | 237 +- .../V_8_0_0/Models/PropertyTypeDto80.cs | 116 +- .../V_8_0_0/PropertyEditorsMigration.cs | 85 +- .../V_8_0_0/PropertyEditorsMigrationBase.cs | 154 +- ...adioAndCheckboxPropertyEditorsMigration.cs | 178 +- .../Upgrade/V_8_0_0/RefactorMacroColumns.cs | 70 +- .../Upgrade/V_8_0_0/RefactorVariantsModel.cs | 129 +- ...meLabelAndRichTextPropertyEditorAliases.cs | 54 +- .../V_8_0_0/RenameMediaVersionTable.cs | 58 +- .../V_8_0_0/RenameUmbracoDomainsTable.cs | 19 +- .../Migrations/Upgrade/V_8_0_0/SuperZero.cs | 43 +- .../V_8_0_0/TablesForScheduledPublishing.cs | 95 +- .../Upgrade/V_8_0_0/TagsMigration.cs | 29 +- .../Upgrade/V_8_0_0/TagsMigrationFix.cs | 22 +- .../V_8_0_0/UpdateDefaultMandatoryLanguage.cs | 72 +- .../V_8_0_0/UpdatePickerIntegerValuesToUdi.cs | 173 +- .../Upgrade/V_8_0_0/UserForeignKeys.cs | 56 +- .../Upgrade/V_8_0_0/VariantsMigration.cs | 562 ++-- .../V_8_0_1/ChangeNuCacheJsonFormat.cs | 18 +- .../AddPropertyTypeLabelOnTopColumn.cs | 24 +- .../V_8_15_0/AddCmsContentNuByteColumn.cs | 59 +- .../V_8_15_0/UpdateCmsPropertyGroupIdSeed.cs | 17 +- .../V_8_15_0/UpgradedIncludeIndexes.cs | 97 +- .../V_8_17_0/AddPropertyTypeGroupColumns.cs | 85 +- ...nvertTinyMceAndGridMediaUrlsToLocalLink.cs | 180 +- .../Upgrade/V_8_1_0/FixContentNuCascade.cs | 20 +- .../V_8_1_0/RenameUserLoginDtoDateIndex.cs | 52 +- .../Upgrade/V_8_6_0/AddMainDomLock.cs | 21 +- .../Upgrade/V_8_6_0/AddNewRelationTypes.cs | 54 +- ...AddPropertyTypeValidationMessageColumns.cs | 26 +- .../V_8_6_0/MissingContentVersionsIndexes.cs | 44 +- .../V_8_6_0/UpdateRelationTypeTable.cs | 57 +- .../Upgrade/V_8_7_0/MissingDictionaryIndex.cs | 44 +- .../V_8_9_0/ExternalLoginTableUserData.cs | 28 +- .../V_9_0_0/AddPasswordConfigToMemberTable.cs | 30 +- .../V_9_0_0/DictionaryTablesIndexes.cs | 190 +- .../V_9_0_0/ExternalLoginTableIndexes.cs | 117 +- .../V_9_0_0/ExternalLoginTableIndexesFixup.cs | 92 +- .../V_9_0_0/ExternalLoginTokenTable.cs | 115 +- .../Upgrade/V_9_0_0/MemberTableColumns.cs | 30 +- .../MigrateLogViewerQueriesFromFileToDb.cs | 165 +- .../Upgrade/V_9_0_0/UmbracoServerColumn.cs | 26 +- .../AddContentVersionCleanupFeature.cs | 37 +- .../AddDefaultForNotificationsToggle.cs | 26 +- .../Upgrade/V_9_2_0/AddUserGroup2NodeTable.cs | 37 +- .../Upgrade/V_9_3_0/AddTwoFactorLoginTable.cs | 28 +- .../Upgrade/V_9_3_0/MovePackageXMLToDb.cs | 98 +- .../UpdateExternalLoginToUseKeyInsteadOfId.cs | 101 +- .../V_9_4_0/AddScheduledPublishingLock.cs | 19 +- ...UpdateRelationTypesToHandleDependencies.cs | 41 +- .../Models/Blocks/BlockEditorData.cs | 71 +- .../Models/Blocks/BlockEditorDataConverter.cs | 93 +- .../Models/Blocks/BlockItemData.cs | 101 +- .../Blocks/BlockListEditorDataConverter.cs | 29 +- .../Models/Blocks/BlockListLayoutItem.cs | 25 +- .../Models/Blocks/BlockValue.cs | 19 +- .../Models/GridValue.cs | 105 +- .../Models/Mapping/EntityMapDefinition.cs | 479 +-- .../Models/MediaWithCrops.cs | 125 +- .../Models/PathValidationExtensions.cs | 183 +- .../ModelsBuilder/ApiVersion.cs | 46 +- .../AutoModelsNotificationHandler.cs | 191 +- .../ModelsBuilder/Building/Builder.cs | 369 ++- .../ModelsBuilder/Building/ModelsGenerator.cs | 85 +- .../ModelsBuilder/Building/PropertyModel.cs | 109 +- .../ModelsBuilder/Building/TextBuilder.cs | 979 +++--- .../Building/TextHeaderWriter.cs | 37 +- .../ModelsBuilder/Building/TypeModel.cs | 326 +- .../ModelsBuilder/Building/TypeModelHasher.cs | 63 +- .../ImplementPropertyTypeAttribute.cs | 21 +- .../ModelsBuilderAssemblyAttribute.cs | 31 +- .../ModelsBuilder/ModelsGenerationError.cs | 119 +- .../ModelsBuilder/OutOfDateModelsStatus.cs | 133 +- .../PublishedElementExtensions.cs | 68 +- .../ModelsBuilder/PublishedModelUtility.cs | 110 +- .../ModelsBuilder/RoslynCompiler.cs | 114 +- .../ModelsBuilder/TypeExtensions.cs | 33 +- .../ModelsBuilder/UmbracoServices.cs | 341 +- .../AutomaticPackageMigrationPlan.cs | 62 +- .../Packaging/IImportPackageBuilder.cs | 16 +- .../Packaging/ImportPackageBuilder.cs | 79 +- .../ImportPackageBuilderExpression.cs | 214 +- .../Packaging/PackageDataInstallation.cs | 2952 +++++++++-------- .../Packaging/PackageInstallation.cs | 127 +- .../Packaging/PackageMigrationBase.cs | 123 +- .../Packaging/PackageMigrationPlan.cs | 78 +- .../PackageMigrationPlanCollection.cs | 17 +- .../PackageMigrationPlanCollectionBuilder.cs | 10 +- .../Packaging/PendingPackageMigrations.cs | 88 +- ...onnectionStringDatabaseProviderMetadata.cs | 3 +- .../ConstraintAttribute.cs | 35 +- .../ForeignKeyAttribute.cs | 63 +- .../DatabaseAnnotations/IndexAttribute.cs | 58 +- .../DatabaseAnnotations/IndexTypes.cs | 19 +- .../DatabaseAnnotations/LengthAttribute.cs | 28 +- .../NullSettingAttribute.cs | 27 +- .../DatabaseAnnotations/NullSettings.cs | 17 +- .../PrimaryKeyColumnAttribute.cs | 97 +- .../ReferencesAttribute.cs | 26 +- .../DatabaseAnnotations/SpecialDbType.cs | 58 +- .../SpecialDbTypeAttribute.cs | 37 +- .../DatabaseAnnotations/SpecialDbTypes.cs | 19 +- .../Persistence/DatabaseDebugHelper.cs | 6 +- .../ColumnDefinition.cs | 62 +- .../ConstraintDefinition.cs | 31 +- .../ConstraintType.cs | 13 +- .../DbIndexDefinition.cs | 33 +- .../DefinitionFactory.cs | 298 +- .../DeletionDataDefinition.cs | 8 +- .../ForeignKeyDefinition.cs | 42 +- .../IndexColumnDefinition.cs | 11 +- .../IndexDefinition.cs | 24 +- .../InsertionDataDefinition.cs | 8 +- .../ModificationType.cs | 21 +- .../DatabaseModelDefinitions/SystemMethods.cs | 16 +- .../TableDefinition.cs | 27 +- .../Persistence/DbCommandExtensions.cs | 42 +- .../Persistence/DbConnectionExtensions.cs | 99 +- .../Persistence/DbProviderFactoryCreator.cs | 139 +- .../Persistence/Dtos/AccessDto.cs | 74 +- .../Persistence/Dtos/AccessRuleDto.cs | 50 +- .../Persistence/Dtos/AuditEntryDto.cs | 95 +- .../Persistence/Dtos/AxisDefintionDto.cs | 16 +- .../Persistence/Dtos/CacheInstructionDto.cs | 55 +- .../Persistence/Dtos/ConsentDto.cs | 65 +- .../Persistence/Dtos/ContentDto.cs | 48 +- .../Persistence/Dtos/ContentNuDto.cs | 56 +- .../Persistence/Dtos/ContentScheduleDto.cs | 45 +- .../Dtos/ContentType2ContentTypeDto.cs | 27 +- .../Dtos/ContentTypeAllowedContentTypeDto.cs | 42 +- .../Persistence/Dtos/ContentTypeDto.cs | 112 +- .../Dtos/ContentTypeTemplateDto.cs | 43 +- .../Dtos/ContentVersionCleanupPolicyDto.cs | 40 +- .../Dtos/ContentVersionCultureVariationDto.cs | 77 +- .../Persistence/Dtos/ContentVersionDto.cs | 102 +- .../Dtos/CreatedPackageSchemaDto.cs | 53 +- .../Persistence/Dtos/DataTypeDto.cs | 43 +- .../Persistence/Dtos/DictionaryDto.cs | 52 +- .../Dtos/DocumentCultureVariationDto.cs | 88 +- .../Persistence/Dtos/DocumentDto.cs | 104 +- .../Dtos/DocumentPublishedReadOnlyDto.cs | 32 +- .../Persistence/Dtos/DocumentVersionDto.cs | 41 +- .../Persistence/Dtos/DomainDto.cs | 45 +- .../Persistence/Dtos/ExternalLoginDto.cs | 102 +- .../Persistence/Dtos/ExternalLoginTokenDto.cs | 69 +- .../Persistence/Dtos/KeyValueDto.cs | 37 +- .../Persistence/Dtos/LanguageDto.cs | 110 +- .../Persistence/Dtos/LanguageTextDto.cs | 39 +- .../Persistence/Dtos/LockDto.cs | 34 +- .../Persistence/Dtos/LogDto.cs | 107 +- .../Persistence/Dtos/LogViewerQueryDto.cs | 27 +- .../Persistence/Dtos/MacroDto.cs | 110 +- .../Persistence/Dtos/MacroPropertyDto.cs | 67 +- .../Persistence/Dtos/MediaDto.cs | 28 +- .../Persistence/Dtos/MediaVersionDto.cs | 38 +- .../Persistence/Dtos/Member2MemberGroupDto.cs | 28 +- .../Persistence/Dtos/MemberDto.cs | 159 +- .../Persistence/Dtos/MemberPropertyTypeDto.cs | 47 +- .../Persistence/Dtos/NodeDto.cs | 106 +- .../Persistence/Dtos/PropertyDataDto.cs | 224 +- .../Persistence/Dtos/PropertyTypeCommonDto.cs | 20 +- .../Persistence/Dtos/PropertyTypeDto.cs | 159 +- .../Persistence/Dtos/PropertyTypeGroupDto.cs | 77 +- .../Dtos/PropertyTypeGroupReadOnlyDto.cs | 32 +- .../Dtos/PropertyTypeReadOnlyDto.cs | 78 +- .../Persistence/Dtos/RedirectUrlDto.cs | 96 +- .../Persistence/Dtos/RelationDto.cs | 78 +- .../Persistence/Dtos/RelationTypeDto.cs | 86 +- .../Persistence/Dtos/ServerRegistrationDto.cs | 63 +- .../Persistence/Dtos/TagDto.cs | 65 +- .../Persistence/Dtos/TagRelationshipDto.cs | 37 +- .../Persistence/Dtos/TemplateDto.cs | 40 +- .../Persistence/Dtos/TwoFactorLoginDto.cs | 47 +- .../Persistence/Dtos/User2NodeNotifyDto.cs | 36 +- .../Persistence/Dtos/User2UserGroupDto.cs | 26 +- .../Persistence/Dtos/UserDto.cs | 232 +- .../Persistence/Dtos/UserGroup2AppDto.cs | 24 +- .../Persistence/Dtos/UserGroup2NodeDto.cs | 29 +- .../Dtos/UserGroup2NodePermissionDto.cs | 32 +- .../Persistence/Dtos/UserGroupDto.cs | 107 +- .../Persistence/Dtos/UserLoginDto.cs | 109 +- .../Persistence/Dtos/UserNotificationDto.cs | 20 +- .../Persistence/Dtos/UserStartNodeDto.cs | 101 +- .../Factories/AuditEntryFactory.cs | 79 +- .../Factories/CacheInstructionFactory.cs | 34 +- .../Persistence/Factories/ConsentFactory.cs | 97 +- .../Factories/ContentBaseFactory.cs | 520 ++- .../Factories/ContentTypeFactory.cs | 293 +- .../Persistence/Factories/DataTypeFactory.cs | 134 +- .../Factories/DictionaryItemFactory.cs | 98 +- .../Factories/DictionaryTranslationFactory.cs | 63 +- .../Factories/ExternalLoginFactory.cs | 112 +- .../Persistence/Factories/LanguageFactory.cs | 79 +- .../Persistence/Factories/MacroFactory.cs | 115 +- .../Factories/MemberGroupFactory.cs | 98 +- .../Persistence/Factories/PropertyFactory.cs | 302 +- .../Factories/PropertyGroupFactory.cs | 267 +- .../Factories/PublicAccessEntryFactory.cs | 73 +- .../Persistence/Factories/RelationFactory.cs | 94 +- .../Factories/RelationTypeFactory.cs | 89 +- .../Factories/ServerRegistrationFactory.cs | 48 +- .../Persistence/Factories/TagFactory.cs | 34 +- .../Persistence/Factories/TemplateFactory.cs | 116 +- .../Persistence/Factories/UserFactory.cs | 181 +- .../Persistence/Factories/UserGroupFactory.cs | 111 +- .../ITransientErrorDetectionStrategy.cs | 22 +- .../FaultHandling/RetryDbConnection.cs | 316 +- .../RetryLimitExceededException.cs | 96 +- .../Persistence/FaultHandling/RetryPolicy.cs | 413 +-- .../FaultHandling/RetryPolicyFactory.cs | 98 +- .../FaultHandling/RetryStrategy.cs | 217 +- .../FaultHandling/RetryingEventArgs.cs | 61 +- .../Strategies/ExponentialBackoff.cs | 173 +- .../FaultHandling/Strategies/FixedInterval.cs | 152 +- .../FaultHandling/Strategies/Incremental.cs | 148 +- ...tworkConnectivityErrorDetectionStrategy.cs | 42 +- ...SqlAzureTransientErrorDetectionStrategy.cs | 275 +- .../FaultHandling/ThrottlingCondition.cs | 567 ++-- .../Persistence/IBulkSqlInsertProvider.cs | 11 +- .../Persistence/IDatabaseCreator.cs | 11 +- .../Persistence/IDatabaseProviderMetadata.cs | 29 +- .../Persistence/IDbProviderFactoryCreator.cs | 24 +- .../IProviderSpecificInterceptor.cs | 2 +- .../IProviderSpecificMapperFactory.cs | 11 +- .../Persistence/IScalarMapper.cs | 5 +- .../Persistence/ISqlContext.cs | 89 +- .../Persistence/IUmbracoDatabase.cs | 50 +- .../Persistence/IUmbracoDatabaseFactory.cs | 133 +- .../Persistence/LocalDb.cs | 1558 +++++---- .../Persistence/Mappers/AccessMapper.cs | 33 +- .../Persistence/Mappers/AuditEntryMapper.cs | 49 +- .../Persistence/Mappers/AuditItemMapper.cs | 35 +- .../Persistence/Mappers/BaseMapper.cs | 133 +- .../Persistence/Mappers/ConsentMapper.cs | 45 +- .../Persistence/Mappers/ContentMapper.cs | 77 +- .../Persistence/Mappers/ContentTypeMapper.cs | 65 +- .../Persistence/Mappers/DataTypeMapper.cs | 55 +- .../Persistence/Mappers/DictionaryMapper.cs | 39 +- .../Mappers/DictionaryTranslationMapper.cs | 43 +- .../Persistence/Mappers/DomainMapper.cs | 31 +- .../Mappers/ExternalLoginMapper.cs | 38 +- .../Mappers/ExternalLoginTokenMapper.cs | 40 +- .../Persistence/Mappers/IMapperCollection.cs | 14 +- .../Persistence/Mappers/KeyValueMapper.cs | 27 +- .../Persistence/Mappers/LanguageMapper.cs | 37 +- .../Mappers/LogViewerQueryMapper.cs | 29 +- .../Persistence/Mappers/MacroMapper.cs | 41 +- .../Persistence/Mappers/MapperCollection.cs | 64 +- .../Mappers/MapperCollectionBuilder.cs | 103 +- .../Mappers/MapperConfigurationStore.cs | 9 +- .../Persistence/Mappers/MapperForAttribute.cs | 21 +- .../Persistence/Mappers/MediaMapper.cs | 59 +- .../Persistence/Mappers/MediaTypeMapper.cs | 65 +- .../Persistence/Mappers/MemberGroupMapper.cs | 33 +- .../Persistence/Mappers/MemberMapper.cs | 92 +- .../Persistence/Mappers/MemberTypeMapper.cs | 65 +- .../Persistence/Mappers/NullableDateMapper.cs | 40 +- .../Mappers/PropertyGroupMapper.cs | 43 +- .../Persistence/Mappers/PropertyMapper.cs | 25 +- .../Persistence/Mappers/PropertyTypeMapper.cs | 60 +- .../Persistence/Mappers/RelationMapper.cs | 43 +- .../Persistence/Mappers/RelationTypeMapper.cs | 48 +- .../Mappers/ServerRegistrationMapper.cs | 42 +- .../Mappers/SimpleContentTypeMapper.cs | 54 +- .../Persistence/Mappers/TagMapper.cs | 39 +- .../Persistence/Mappers/TemplateMapper.cs | 39 +- .../Mappers/UmbracoEntityMapper.cs | 41 +- .../Persistence/Mappers/UserGroupMapper.cs | 43 +- .../Persistence/Mappers/UserMapper.cs | 55 +- .../Persistence/Mappers/UserSectionMapper.cs | 50 +- .../NPocoDatabaseExtensions-Bulk.cs | 180 +- .../Persistence/NPocoDatabaseExtensions.cs | 512 +-- .../NPocoDatabaseTypeExtensions.cs | 27 +- .../Persistence/NPocoMapperCollection.cs | 11 +- .../NPocoMapperCollectionBuilder.cs | 11 +- .../Persistence/NPocoSqlExtensions.cs | 2100 ++++++------ .../Persistence/Querying/CachedExpression.cs | 70 +- .../Querying/ExpressionVisitorBase.cs | 1347 ++++---- .../Querying/ModelToSqlExpressionVisitor.cs | 215 +- .../Querying/PocoToSqlExpressionVisitor.cs | 453 +-- .../Persistence/Querying/Query.cs | 141 +- .../Persistence/Querying/QueryExtensions.cs | 37 +- .../Persistence/Querying/SqlTranslator.cs | 40 +- .../Persistence/RecordPersistenceType.cs | 13 +- .../Repositories/IEntityRepositoryExtended.cs | 50 +- .../Implement/AuditEntryRepository.cs | 179 +- .../Repositories/Implement/AuditRepository.cs | 276 +- .../Implement/CacheInstructionRepository.cs | 104 +- .../Implement/ConsentRepository.cs | 123 +- .../Implement/ContentRepositoryBase.cs | 1805 +++++----- .../Implement/ContentTypeCommonRepository.cs | 632 ++-- .../Implement/ContentTypeRepository.cs | 498 +-- .../Implement/ContentTypeRepositoryBase.cs | 2454 +++++++------- .../CreatedPackageSchemaRepository.cs | 1100 +++--- .../Implement/DataTypeContainerRepository.cs | 12 +- .../Implement/DataTypeRepository.cs | 523 ++- .../Implement/DictionaryRepository.cs | 558 ++-- .../Implement/DocumentBlueprintRepository.cs | 72 +- .../Implement/DocumentRepository.cs | 2752 +++++++-------- .../DocumentTypeContainerRepository.cs | 12 +- .../Implement/DocumentVersionRepository.cs | 263 +- .../Implement/DomainRepository.cs | 290 +- .../Implement/EntityContainerRepository.cs | 522 ++- .../Implement/EntityRepository.cs | 1147 ++++--- .../Implement/EntityRepositoryBase.cs | 385 ++- .../Implement/ExternalLoginRepository.cs | 434 +-- .../Repositories/Implement/FileRepository.cs | 364 +- .../Implement/IdKeyMapRepository.cs | 47 +- .../Implement/KeyValueRepository.cs | 160 +- .../Implement/LanguageRepository.cs | 492 +-- .../Implement/LanguageRepositoryExtensions.cs | 13 +- .../Implement/LogViewerQueryRepository.cs | 174 +- .../Repositories/Implement/MacroRepository.cs | 370 +-- .../Repositories/Implement/MediaRepository.cs | 865 +++-- .../Implement/MediaTypeContainerRepository.cs | 12 +- .../Implement/MediaTypeRepository.cs | 189 +- .../Implement/MemberGroupRepository.cs | 484 ++- .../Implement/MemberRepository.cs | 1281 ++++--- .../Implement/MemberTypeRepository.cs | 370 ++- .../Implement/NodeCountRepository.cs | 10 +- .../Implement/NotificationsRepository.cs | 175 +- .../Implement/PartialViewMacroRepository.cs | 14 +- .../Implement/PartialViewRepository.cs | 198 +- .../Implement/PermissionRepository.cs | 549 ++- .../Implement/PublicAccessRepository.cs | 213 +- .../Repositories/Implement/QueryType.cs | 41 +- .../Implement/RedirectUrlRepository.cs | 355 +- .../Implement/RelationRepository.cs | 671 ++-- .../Implement/RelationTypeRepository.cs | 204 +- .../Repositories/Implement/RepositoryBase.cs | 112 +- .../Implement/ScriptRepository.cs | 141 +- .../Implement/ServerRegistrationRepository.cs | 210 +- .../Repositories/Implement/SimilarNodeName.cs | 346 +- .../Implement/SimpleGetRepository.cs | 126 +- .../Implement/StylesheetRepository.cs | 162 +- .../Repositories/Implement/TagRepository.cs | 824 +++-- .../Implement/TemplateRepository.cs | 937 +++--- .../Implement/TrackedReferencesRepository.cs | 342 +- .../Repositories/Implement/TupleExtensions.cs | 17 +- .../Implement/TwoFactorLoginRepository.cs | 193 +- .../Implement/UserGroupRepository.cs | 699 ++-- .../Repositories/Implement/UserRepository.cs | 1444 ++++---- .../Persistence/ScalarMapper.cs | 10 +- .../Persistence/SqlContext.cs | 92 +- .../Persistence/SqlContextExtensions.cs | 193 +- .../SqlServerDbProviderFactoryCreator.cs | 1 + .../Persistence/SqlSyntax/ColumnInfo.cs | 68 +- .../Persistence/SqlSyntax/DbTypes.cs | 21 +- .../Persistence/SqlSyntax/DbTypesFactory.cs | 27 +- .../SqlSyntax/ISqlSyntaxProvider.cs | 311 +- .../SqlSyntax/SqlServerVersionName.cs | 37 +- .../SqlSyntax/SqlSyntaxProviderBase.cs | 944 +++--- .../SqlSyntax/SqlSyntaxProviderExtensions.cs | 92 +- .../Persistence/SqlSyntaxExtensions.cs | 51 +- .../Persistence/SqlTemplate.cs | 186 +- .../Persistence/SqlTemplates.cs | 40 +- .../Persistence/UmbracoDatabase.cs | 458 +-- .../Persistence/UmbracoDatabaseExtensions.cs | 111 +- .../Persistence/UmbracoDatabaseFactory.cs | 442 ++- .../Persistence/UmbracoPocoDataBuilder.cs | 54 +- .../BlockEditorPropertyEditor.cs | 684 ++-- .../BlockEditorPropertyHandler.cs | 309 +- .../BlockListConfigurationEditor.cs | 14 +- .../BlockListPropertyEditor.cs | 88 +- .../CheckBoxListPropertyEditor.cs | 87 +- .../ColorPickerConfigurationEditor.cs | 264 +- .../ColorPickerPropertyEditor.cs | 72 +- .../PropertyEditors/ComplexEditorValidator.cs | 152 +- ...ropertyEditorContentNotificationHandler.cs | 73 +- .../PropertyEditors/DateTimePropertyEditor.cs | 78 +- .../DropDownFlexibleConfigurationEditor.cs | 114 +- .../DropDownFlexiblePropertyEditor.cs | 81 +- .../EmailAddressPropertyEditor.cs | 63 +- .../FileUploadPropertyEditor.cs | 358 +- .../FileUploadPropertyValueEditor.cs | 215 +- .../PropertyEditors/GridConfiguration.cs | 41 +- .../GridConfigurationEditor.cs | 84 +- .../PropertyEditors/GridPropertyEditor.cs | 478 +-- .../GridPropertyIndexValueFactory.cs | 119 +- .../ImageCropperConfiguration.cs | 81 +- .../ImageCropperConfigurationEditor.cs | 36 +- .../ImageCropperPropertyEditor.cs | 529 +-- .../ImageCropperPropertyValueEditor.cs | 362 +- .../PropertyEditors/ListViewPropertyEditor.cs | 75 +- .../MediaPicker3PropertyEditor.cs | 335 +- .../MediaPickerPropertyEditor.cs | 119 +- .../MultiNodeTreePickerPropertyEditor.cs | 103 +- .../MultiUrlPickerPropertyEditor.cs | 69 +- .../MultiUrlPickerValueEditor.cs | 337 +- .../MultipleTextStringConfigurationEditor.cs | 90 +- .../MultipleTextStringPropertyEditor.cs | 252 +- .../PropertyEditors/MultipleValueEditor.cs | 107 +- .../NestedContentPropertyEditor.cs | 773 +++-- .../NestedContentPropertyHandler.cs | 103 +- .../RadioButtonsPropertyEditor.cs | 97 +- .../RichTextEditorPastedImages.cs | 266 +- .../PropertyEditors/RichTextPropertyEditor.cs | 459 +-- .../PropertyEditors/SliderPropertyEditor.cs | 78 +- .../PropertyEditors/TagsPropertyEditor.cs | 194 +- .../PropertyEditors/TextAreaPropertyEditor.cs | 81 +- .../PropertyEditors/TextboxPropertyEditor.cs | 80 +- .../TrueFalsePropertyEditor.cs | 79 +- .../UploadFileTypeValidator.cs | 125 +- .../ValueConverters/BlockEditorConverter.cs | 85 +- .../BlockListPropertyValueConverter.cs | 297 +- .../ColorPickerValueConverter.cs | 85 +- .../FlexibleDropdownPropertyValueConverter.cs | 64 +- .../ValueConverters/GridValueConverter.cs | 153 +- .../ValueConverters/ImageCropperValue.cs | 599 ++-- .../ImageCropperValueConverter.cs | 89 +- .../ImageCropperValueTypeConverter.cs | 50 +- .../ValueConverters/JsonValueConverter.cs | 108 +- .../MarkdownEditorValueConverter.cs | 81 +- .../MediaPickerWithCropsValueConverter.cs | 151 +- .../MultiUrlPickerValueConverter.cs | 158 +- .../NestedContentManyValueConverter.cs | 125 +- .../NestedContentSingleValueConverter.cs | 101 +- .../NestedContentValueConverterBase.cs | 94 +- .../RteMacroRenderingValueConverter.cs | 192 +- .../ValueListConfigurationEditor.cs | 178 +- .../ValueListUniqueValueValidator.cs | 52 +- .../PublishedContentTypeCache.cs | 508 +-- .../PublishedContentQuery.cs | 572 ++-- .../PublishedContentQueryAccessor.cs | 26 +- .../Routing/ContentFinderByConfigured404.cs | 198 +- .../Routing/NotFoundHandlerHelper.cs | 149 +- .../Routing/RedirectTrackingHandler.cs | 255 +- .../Runtime/CoreRuntime.cs | 454 +-- .../Runtime/DefaultMainDomKeyGenerator.cs | 36 +- .../Runtime/FileSystemMainDomLock.cs | 184 +- .../Runtime/RuntimeState.cs | 482 +-- .../Runtime/SqlMainDomLock.cs | 702 ++-- .../Scoping/HttpScopeReference.cs | 62 +- .../Scoping/IHttpScopeReference.cs | 17 +- src/Umbraco.Infrastructure/Scoping/IScope.cs | 4 +- .../Scoping/IScopeAccessor.cs | 17 +- .../Scoping/IScopeProvider.cs | 37 +- .../Scoping/LegacyIScope.cs | 5 +- .../Scoping/LegacyIScopeProvider.cs | 5 +- src/Umbraco.Infrastructure/Scoping/Scope.cs | 1876 ++++++----- .../Scoping/ScopeContext.cs | 167 +- .../Scoping/ScopeContextualBase.cs | 103 +- .../Scoping/ScopeProvider.cs | 58 +- .../Search/IUmbracoIndexingHandler.cs | 76 +- .../IndexingNotificationHandler.Content.cs | 230 +- ...IndexingNotificationHandler.ContentType.cs | 246 +- .../IndexingNotificationHandler.Language.cs | 69 +- .../IndexingNotificationHandler.Media.cs | 154 +- .../IndexingNotificationHandler.Member.cs | 126 +- .../Search/UmbracoTreeSearcher.cs | 298 +- .../Search/UmbracoTreeSearcherFields.cs | 103 +- .../BackOfficeClaimsPrincipalFactory.cs | 103 +- .../Security/BackOfficeErrorDescriber.cs | 192 +- .../Security/BackOfficeIdentityBuilder.cs | 117 +- .../Security/BackOfficeIdentityUser.cs | 275 +- .../Security/BackOfficePasswordHasher.cs | 26 +- .../Security/BackOfficeUserStore.cs | 1196 +++---- .../Security/BackOfficeUserValidator.cs | 21 +- .../Security/ClaimsIdentityExtensions.cs | 55 +- ...eteExternalLoginsOnMemberDeletedHandler.cs | 34 +- ...teTwoFactorLoginsOnMemberDeletedHandler.cs | 37 +- .../Security/IBackOfficeUserManager.cs | 19 +- .../IBackOfficeUserPasswordChecker.cs | 32 +- .../Security/IMemberManager.cs | 103 +- .../Security/IMemberUserStore.cs | 15 +- .../Security/IUmbracoUserManager.cs | 796 ++--- .../Security/IUserSessionStore.cs | 21 +- .../Security/IdentityExtensions.cs | 18 +- .../Security/IdentityMapDefinition.cs | 192 +- .../Security/MemberIdentityBuilder.cs | 92 +- .../Security/MemberIdentityUser.cs | 126 +- .../Security/MemberPasswordHasher.cs | 271 +- .../Security/MemberRoleStore.cs | 414 ++- .../Security/MemberUserStore.cs | 1197 +++---- .../Security/NoOpLookupNormalizer.cs | 17 +- .../Security/SignOutSuccessResult.cs | 9 +- .../Security/UmbracoErrorDescriberBase.cs | 182 +- .../Security/UmbracoIdentityRole.cs | 163 +- .../Security/UmbracoIdentityUser.cs | 586 ++-- .../Security/UmbracoPasswordHasher.cs | 162 +- .../Security/UmbracoUserConfirmation.cs | 22 +- .../Security/UmbracoUserManager.cs | 442 +-- .../Security/UmbracoUserStore.cs | 424 +-- .../AutoInterningStringConverter.cs | 54 +- ...ngKeyCaseInsensitiveDictionaryConverter.cs | 75 +- .../CaseInsensitiveDictionaryConverter.cs | 47 +- .../ConfigurationEditorJsonSerializer.cs | 52 +- .../Serialization/ForceInt32Converter.cs | 36 +- .../Serialization/FuzzyBooleanConverter.cs | 62 +- .../Serialization/JsonNetSerializer.cs | 49 +- .../Serialization/JsonReadConverter.cs | 84 +- .../Serialization/JsonToStringConverter.cs | 43 +- .../KnownTypeUdiJsonConverter.cs | 32 +- .../NoTypeConverterJsonConverter.cs | 79 +- .../Serialization/UdiJsonConverter.cs | 32 +- .../Serialization/UdiRangeJsonConverter.cs | 32 +- .../Services/CacheInstructionService.cs | 63 +- .../Services/EditorConfigurationParser.cs | 17 +- .../Implement/ExamineIndexCountService.cs | 22 +- .../Services/Implement/PackagingService.cs | 252 +- src/Umbraco.Infrastructure/Suspendable.cs | 153 +- .../Sync/BatchedDatabaseServerMessenger.cs | 161 +- .../Sync/DatabaseServerMessenger.cs | 475 +-- .../Sync/LastSyncedFileManager.cs | 137 +- .../Sync/RefreshInstructionEnvelope.cs | 31 +- .../Sync/ServerMessengerBase.cs | 642 ++-- .../Sync/SyncBootStateAccessor.cs | 116 +- src/Umbraco.Infrastructure/TagQuery.cs | 152 +- .../Interfaces/IDetailedTelemetryProvider.cs | 12 +- .../Providers/ContentTelemetryProvider.cs | 25 +- .../Providers/DomainTelemetryProvider.cs | 23 +- .../Providers/ExamineTelemetryProvider.cs | 23 +- .../Providers/LanguagesTelemetryProvider.cs | 27 +- .../Providers/MacroTelemetryProvider.cs | 26 +- .../Providers/MediaTelemetryProvider.cs | 20 +- .../Providers/NodeCountTelemetryProvider.cs | 32 +- .../PropertyEditorTelemetryProvider.cs | 32 +- .../SystemInformationTelemetryProvider.cs | 5 +- .../Providers/UserTelemetryProvider.cs | 30 +- .../Services/UsageInformationService.cs | 46 +- .../Templates/HtmlMacroParameterParser.cs | 228 +- .../Templates/IHtmlMacroParameterParser.cs | 35 +- .../Trees/TreeRootNode.cs | 259 +- .../BackOfficeJavaScriptInitializer.cs | 117 +- .../WebAssets/BackOfficeWebAssets.cs | 513 +-- .../WebAssets/PropertyEditorAssetAttribute.cs | 36 +- .../WebAssets/ServerVariablesParser.cs | 45 +- 835 files changed, 63717 insertions(+), 61277 deletions(-) diff --git a/src/Umbraco.Infrastructure/Cache/DatabaseServerMessengerNotificationHandler.cs b/src/Umbraco.Infrastructure/Cache/DatabaseServerMessengerNotificationHandler.cs index fc59d0601676..4657c8a68ab1 100644 --- a/src/Umbraco.Infrastructure/Cache/DatabaseServerMessengerNotificationHandler.cs +++ b/src/Umbraco.Infrastructure/Cache/DatabaseServerMessengerNotificationHandler.cs @@ -8,54 +8,55 @@ using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// Ensures that distributed cache events are setup and the is initialized +/// +public sealed class DatabaseServerMessengerNotificationHandler : + INotificationHandler, INotificationHandler { + private readonly IUmbracoDatabaseFactory _databaseFactory; + private readonly ILogger _logger; + private readonly IServerMessenger _messenger; + private readonly IRuntimeState _runtimeState; + /// - /// Ensures that distributed cache events are setup and the is initialized + /// Initializes a new instance of the class. /// - public sealed class DatabaseServerMessengerNotificationHandler : INotificationHandler, INotificationHandler + public DatabaseServerMessengerNotificationHandler( + IServerMessenger serverMessenger, + IUmbracoDatabaseFactory databaseFactory, + ILogger logger, + IRuntimeState runtimeState) + { + _databaseFactory = databaseFactory; + _logger = logger; + _messenger = serverMessenger; + _runtimeState = runtimeState; + } + + /// + public void Handle(UmbracoApplicationStartingNotification notification) { - private readonly IServerMessenger _messenger; - private readonly IUmbracoDatabaseFactory _databaseFactory; - private readonly ILogger _logger; - private readonly IRuntimeState _runtimeState; - - /// - /// Initializes a new instance of the class. - /// - public DatabaseServerMessengerNotificationHandler( - IServerMessenger serverMessenger, - IUmbracoDatabaseFactory databaseFactory, - ILogger logger, - IRuntimeState runtimeState) + if (_runtimeState.Level != RuntimeLevel.Run) { - _databaseFactory = databaseFactory; - _logger = logger; - _messenger = serverMessenger; - _runtimeState = runtimeState; + return; } - /// - public void Handle(UmbracoApplicationStartingNotification notification) + if (_databaseFactory.CanConnect == false) { - if (_runtimeState.Level != RuntimeLevel.Run) - { - return; - } - - if (_databaseFactory.CanConnect == false) - { - _logger.LogWarning("Cannot connect to the database, distributed calls will not be enabled for this server."); - return; - } - - // Sync on startup, this will run through the messenger's initialization sequence - _messenger?.Sync(); + _logger.LogWarning( + "Cannot connect to the database, distributed calls will not be enabled for this server."); + return; } - /// - /// Clear the batch on end request - /// - public void Handle(UmbracoRequestEndNotification notification) => _messenger?.SendMessages(); + // Sync on startup, this will run through the messenger's initialization sequence + _messenger?.Sync(); } + + /// + /// Clear the batch on end request + /// + public void Handle(UmbracoRequestEndNotification notification) => _messenger?.SendMessages(); } diff --git a/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs b/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs index b18cce9b3d55..4dbf4352d9c9 100644 --- a/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs +++ b/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs @@ -1,268 +1,274 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Models.Entities; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// Represents the default cache policy. +/// +/// The type of the entity. +/// The type of the identifier. +/// +/// The default cache policy caches entities with a 5 minutes sliding expiration. +/// Each entity is cached individually. +/// If options.GetAllCacheAllowZeroCount then a 'zero-count' array is cached when GetAll finds nothing. +/// If options.GetAllCacheValidateCount then we check against the db when getting many entities. +/// +public class DefaultRepositoryCachePolicy : RepositoryCachePolicyBase + where TEntity : class, IEntity { - /// - /// Represents the default cache policy. - /// - /// The type of the entity. - /// The type of the identifier. - /// - /// The default cache policy caches entities with a 5 minutes sliding expiration. - /// Each entity is cached individually. - /// If options.GetAllCacheAllowZeroCount then a 'zero-count' array is cached when GetAll finds nothing. - /// If options.GetAllCacheValidateCount then we check against the db when getting many entities. - /// - public class DefaultRepositoryCachePolicy : RepositoryCachePolicyBase - where TEntity : class, IEntity - { - private static readonly TEntity[] s_emptyEntities = new TEntity[0]; // const - private readonly RepositoryCachePolicyOptions _options; + private static readonly TEntity[] s_emptyEntities = new TEntity[0]; // const + private readonly RepositoryCachePolicyOptions _options; - public DefaultRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, RepositoryCachePolicyOptions options) - : base(cache, scopeAccessor) - { - _options = options ?? throw new ArgumentNullException(nameof(options)); - } + public DefaultRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, + RepositoryCachePolicyOptions options) + : base(cache, scopeAccessor) => + _options = options ?? throw new ArgumentNullException(nameof(options)); - protected string GetEntityCacheKey(int id) => EntityTypeCacheKey + id; + protected string EntityTypeCacheKey { get; } = $"uRepo_{typeof(TEntity).Name}_"; - protected string GetEntityCacheKey(TId? id) + protected string GetEntityCacheKey(int id) => EntityTypeCacheKey + id; + + protected string GetEntityCacheKey(TId? id) + { + if (EqualityComparer.Default.Equals(id, default)) { - if (EqualityComparer.Default.Equals(id, default)) - { - return string.Empty; - } + return string.Empty; + } - if (typeof(TId).IsValueType) - { - return EntityTypeCacheKey + id; - } - else - { - return EntityTypeCacheKey + id?.ToString()?.ToUpperInvariant(); - } + if (typeof(TId).IsValueType) + { + return EntityTypeCacheKey + id; } - protected string EntityTypeCacheKey { get; } = $"uRepo_{typeof(TEntity).Name}_"; + return EntityTypeCacheKey + id?.ToString()?.ToUpperInvariant(); + } - protected virtual void InsertEntity(string cacheKey, TEntity entity) - => Cache.Insert(cacheKey, () => entity, TimeSpan.FromMinutes(5), true); + protected virtual void InsertEntity(string cacheKey, TEntity entity) + => Cache.Insert(cacheKey, () => entity, TimeSpan.FromMinutes(5), true); - protected virtual void InsertEntities(TId[]? ids, TEntity[]? entities) + protected virtual void InsertEntities(TId[]? ids, TEntity[]? entities) + { + if (ids?.Length == 0 && entities?.Length == 0 && _options.GetAllCacheAllowZeroCount) { - if (ids?.Length == 0 && entities?.Length == 0 && _options.GetAllCacheAllowZeroCount) - { - // getting all of them, and finding nothing. - // if we can cache a zero count, cache an empty array, - // for as long as the cache is not cleared (no expiration) - Cache.Insert(EntityTypeCacheKey, () => s_emptyEntities); - } - else - { - if (entities is not null) - { - // individually cache each item - foreach (var entity in entities) - { - var capture = entity; - Cache.Insert(GetEntityCacheKey(entity.Id), () => capture, TimeSpan.FromMinutes(5), true); - } - } - } + // getting all of them, and finding nothing. + // if we can cache a zero count, cache an empty array, + // for as long as the cache is not cleared (no expiration) + Cache.Insert(EntityTypeCacheKey, () => s_emptyEntities); } - - /// - public override void Create(TEntity entity, Action persistNew) + else { - if (entity == null) throw new ArgumentNullException(nameof(entity)); - - try + if (entities is not null) { - persistNew(entity); - - // just to be safe, we cannot cache an item without an identity - if (entity.HasIdentity) + // individually cache each item + foreach (TEntity entity in entities) { - Cache.Insert(GetEntityCacheKey(entity.Id), () => entity, TimeSpan.FromMinutes(5), true); + TEntity capture = entity; + Cache.Insert(GetEntityCacheKey(entity.Id), () => capture, TimeSpan.FromMinutes(5), true); } - - // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.Clear(EntityTypeCacheKey); } - catch - { - // if an exception is thrown we need to remove the entry from cache, - // this is ONLY a work around because of the way - // that we cache entities: http://issues.umbraco.org/issue/U4-4259 - Cache.Clear(GetEntityCacheKey(entity.Id)); - - // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.Clear(EntityTypeCacheKey); + } + } - throw; - } + /// + public override void Create(TEntity entity, Action persistNew) + { + if (entity == null) + { + throw new ArgumentNullException(nameof(entity)); } - /// - public override void Update(TEntity entity, Action persistUpdated) + try { - if (entity == null) throw new ArgumentNullException(nameof(entity)); + persistNew(entity); - try + // just to be safe, we cannot cache an item without an identity + if (entity.HasIdentity) { - persistUpdated(entity); + Cache.Insert(GetEntityCacheKey(entity.Id), () => entity, TimeSpan.FromMinutes(5), true); + } - // just to be safe, we cannot cache an item without an identity - if (entity.HasIdentity) - { - Cache.Insert(GetEntityCacheKey(entity.Id), () => entity, TimeSpan.FromMinutes(5), true); - } + // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared + Cache.Clear(EntityTypeCacheKey); + } + catch + { + // if an exception is thrown we need to remove the entry from cache, + // this is ONLY a work around because of the way + // that we cache entities: http://issues.umbraco.org/issue/U4-4259 + Cache.Clear(GetEntityCacheKey(entity.Id)); - // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.Clear(EntityTypeCacheKey); - } - catch - { - // if an exception is thrown we need to remove the entry from cache, - // this is ONLY a work around because of the way - // that we cache entities: http://issues.umbraco.org/issue/U4-4259 - Cache.Clear(GetEntityCacheKey(entity.Id)); + // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared + Cache.Clear(EntityTypeCacheKey); - // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.Clear(EntityTypeCacheKey); + throw; + } + } - throw; - } + /// + public override void Update(TEntity entity, Action persistUpdated) + { + if (entity == null) + { + throw new ArgumentNullException(nameof(entity)); } - /// - public override void Delete(TEntity entity, Action persistDeleted) + try { - if (entity == null) throw new ArgumentNullException(nameof(entity)); + persistUpdated(entity); - try + // just to be safe, we cannot cache an item without an identity + if (entity.HasIdentity) { - persistDeleted(entity); + Cache.Insert(GetEntityCacheKey(entity.Id), () => entity, TimeSpan.FromMinutes(5), true); } - finally - { - // whatever happens, clear the cache - var cacheKey = GetEntityCacheKey(entity.Id); - Cache.Clear(cacheKey); - // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.Clear(EntityTypeCacheKey); - } - } - /// - public override TEntity? Get(TId? id, Func performGet, Func?> performGetAll) + // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared + Cache.Clear(EntityTypeCacheKey); + } + catch { - var cacheKey = GetEntityCacheKey(id); - var fromCache = Cache.GetCacheItem(cacheKey); + // if an exception is thrown we need to remove the entry from cache, + // this is ONLY a work around because of the way + // that we cache entities: http://issues.umbraco.org/issue/U4-4259 + Cache.Clear(GetEntityCacheKey(entity.Id)); - // if found in cache then return else fetch and cache - if (fromCache != null) - { - return fromCache; - } + // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared + Cache.Clear(EntityTypeCacheKey); - var entity = performGet(id); + throw; + } + } - if (entity != null && entity.HasIdentity) - { - InsertEntity(cacheKey, entity); - } + /// + public override void Delete(TEntity entity, Action persistDeleted) + { + if (entity == null) + { + throw new ArgumentNullException(nameof(entity)); + } - return entity; + try + { + persistDeleted(entity); + } + finally + { + // whatever happens, clear the cache + var cacheKey = GetEntityCacheKey(entity.Id); + Cache.Clear(cacheKey); + // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared + Cache.Clear(EntityTypeCacheKey); } + } - /// - public override TEntity? GetCached(TId id) + /// + public override TEntity? Get(TId? id, Func performGet, + Func?> performGetAll) + { + var cacheKey = GetEntityCacheKey(id); + TEntity? fromCache = Cache.GetCacheItem(cacheKey); + + // if found in cache then return else fetch and cache + if (fromCache != null) { - var cacheKey = GetEntityCacheKey(id); - return Cache.GetCacheItem(cacheKey); + return fromCache; } - /// - public override bool Exists(TId id, Func performExists, Func?> performGetAll) + TEntity? entity = performGet(id); + + if (entity != null && entity.HasIdentity) { - // if found in cache the return else check - var cacheKey = GetEntityCacheKey(id); - var fromCache = Cache.GetCacheItem(cacheKey); - return fromCache != null || performExists(id); + InsertEntity(cacheKey, entity); } - /// - public override TEntity[] GetAll(TId[]? ids, Func?> performGetAll) + return entity; + } + + /// + public override TEntity? GetCached(TId id) + { + var cacheKey = GetEntityCacheKey(id); + return Cache.GetCacheItem(cacheKey); + } + + /// + public override bool Exists(TId id, Func performExists, Func?> performGetAll) + { + // if found in cache the return else check + var cacheKey = GetEntityCacheKey(id); + TEntity? fromCache = Cache.GetCacheItem(cacheKey); + return fromCache != null || performExists(id); + } + + /// + public override TEntity[] GetAll(TId[]? ids, Func?> performGetAll) + { + if (ids?.Length > 0) { - if (ids?.Length > 0) + // try to get each entity from the cache + // if we can find all of them, return + TEntity[] entities = ids.Select(GetCached).WhereNotNull().ToArray(); + if (ids.Length.Equals(entities.Length)) { - // try to get each entity from the cache - // if we can find all of them, return - var entities = ids.Select(GetCached).WhereNotNull().ToArray(); - if (ids.Length.Equals(entities.Length)) - return entities; // no need for null checks, we are not caching nulls + return entities; // no need for null checks, we are not caching nulls } - else - { - // get everything we have - var entities = Cache.GetCacheItemsByKeySearch(EntityTypeCacheKey)? - .ToArray(); // no need for null checks, we are not caching nulls + } + else + { + // get everything we have + TEntity?[]? entities = Cache.GetCacheItemsByKeySearch(EntityTypeCacheKey)? + .ToArray(); // no need for null checks, we are not caching nulls - if (entities?.Length > 0) + if (entities?.Length > 0) + { + // if some of them were in the cache... + if (_options.GetAllCacheValidateCount) { - // if some of them were in the cache... - if (_options.GetAllCacheValidateCount) + // need to validate the count, get the actual count and return if ok + if (_options.PerformCount is not null) { - // need to validate the count, get the actual count and return if ok - if (_options.PerformCount is not null) + var totalCount = _options.PerformCount(); + if (entities.Length == totalCount) { - var totalCount = _options.PerformCount(); - if (entities.Length == totalCount) - return entities.WhereNotNull().ToArray(); + return entities.WhereNotNull().ToArray(); } } - else - { - // no need to validate, just return what we have and assume it's all there is - return entities.WhereNotNull().ToArray(); - } } - else if (_options.GetAllCacheAllowZeroCount) + else { - // if none of them were in the cache - // and we allow zero count - check for the special (empty) entry - var empty = Cache.GetCacheItem(EntityTypeCacheKey); - if (empty != null) return empty; + // no need to validate, just return what we have and assume it's all there is + return entities.WhereNotNull().ToArray(); } } + else if (_options.GetAllCacheAllowZeroCount) + { + // if none of them were in the cache + // and we allow zero count - check for the special (empty) entry + TEntity[]? empty = Cache.GetCacheItem(EntityTypeCacheKey); + if (empty != null) + { + return empty; + } + } + } - // cache failed, get from repo and cache - var repoEntities = performGetAll(ids)? - .WhereNotNull() // exclude nulls! - .Where(x => x.HasIdentity) // be safe, though would be weird... - .ToArray(); - - // note: if empty & allow zero count, will cache a special (empty) entry - InsertEntities(ids, repoEntities); + // cache failed, get from repo and cache + TEntity[]? repoEntities = performGetAll(ids)? + .WhereNotNull() // exclude nulls! + .Where(x => x.HasIdentity) // be safe, though would be weird... + .ToArray(); - return repoEntities ?? Array.Empty(); - } + // note: if empty & allow zero count, will cache a special (empty) entry + InsertEntities(ids, repoEntities); - /// - public override void ClearAll() - { - Cache.ClearByKey(EntityTypeCacheKey); - } + return repoEntities ?? Array.Empty(); } + + /// + public override void ClearAll() => Cache.ClearByKey(EntityTypeCacheKey); } diff --git a/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs b/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs index 6e6f549b034d..e362a31581c3 100644 --- a/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs +++ b/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs @@ -1,7 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Linq; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; @@ -9,357 +8,341 @@ using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// Default implementation. +/// +public class DistributedCacheBinder : + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler { + private readonly DistributedCache _distributedCache; + /// - /// Default implementation. + /// Initializes a new instance of the class. /// - public class DistributedCacheBinder : - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler - { - private readonly DistributedCache _distributedCache; + public DistributedCacheBinder(DistributedCache distributedCache) => _distributedCache = distributedCache; - /// - /// Initializes a new instance of the class. - /// - public DistributedCacheBinder(DistributedCache distributedCache) - { - _distributedCache = distributedCache; - } + #region MediaService - #region PublicAccessService + public void Handle(MediaTreeChangeNotification notification) => + _distributedCache.RefreshMediaCache(notification.Changes.ToArray()); - public void Handle(PublicAccessEntrySavedNotification notification) - { - _distributedCache.RefreshPublicAccess(); - } + #endregion - public void Handle(PublicAccessEntryDeletedNotification notification) - { - _distributedCache.RefreshPublicAccess(); + #region PublicAccessService - } + public void Handle(PublicAccessEntrySavedNotification notification) => _distributedCache.RefreshPublicAccess(); - #endregion + public void Handle(PublicAccessEntryDeletedNotification notification) => _distributedCache.RefreshPublicAccess(); - #region ContentService + #endregion - /// - /// Handles cache refreshing for when content is copied - /// - /// - /// - /// - /// When an entity is copied new permissions may be assigned to it based on it's parent, if that is the - /// case then we need to clear all user permissions cache. - /// - private void ContentService_Copied(IContentService sender, CopyEventArgs e) - { - } + #region ContentService + /// + /// Handles cache refreshing for when content is copied + /// + /// + /// + /// + /// When an entity is copied new permissions may be assigned to it based on it's parent, if that is the + /// case then we need to clear all user permissions cache. + /// + private void ContentService_Copied(IContentService sender, CopyEventArgs e) + { + } - public void Handle(ContentTreeChangeNotification notification) - { - _distributedCache.RefreshContentCache(notification.Changes.ToArray()); - } - //private void ContentService_SavedBlueprint(IContentService sender, SaveEventArgs e) - //{ - // _distributedCache.RefreshUnpublishedPageCache(e.SavedEntities.ToArray()); - //} + public void Handle(ContentTreeChangeNotification notification) => + _distributedCache.RefreshContentCache(notification.Changes.ToArray()); - //private void ContentService_DeletedBlueprint(IContentService sender, DeleteEventArgs e) - //{ - // _distributedCache.RemoveUnpublishedPageCache(e.DeletedEntities.ToArray()); - //} + //private void ContentService_SavedBlueprint(IContentService sender, SaveEventArgs e) + //{ + // _distributedCache.RefreshUnpublishedPageCache(e.SavedEntities.ToArray()); + //} - #endregion + //private void ContentService_DeletedBlueprint(IContentService sender, DeleteEventArgs e) + //{ + // _distributedCache.RemoveUnpublishedPageCache(e.DeletedEntities.ToArray()); + //} - #region LocalizationService / Dictionary - public void Handle(DictionaryItemSavedNotification notification) + #endregion + + #region LocalizationService / Dictionary + + public void Handle(DictionaryItemSavedNotification notification) + { + foreach (IDictionaryItem entity in notification.SavedEntities) { - foreach (IDictionaryItem entity in notification.SavedEntities) - { - _distributedCache.RefreshDictionaryCache(entity.Id); - } + _distributedCache.RefreshDictionaryCache(entity.Id); } + } - public void Handle(DictionaryItemDeletedNotification notification) + public void Handle(DictionaryItemDeletedNotification notification) + { + foreach (IDictionaryItem entity in notification.DeletedEntities) { - foreach (IDictionaryItem entity in notification.DeletedEntities) - { - _distributedCache.RemoveDictionaryCache(entity.Id); - } + _distributedCache.RemoveDictionaryCache(entity.Id); } + } - #endregion + #endregion - #region DataTypeService + #region DataTypeService - public void Handle(DataTypeSavedNotification notification) + public void Handle(DataTypeSavedNotification notification) + { + foreach (IDataType entity in notification.SavedEntities) { - foreach (IDataType entity in notification.SavedEntities) - { - _distributedCache.RefreshDataTypeCache(entity); - } - _distributedCache.RefreshValueEditorCache(notification.SavedEntities); + _distributedCache.RefreshDataTypeCache(entity); } - public void Handle(DataTypeDeletedNotification notification) + _distributedCache.RefreshValueEditorCache(notification.SavedEntities); + } + + public void Handle(DataTypeDeletedNotification notification) + { + foreach (IDataType entity in notification.DeletedEntities) { - foreach (IDataType entity in notification.DeletedEntities) - { - _distributedCache.RemoveDataTypeCache(entity); - } - _distributedCache.RefreshValueEditorCache(notification.DeletedEntities); + _distributedCache.RemoveDataTypeCache(entity); } - #endregion + _distributedCache.RefreshValueEditorCache(notification.DeletedEntities); + } + + #endregion - #region DomainService + #region DomainService - public void Handle(DomainSavedNotification notification) + public void Handle(DomainSavedNotification notification) + { + foreach (IDomain entity in notification.SavedEntities) { - foreach (IDomain entity in notification.SavedEntities) - { - _distributedCache.RefreshDomainCache(entity); - } + _distributedCache.RefreshDomainCache(entity); } + } - public void Handle(DomainDeletedNotification notification) + public void Handle(DomainDeletedNotification notification) + { + foreach (IDomain entity in notification.DeletedEntities) { - foreach (IDomain entity in notification.DeletedEntities) - { - _distributedCache.RemoveDomainCache(entity); - } + _distributedCache.RemoveDomainCache(entity); } + } - #endregion + #endregion - #region LocalizationService / Language + #region LocalizationService / Language - /// - /// Fires when a language is deleted - /// - /// - public void Handle(LanguageDeletedNotification notification) + /// + /// Fires when a language is deleted + /// + /// + public void Handle(LanguageDeletedNotification notification) + { + foreach (ILanguage entity in notification.DeletedEntities) { - foreach (ILanguage entity in notification.DeletedEntities) - { - _distributedCache.RemoveLanguageCache(entity); - } + _distributedCache.RemoveLanguageCache(entity); } + } - /// - /// Fires when a language is saved - /// - /// - public void Handle(LanguageSavedNotification notification) + /// + /// Fires when a language is saved + /// + /// + public void Handle(LanguageSavedNotification notification) + { + foreach (ILanguage entity in notification.SavedEntities) { - foreach (ILanguage entity in notification.SavedEntities) - { - _distributedCache.RefreshLanguageCache(entity); - } + _distributedCache.RefreshLanguageCache(entity); } + } - #endregion + #endregion - #region Content|Media|MemberTypeService + #region Content|Media|MemberTypeService - public void Handle(ContentTypeChangedNotification notification) => - _distributedCache.RefreshContentTypeCache(notification.Changes.ToArray()); + public void Handle(ContentTypeChangedNotification notification) => + _distributedCache.RefreshContentTypeCache(notification.Changes.ToArray()); - public void Handle(MediaTypeChangedNotification notification) => - _distributedCache.RefreshContentTypeCache(notification.Changes.ToArray()); + public void Handle(MediaTypeChangedNotification notification) => + _distributedCache.RefreshContentTypeCache(notification.Changes.ToArray()); - public void Handle(MemberTypeChangedNotification notification) => - _distributedCache.RefreshContentTypeCache(notification.Changes.ToArray()); + public void Handle(MemberTypeChangedNotification notification) => + _distributedCache.RefreshContentTypeCache(notification.Changes.ToArray()); - #endregion + #endregion - #region UserService + #region UserService - public void Handle(UserSavedNotification notification) + public void Handle(UserSavedNotification notification) + { + foreach (IUser entity in notification.SavedEntities) { - foreach (IUser entity in notification.SavedEntities) - { - _distributedCache.RefreshUserCache(entity.Id); - } + _distributedCache.RefreshUserCache(entity.Id); } + } - public void Handle(UserDeletedNotification notification) + public void Handle(UserDeletedNotification notification) + { + foreach (IUser entity in notification.DeletedEntities) { - foreach (IUser entity in notification.DeletedEntities) - { - _distributedCache.RemoveUserCache(entity.Id); - } + _distributedCache.RemoveUserCache(entity.Id); } + } - public void Handle(UserGroupWithUsersSavedNotification notification) + public void Handle(UserGroupWithUsersSavedNotification notification) + { + foreach (UserGroupWithUsers entity in notification.SavedEntities) { - foreach (UserGroupWithUsers entity in notification.SavedEntities) - { - _distributedCache.RefreshUserGroupCache(entity.UserGroup.Id); - } + _distributedCache.RefreshUserGroupCache(entity.UserGroup.Id); } + } - public void Handle(UserGroupDeletedNotification notification) + public void Handle(UserGroupDeletedNotification notification) + { + foreach (IUserGroup entity in notification.DeletedEntities) { - foreach (IUserGroup entity in notification.DeletedEntities) - { - _distributedCache.RemoveUserGroupCache(entity.Id); - } + _distributedCache.RemoveUserGroupCache(entity.Id); } + } - #endregion + #endregion - #region FileService + #region FileService - /// - /// Removes cache for template - /// - /// - public void Handle(TemplateDeletedNotification notification) + /// + /// Removes cache for template + /// + /// + public void Handle(TemplateDeletedNotification notification) + { + foreach (ITemplate entity in notification.DeletedEntities) { - foreach (ITemplate entity in notification.DeletedEntities) - { - _distributedCache.RemoveTemplateCache(entity.Id); - } + _distributedCache.RemoveTemplateCache(entity.Id); } + } - /// - /// Refresh cache for template - /// - /// - public void Handle(TemplateSavedNotification notification) + /// + /// Refresh cache for template + /// + /// + public void Handle(TemplateSavedNotification notification) + { + foreach (ITemplate entity in notification.SavedEntities) { - foreach (ITemplate entity in notification.SavedEntities) - { - _distributedCache.RefreshTemplateCache(entity.Id); - } + _distributedCache.RefreshTemplateCache(entity.Id); } + } - #endregion - - #region MacroService + #endregion - public void Handle(MacroDeletedNotification notification) - { - foreach (IMacro entity in notification.DeletedEntities) - { - _distributedCache.RemoveMacroCache(entity); - } - } + #region MacroService - public void Handle(MacroSavedNotification notification) + public void Handle(MacroDeletedNotification notification) + { + foreach (IMacro entity in notification.DeletedEntities) { - foreach (IMacro entity in notification.SavedEntities) - { - _distributedCache.RefreshMacroCache(entity); - } + _distributedCache.RemoveMacroCache(entity); } + } - #endregion - - #region MediaService - - public void Handle(MediaTreeChangeNotification notification) + public void Handle(MacroSavedNotification notification) + { + foreach (IMacro entity in notification.SavedEntities) { - _distributedCache.RefreshMediaCache(notification.Changes.ToArray()); + _distributedCache.RefreshMacroCache(entity); } + } - #endregion + #endregion - #region MemberService + #region MemberService - public void Handle(MemberDeletedNotification notification) - { - _distributedCache.RemoveMemberCache(notification.DeletedEntities.ToArray()); - } + public void Handle(MemberDeletedNotification notification) => + _distributedCache.RemoveMemberCache(notification.DeletedEntities.ToArray()); - public void Handle(MemberSavedNotification notification) - { - _distributedCache.RefreshMemberCache(notification.SavedEntities.ToArray()); - } + public void Handle(MemberSavedNotification notification) => + _distributedCache.RefreshMemberCache(notification.SavedEntities.ToArray()); - #endregion + #endregion - #region MemberGroupService + #region MemberGroupService - /// - /// Fires when a member group is deleted - /// - /// - public void Handle(MemberGroupDeletedNotification notification) + /// + /// Fires when a member group is deleted + /// + /// + public void Handle(MemberGroupDeletedNotification notification) + { + foreach (IMemberGroup entity in notification.DeletedEntities) { - foreach (IMemberGroup entity in notification.DeletedEntities) - { - _distributedCache.RemoveMemberGroupCache(entity.Id); - } + _distributedCache.RemoveMemberGroupCache(entity.Id); } + } - /// - /// Fires when a member group is saved - /// - /// - public void Handle(MemberGroupSavedNotification notification) + /// + /// Fires when a member group is saved + /// + /// + public void Handle(MemberGroupSavedNotification notification) + { + foreach (IMemberGroup entity in notification.SavedEntities) { - foreach (IMemberGroup entity in notification.SavedEntities) - { - _distributedCache.RemoveMemberGroupCache(entity.Id); - } + _distributedCache.RemoveMemberGroupCache(entity.Id); } + } - #endregion + #endregion - #region RelationType + #region RelationType - public void Handle(RelationTypeSavedNotification notification) + public void Handle(RelationTypeSavedNotification notification) + { + DistributedCache dc = _distributedCache; + foreach (IRelationType entity in notification.SavedEntities) { - DistributedCache dc = _distributedCache; - foreach (IRelationType entity in notification.SavedEntities) - { - dc.RefreshRelationTypeCache(entity.Id); - } + dc.RefreshRelationTypeCache(entity.Id); } + } - public void Handle(RelationTypeDeletedNotification notification) + public void Handle(RelationTypeDeletedNotification notification) + { + DistributedCache dc = _distributedCache; + foreach (IRelationType entity in notification.DeletedEntities) { - DistributedCache dc = _distributedCache; - foreach (IRelationType entity in notification.DeletedEntities) - { - dc.RemoveRelationTypeCache(entity.Id); - } + dc.RemoveRelationTypeCache(entity.Id); } - - #endregion } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Cache/DistributedCacheExtensions.cs b/src/Umbraco.Infrastructure/Cache/DistributedCacheExtensions.cs index ceac767a8cec..5201e794794a 100644 --- a/src/Umbraco.Infrastructure/Cache/DistributedCacheExtensions.cs +++ b/src/Umbraco.Infrastructure/Cache/DistributedCacheExtensions.cs @@ -1,327 +1,370 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services.Changes; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Extension methods for . +/// +public static class DistributedCacheExtensions { - /// - /// Extension methods for . - /// - public static class DistributedCacheExtensions - { - #region PublicAccessCache + #region PublicAccessCache - public static void RefreshPublicAccess(this DistributedCache dc) - { - dc.RefreshAll(PublicAccessCacheRefresher.UniqueId); - } + public static void RefreshPublicAccess(this DistributedCache dc) => + dc.RefreshAll(PublicAccessCacheRefresher.UniqueId); - #endregion + #endregion - #region User cache + #region ValueEditorCache - public static void RemoveUserCache(this DistributedCache dc, int userId) + public static void RefreshValueEditorCache(this DistributedCache dc, IEnumerable dataTypes) + { + if (dataTypes is null) { - dc.Remove(UserCacheRefresher.UniqueId, userId); + return; } - public static void RefreshUserCache(this DistributedCache dc, int userId) - { - dc.Refresh(UserCacheRefresher.UniqueId, userId); - } + IEnumerable payloads = + dataTypes.Select(x => new DataTypeCacheRefresher.JsonPayload(x.Id, x.Key, false)); + dc.RefreshByPayload(ValueEditorCacheRefresher.UniqueId, payloads); + } - public static void RefreshAllUserCache(this DistributedCache dc) - { - dc.RefreshAll(UserCacheRefresher.UniqueId); - } + #endregion - #endregion + #region Published Snapshot - #region User group cache + public static void RefreshAllPublishedSnapshot(this DistributedCache dc) + { + // note: refresh all content & media caches does refresh content types too + dc.RefreshAllContentCache(); + dc.RefreshAllMediaCache(); + dc.RefreshAllDomainCache(); + } - public static void RemoveUserGroupCache(this DistributedCache dc, int userId) - { - dc.Remove(UserGroupCacheRefresher.UniqueId, userId); - } + #endregion - public static void RefreshUserGroupCache(this DistributedCache dc, int userId) - { - dc.Refresh(UserGroupCacheRefresher.UniqueId, userId); - } + #region User cache - public static void RefreshAllUserGroupCache(this DistributedCache dc) - { - dc.RefreshAll(UserGroupCacheRefresher.UniqueId); - } + public static void RemoveUserCache(this DistributedCache dc, int userId) => + dc.Remove(UserCacheRefresher.UniqueId, userId); - #endregion + public static void RefreshUserCache(this DistributedCache dc, int userId) => + dc.Refresh(UserCacheRefresher.UniqueId, userId); - #region TemplateCache + public static void RefreshAllUserCache(this DistributedCache dc) => dc.RefreshAll(UserCacheRefresher.UniqueId); - public static void RefreshTemplateCache(this DistributedCache dc, int templateId) - { - dc.Refresh(TemplateCacheRefresher.UniqueId, templateId); - } + #endregion - public static void RemoveTemplateCache(this DistributedCache dc, int templateId) - { - dc.Remove(TemplateCacheRefresher.UniqueId, templateId); - } + #region User group cache - #endregion + public static void RemoveUserGroupCache(this DistributedCache dc, int userId) => + dc.Remove(UserGroupCacheRefresher.UniqueId, userId); - #region DictionaryCache + public static void RefreshUserGroupCache(this DistributedCache dc, int userId) => + dc.Refresh(UserGroupCacheRefresher.UniqueId, userId); - public static void RefreshDictionaryCache(this DistributedCache dc, int dictionaryItemId) - { - dc.Refresh(DictionaryCacheRefresher.UniqueId, dictionaryItemId); - } + public static void RefreshAllUserGroupCache(this DistributedCache dc) => + dc.RefreshAll(UserGroupCacheRefresher.UniqueId); - public static void RemoveDictionaryCache(this DistributedCache dc, int dictionaryItemId) - { - dc.Remove(DictionaryCacheRefresher.UniqueId, dictionaryItemId); - } + #endregion - #endregion + #region TemplateCache - #region DataTypeCache + public static void RefreshTemplateCache(this DistributedCache dc, int templateId) => + dc.Refresh(TemplateCacheRefresher.UniqueId, templateId); - public static void RefreshDataTypeCache(this DistributedCache dc, IDataType dataType) - { - if (dataType == null) return; - var payloads = new[] { new DataTypeCacheRefresher.JsonPayload(dataType.Id, dataType.Key, false) }; - dc.RefreshByPayload(DataTypeCacheRefresher.UniqueId, payloads); - } + public static void RemoveTemplateCache(this DistributedCache dc, int templateId) => + dc.Remove(TemplateCacheRefresher.UniqueId, templateId); - public static void RemoveDataTypeCache(this DistributedCache dc, IDataType dataType) - { - if (dataType == null) return; - var payloads = new[] { new DataTypeCacheRefresher.JsonPayload(dataType.Id, dataType.Key, true) }; - dc.RefreshByPayload(DataTypeCacheRefresher.UniqueId, payloads); - } + #endregion - #endregion + #region DictionaryCache - #region ValueEditorCache + public static void RefreshDictionaryCache(this DistributedCache dc, int dictionaryItemId) => + dc.Refresh(DictionaryCacheRefresher.UniqueId, dictionaryItemId); - public static void RefreshValueEditorCache(this DistributedCache dc, IEnumerable dataTypes) - { - if (dataTypes is null) - { - return; - } + public static void RemoveDictionaryCache(this DistributedCache dc, int dictionaryItemId) => + dc.Remove(DictionaryCacheRefresher.UniqueId, dictionaryItemId); - var payloads = dataTypes.Select(x => new DataTypeCacheRefresher.JsonPayload(x.Id, x.Key, false)); - dc.RefreshByPayload(ValueEditorCacheRefresher.UniqueId, payloads); - } + #endregion - #endregion + #region DataTypeCache - #region ContentCache + public static void RefreshDataTypeCache(this DistributedCache dc, IDataType dataType) + { + if (dataType == null) + { + return; + } - public static void RefreshAllContentCache(this DistributedCache dc) + DataTypeCacheRefresher.JsonPayload[] payloads = new[] { - var payloads = new[] { new ContentCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }; + new DataTypeCacheRefresher.JsonPayload(dataType.Id, dataType.Key, false) + }; + dc.RefreshByPayload(DataTypeCacheRefresher.UniqueId, payloads); + } - // note: refresh all content cache does refresh content types too - dc.RefreshByPayload(ContentCacheRefresher.UniqueId, payloads); + public static void RemoveDataTypeCache(this DistributedCache dc, IDataType dataType) + { + if (dataType == null) + { + return; } - public static void RefreshContentCache(this DistributedCache dc, TreeChange[] changes) + DataTypeCacheRefresher.JsonPayload[] payloads = new[] { - if (changes.Length == 0) return; + new DataTypeCacheRefresher.JsonPayload(dataType.Id, dataType.Key, true) + }; + dc.RefreshByPayload(DataTypeCacheRefresher.UniqueId, payloads); + } - var payloads = changes - .Select(x => new ContentCacheRefresher.JsonPayload(x.Item.Id, x.Item.Key, x.ChangeTypes)); + #endregion - dc.RefreshByPayload(ContentCacheRefresher.UniqueId, payloads); - } + #region ContentCache - #endregion + public static void RefreshAllContentCache(this DistributedCache dc) + { + ContentCacheRefresher.JsonPayload[] payloads = new[] + { + new ContentCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) + }; - #region MemberCache + // note: refresh all content cache does refresh content types too + dc.RefreshByPayload(ContentCacheRefresher.UniqueId, payloads); + } - public static void RefreshMemberCache(this DistributedCache dc, params IMember[] members) + public static void RefreshContentCache(this DistributedCache dc, TreeChange[] changes) + { + if (changes.Length == 0) { - if (members.Length == 0) return; - dc.RefreshByPayload(MemberCacheRefresher.UniqueId, members.Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username, false))); + return; } - public static void RemoveMemberCache(this DistributedCache dc, params IMember[] members) - { - if (members.Length == 0) return; - dc.RefreshByPayload(MemberCacheRefresher.UniqueId, members.Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username, true))); - } + IEnumerable payloads = changes + .Select(x => new ContentCacheRefresher.JsonPayload(x.Item.Id, x.Item.Key, x.ChangeTypes)); + + dc.RefreshByPayload(ContentCacheRefresher.UniqueId, payloads); + } - #endregion + #endregion - #region MemberGroupCache + #region MemberCache - public static void RefreshMemberGroupCache(this DistributedCache dc, int memberGroupId) + public static void RefreshMemberCache(this DistributedCache dc, params IMember[] members) + { + if (members.Length == 0) { - dc.Refresh(MemberGroupCacheRefresher.UniqueId, memberGroupId); + return; } - public static void RemoveMemberGroupCache(this DistributedCache dc, int memberGroupId) + dc.RefreshByPayload(MemberCacheRefresher.UniqueId, + members.Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username, false))); + } + + public static void RemoveMemberCache(this DistributedCache dc, params IMember[] members) + { + if (members.Length == 0) { - dc.Remove(MemberGroupCacheRefresher.UniqueId, memberGroupId); + return; } - #endregion + dc.RefreshByPayload(MemberCacheRefresher.UniqueId, + members.Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username, true))); + } - #region MediaCache + #endregion - public static void RefreshAllMediaCache(this DistributedCache dc) - { - var payloads = new[] { new MediaCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }; + #region MemberGroupCache - // note: refresh all media cache does refresh content types too - dc.RefreshByPayload(MediaCacheRefresher.UniqueId, payloads); - } + public static void RefreshMemberGroupCache(this DistributedCache dc, int memberGroupId) => + dc.Refresh(MemberGroupCacheRefresher.UniqueId, memberGroupId); - public static void RefreshMediaCache(this DistributedCache dc, TreeChange[] changes) - { - if (changes.Length == 0) return; + public static void RemoveMemberGroupCache(this DistributedCache dc, int memberGroupId) => + dc.Remove(MemberGroupCacheRefresher.UniqueId, memberGroupId); - var payloads = changes - .Select(x => new MediaCacheRefresher.JsonPayload(x.Item.Id, x.Item.Key, x.ChangeTypes)); + #endregion - dc.RefreshByPayload(MediaCacheRefresher.UniqueId, payloads); - } + #region MediaCache - #endregion + public static void RefreshAllMediaCache(this DistributedCache dc) + { + MediaCacheRefresher.JsonPayload[] payloads = new[] + { + new MediaCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) + }; - #region Published Snapshot + // note: refresh all media cache does refresh content types too + dc.RefreshByPayload(MediaCacheRefresher.UniqueId, payloads); + } - public static void RefreshAllPublishedSnapshot(this DistributedCache dc) + public static void RefreshMediaCache(this DistributedCache dc, TreeChange[] changes) + { + if (changes.Length == 0) { - // note: refresh all content & media caches does refresh content types too - dc.RefreshAllContentCache(); - dc.RefreshAllMediaCache(); - dc.RefreshAllDomainCache(); + return; } - #endregion + IEnumerable payloads = changes + .Select(x => new MediaCacheRefresher.JsonPayload(x.Item.Id, x.Item.Key, x.ChangeTypes)); + + dc.RefreshByPayload(MediaCacheRefresher.UniqueId, payloads); + } + + #endregion - #region MacroCache + #region MacroCache - public static void RefreshMacroCache(this DistributedCache dc, IMacro macro) + public static void RefreshMacroCache(this DistributedCache dc, IMacro macro) + { + if (macro == null) { - if (macro == null) return; - var payloads = new[] { new MacroCacheRefresher.JsonPayload(macro.Id, macro.Alias) }; - dc.RefreshByPayload(MacroCacheRefresher.UniqueId, payloads); + return; } - public static void RemoveMacroCache(this DistributedCache dc, IMacro macro) + MacroCacheRefresher.JsonPayload[] payloads = new[] {new MacroCacheRefresher.JsonPayload(macro.Id, macro.Alias)}; + dc.RefreshByPayload(MacroCacheRefresher.UniqueId, payloads); + } + + public static void RemoveMacroCache(this DistributedCache dc, IMacro macro) + { + if (macro == null) { - if (macro == null) return; - var payloads = new[] { new MacroCacheRefresher.JsonPayload(macro.Id, macro.Alias) }; - dc.RefreshByPayload(MacroCacheRefresher.UniqueId, payloads); + return; } - #endregion + MacroCacheRefresher.JsonPayload[] payloads = new[] {new MacroCacheRefresher.JsonPayload(macro.Id, macro.Alias)}; + dc.RefreshByPayload(MacroCacheRefresher.UniqueId, payloads); + } + + #endregion - #region Content/Media/Member type cache + #region Content/Media/Member type cache - public static void RefreshContentTypeCache(this DistributedCache dc, ContentTypeChange[] changes) + public static void RefreshContentTypeCache(this DistributedCache dc, ContentTypeChange[] changes) + { + if (changes.Length == 0) { - if (changes.Length == 0) return; + return; + } - var payloads = changes - .Select(x => new ContentTypeCacheRefresher.JsonPayload(typeof (IContentType).Name, x.Item.Id, x.ChangeTypes)); + IEnumerable payloads = changes + .Select(x => + new ContentTypeCacheRefresher.JsonPayload(typeof(IContentType).Name, x.Item.Id, x.ChangeTypes)); - dc.RefreshByPayload(ContentTypeCacheRefresher.UniqueId, payloads); - } + dc.RefreshByPayload(ContentTypeCacheRefresher.UniqueId, payloads); + } - public static void RefreshContentTypeCache(this DistributedCache dc, ContentTypeChange[] changes) + public static void RefreshContentTypeCache(this DistributedCache dc, ContentTypeChange[] changes) + { + if (changes.Length == 0) { - if (changes.Length == 0) return; + return; + } - var payloads = changes - .Select(x => new ContentTypeCacheRefresher.JsonPayload(typeof(IMediaType).Name, x.Item.Id, x.ChangeTypes)); + IEnumerable payloads = changes + .Select(x => new ContentTypeCacheRefresher.JsonPayload(typeof(IMediaType).Name, x.Item.Id, x.ChangeTypes)); - dc.RefreshByPayload(ContentTypeCacheRefresher.UniqueId, payloads); - } + dc.RefreshByPayload(ContentTypeCacheRefresher.UniqueId, payloads); + } - public static void RefreshContentTypeCache(this DistributedCache dc, ContentTypeChange[] changes) + public static void RefreshContentTypeCache(this DistributedCache dc, ContentTypeChange[] changes) + { + if (changes.Length == 0) { - if (changes.Length == 0) return; + return; + } - var payloads = changes - .Select(x => new ContentTypeCacheRefresher.JsonPayload(typeof(IMemberType).Name, x.Item.Id, x.ChangeTypes)); + IEnumerable payloads = changes + .Select(x => new ContentTypeCacheRefresher.JsonPayload(typeof(IMemberType).Name, x.Item.Id, x.ChangeTypes)); - dc.RefreshByPayload(ContentTypeCacheRefresher.UniqueId, payloads); - } + dc.RefreshByPayload(ContentTypeCacheRefresher.UniqueId, payloads); + } - #endregion + #endregion - #region Domain Cache + #region Domain Cache - public static void RefreshDomainCache(this DistributedCache dc, IDomain domain) + public static void RefreshDomainCache(this DistributedCache dc, IDomain domain) + { + if (domain == null) { - if (domain == null) return; - var payloads = new[] { new DomainCacheRefresher.JsonPayload(domain.Id, DomainChangeTypes.Refresh) }; - dc.RefreshByPayload(DomainCacheRefresher.UniqueId, payloads); + return; } - public static void RemoveDomainCache(this DistributedCache dc, IDomain domain) + DomainCacheRefresher.JsonPayload[] payloads = new[] { - if (domain == null) return; - var payloads = new[] { new DomainCacheRefresher.JsonPayload(domain.Id, DomainChangeTypes.Remove) }; - dc.RefreshByPayload(DomainCacheRefresher.UniqueId, payloads); - } + new DomainCacheRefresher.JsonPayload(domain.Id, DomainChangeTypes.Refresh) + }; + dc.RefreshByPayload(DomainCacheRefresher.UniqueId, payloads); + } - public static void RefreshAllDomainCache(this DistributedCache dc) + public static void RemoveDomainCache(this DistributedCache dc, IDomain domain) + { + if (domain == null) { - var payloads = new[] { new DomainCacheRefresher.JsonPayload(0, DomainChangeTypes.RefreshAll) }; - dc.RefreshByPayload(DomainCacheRefresher.UniqueId, payloads); + return; } - #endregion - - #region Language Cache + DomainCacheRefresher.JsonPayload[] payloads = new[] + { + new DomainCacheRefresher.JsonPayload(domain.Id, DomainChangeTypes.Remove) + }; + dc.RefreshByPayload(DomainCacheRefresher.UniqueId, payloads); + } - public static void RefreshLanguageCache(this DistributedCache dc, ILanguage language) + public static void RefreshAllDomainCache(this DistributedCache dc) + { + DomainCacheRefresher.JsonPayload[] payloads = new[] { - if (language == null) return; + new DomainCacheRefresher.JsonPayload(0, DomainChangeTypes.RefreshAll) + }; + dc.RefreshByPayload(DomainCacheRefresher.UniqueId, payloads); + } - var payload = new LanguageCacheRefresher.JsonPayload(language.Id, language.IsoCode, - language.WasPropertyDirty(nameof(ILanguage.IsoCode)) - ? LanguageCacheRefresher.JsonPayload.LanguageChangeType.ChangeCulture - : LanguageCacheRefresher.JsonPayload.LanguageChangeType.Update); + #endregion - dc.RefreshByPayload(LanguageCacheRefresher.UniqueId, new[] { payload }); - } + #region Language Cache - public static void RemoveLanguageCache(this DistributedCache dc, ILanguage language) + public static void RefreshLanguageCache(this DistributedCache dc, ILanguage language) + { + if (language == null) { - if (language == null) return; - - var payload = new LanguageCacheRefresher.JsonPayload(language.Id, language.IsoCode, LanguageCacheRefresher.JsonPayload.LanguageChangeType.Remove); - dc.RefreshByPayload(LanguageCacheRefresher.UniqueId, new[] { payload }); + return; } - #endregion + var payload = new LanguageCacheRefresher.JsonPayload(language.Id, language.IsoCode, + language.WasPropertyDirty(nameof(ILanguage.IsoCode)) + ? LanguageCacheRefresher.JsonPayload.LanguageChangeType.ChangeCulture + : LanguageCacheRefresher.JsonPayload.LanguageChangeType.Update); - #region Relation type cache + dc.RefreshByPayload(LanguageCacheRefresher.UniqueId, new[] {payload}); + } - public static void RefreshRelationTypeCache(this DistributedCache dc, int id) + public static void RemoveLanguageCache(this DistributedCache dc, ILanguage language) + { + if (language == null) { - dc.Refresh(RelationTypeCacheRefresher.UniqueId, id); + return; } - public static void RemoveRelationTypeCache(this DistributedCache dc, int id) - { - dc.Remove(RelationTypeCacheRefresher.UniqueId, id); - } + var payload = new LanguageCacheRefresher.JsonPayload(language.Id, language.IsoCode, + LanguageCacheRefresher.JsonPayload.LanguageChangeType.Remove); + dc.RefreshByPayload(LanguageCacheRefresher.UniqueId, new[] {payload}); + } - #endregion + #endregion + #region Relation type cache - } + public static void RefreshRelationTypeCache(this DistributedCache dc, int id) => + dc.Refresh(RelationTypeCacheRefresher.UniqueId, id); + + public static void RemoveRelationTypeCache(this DistributedCache dc, int id) => + dc.Remove(RelationTypeCacheRefresher.UniqueId, id); + + #endregion } diff --git a/src/Umbraco.Infrastructure/Cache/FullDataSetRepositoryCachePolicy.cs b/src/Umbraco.Infrastructure/Cache/FullDataSetRepositoryCachePolicy.cs index 34cdd3ce0c4d..f9615e99dbd8 100644 --- a/src/Umbraco.Infrastructure/Cache/FullDataSetRepositoryCachePolicy.cs +++ b/src/Umbraco.Infrastructure/Cache/FullDataSetRepositoryCachePolicy.cs @@ -1,187 +1,194 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Collections; using Umbraco.Cms.Core.Models.Entities; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// Represents a caching policy that caches the entire entities set as a single collection. +/// +/// The type of the entity. +/// The type of the identifier. +/// +/// Caches the entire set of entities as a single collection. +/// +/// Used by Content-, Media- and MemberTypeRepository, DataTypeRepository, DomainRepository, +/// LanguageRepository, PublicAccessRepository, TemplateRepository... things that make sense to +/// keep as a whole in memory. +/// +/// +internal class FullDataSetRepositoryCachePolicy : RepositoryCachePolicyBase + where TEntity : class, IEntity { - /// - /// Represents a caching policy that caches the entire entities set as a single collection. - /// - /// The type of the entity. - /// The type of the identifier. - /// - /// Caches the entire set of entities as a single collection. - /// Used by Content-, Media- and MemberTypeRepository, DataTypeRepository, DomainRepository, - /// LanguageRepository, PublicAccessRepository, TemplateRepository... things that make sense to - /// keep as a whole in memory. - /// - internal class FullDataSetRepositoryCachePolicy : RepositoryCachePolicyBase - where TEntity : class, IEntity + protected static readonly TId[] EmptyIds = new TId[0]; // const + private readonly Func _entityGetId; + private readonly bool _expires; + + public FullDataSetRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, + Func entityGetId, bool expires) + : base(cache, scopeAccessor) { - private readonly Func _entityGetId; - private readonly bool _expires; + _entityGetId = entityGetId; + _expires = expires; + } + + protected string GetEntityTypeCacheKey() => $"uRepo_{typeof(TEntity).Name}_"; - public FullDataSetRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, Func entityGetId, bool expires) - : base(cache, scopeAccessor) + protected void InsertEntities(TEntity[]? entities) + { + if (entities is null) { - _entityGetId = entityGetId; - _expires = expires; + return; } - protected static readonly TId[] EmptyIds = new TId[0]; // const + // cache is expected to be a deep-cloning cache ie it deep-clones whatever is + // IDeepCloneable when it goes in, and out. it also resets dirty properties, + // making sure that no 'dirty' entity is cached. + // + // this policy is caching the entire list of entities. to ensure that entities + // are properly deep-clones when cached, it uses a DeepCloneableList. however, + // we don't want to deep-clone *each* entity in the list when fetching it from + // cache as that would not be efficient for Get(id). so the DeepCloneableList is + // set to ListCloneBehavior.CloneOnce ie it will clone *once* when inserting, + // and then will *not* clone when retrieving. + + var key = GetEntityTypeCacheKey(); - protected string GetEntityTypeCacheKey() + if (_expires) { - return $"uRepo_{typeof (TEntity).Name}_"; + Cache.Insert(key, () => new DeepCloneableList(entities), TimeSpan.FromMinutes(5), true); } - - protected void InsertEntities(TEntity[]? entities) + else { - if (entities is null) - { - return; - } - - // cache is expected to be a deep-cloning cache ie it deep-clones whatever is - // IDeepCloneable when it goes in, and out. it also resets dirty properties, - // making sure that no 'dirty' entity is cached. - // - // this policy is caching the entire list of entities. to ensure that entities - // are properly deep-clones when cached, it uses a DeepCloneableList. however, - // we don't want to deep-clone *each* entity in the list when fetching it from - // cache as that would not be efficient for Get(id). so the DeepCloneableList is - // set to ListCloneBehavior.CloneOnce ie it will clone *once* when inserting, - // and then will *not* clone when retrieving. - - var key = GetEntityTypeCacheKey(); - - if (_expires) - { - Cache.Insert(key, () => new DeepCloneableList(entities), TimeSpan.FromMinutes(5), true); - } - else - { - Cache.Insert(key, () => new DeepCloneableList(entities)); - } + Cache.Insert(key, () => new DeepCloneableList(entities)); } + } - /// - public override void Create(TEntity entity, Action persistNew) + /// + public override void Create(TEntity entity, Action persistNew) + { + if (entity == null) { - if (entity == null) throw new ArgumentNullException(nameof(entity)); - - try - { - persistNew(entity); - } - finally - { - ClearAll(); - } + throw new ArgumentNullException(nameof(entity)); } - /// - public override void Update(TEntity entity, Action persistUpdated) + try { - if (entity == null) throw new ArgumentNullException(nameof(entity)); - - try - { - persistUpdated(entity); - } - finally - { - ClearAll(); - } + persistNew(entity); } - - /// - public override void Delete(TEntity entity, Action persistDeleted) + finally { - if (entity == null) throw new ArgumentNullException(nameof(entity)); - - try - { - persistDeleted(entity); - } - finally - { - ClearAll(); - } + ClearAll(); } + } - /// - public override TEntity? Get(TId? id, Func performGet, Func?> performGetAll) + /// + public override void Update(TEntity entity, Action persistUpdated) + { + if (entity == null) { - // get all from the cache, then look for the entity - var all = GetAllCached(performGetAll); - var entity = all.FirstOrDefault(x => _entityGetId(x)?.Equals(id) ?? false); - - // see note in InsertEntities - what we get here is the original - // cached entity, not a clone, so we need to manually ensure it is deep-cloned. - return (TEntity?)entity?.DeepClone(); + throw new ArgumentNullException(nameof(entity)); } - /// - public override TEntity? GetCached(TId id) + try { - // get all from the cache -- and only the cache, then look for the entity - var all = Cache.GetCacheItem>(GetEntityTypeCacheKey()); - var entity = all?.FirstOrDefault(x => _entityGetId(x)?.Equals(id) ?? false); - - // see note in InsertEntities - what we get here is the original - // cached entity, not a clone, so we need to manually ensure it is deep-cloned. - return (TEntity?) entity?.DeepClone(); + persistUpdated(entity); + } + finally + { + ClearAll(); } + } - /// - public override bool Exists(TId id, Func performExits, Func?> performGetAll) + /// + public override void Delete(TEntity entity, Action persistDeleted) + { + if (entity == null) { - // get all as one set, then look for the entity - var all = GetAllCached(performGetAll); - return all.Any(x => _entityGetId(x)?.Equals(id) ?? false); + throw new ArgumentNullException(nameof(entity)); } - /// - public override TEntity[] GetAll(TId[]? ids, Func?> performGetAll) + try + { + persistDeleted(entity); + } + finally { - // get all as one set, from cache if possible, else repo - var all = GetAllCached(performGetAll); + ClearAll(); + } + } - // if ids have been specified, filter - if (ids?.Length > 0) all = all.Where(x => ids.Contains(_entityGetId(x))); + /// + public override TEntity? Get(TId? id, Func performGet, + Func?> performGetAll) + { + // get all from the cache, then look for the entity + IEnumerable all = GetAllCached(performGetAll); + TEntity? entity = all.FirstOrDefault(x => _entityGetId(x)?.Equals(id) ?? false); - // and return - // see note in SetCacheActionToInsertEntities - what we get here is the original - // cached entities, not clones, so we need to manually ensure they are deep-cloned. - return all.Select(x => (TEntity) x.DeepClone()).ToArray(); - } + // see note in InsertEntities - what we get here is the original + // cached entity, not a clone, so we need to manually ensure it is deep-cloned. + return (TEntity?)entity?.DeepClone(); + } - // does NOT clone anything, so be nice with the returned values - internal IEnumerable GetAllCached(Func?> performGetAll) + /// + public override TEntity? GetCached(TId id) + { + // get all from the cache -- and only the cache, then look for the entity + DeepCloneableList? all = Cache.GetCacheItem>(GetEntityTypeCacheKey()); + TEntity? entity = all?.FirstOrDefault(x => _entityGetId(x)?.Equals(id) ?? false); + + // see note in InsertEntities - what we get here is the original + // cached entity, not a clone, so we need to manually ensure it is deep-cloned. + return (TEntity?)entity?.DeepClone(); + } + + /// + public override bool Exists(TId id, Func performExits, Func?> performGetAll) + { + // get all as one set, then look for the entity + IEnumerable all = GetAllCached(performGetAll); + return all.Any(x => _entityGetId(x)?.Equals(id) ?? false); + } + + /// + public override TEntity[] GetAll(TId[]? ids, Func?> performGetAll) + { + // get all as one set, from cache if possible, else repo + IEnumerable all = GetAllCached(performGetAll); + + // if ids have been specified, filter + if (ids?.Length > 0) { - // try the cache first - var all = Cache.GetCacheItem>(GetEntityTypeCacheKey()); - if (all != null) return all.ToArray(); - - // else get from repo and cache - var entities = performGetAll(EmptyIds)?.WhereNotNull().ToArray(); - InsertEntities(entities); // may be an empty array... - return entities ?? Enumerable.Empty(); + all = all.Where(x => ids.Contains(_entityGetId(x))); } - /// - public override void ClearAll() + // and return + // see note in SetCacheActionToInsertEntities - what we get here is the original + // cached entities, not clones, so we need to manually ensure they are deep-cloned. + return all.Select(x => (TEntity)x.DeepClone()).ToArray(); + } + + // does NOT clone anything, so be nice with the returned values + internal IEnumerable GetAllCached(Func?> performGetAll) + { + // try the cache first + DeepCloneableList? all = Cache.GetCacheItem>(GetEntityTypeCacheKey()); + if (all != null) { - Cache.Clear(GetEntityTypeCacheKey()); + return all.ToArray(); } + + // else get from repo and cache + TEntity[]? entities = performGetAll(EmptyIds)?.WhereNotNull().ToArray(); + InsertEntities(entities); // may be an empty array... + return entities ?? Enumerable.Empty(); } + + /// + public override void ClearAll() => Cache.Clear(GetEntityTypeCacheKey()); } diff --git a/src/Umbraco.Infrastructure/Cache/RepositoryCachePolicyBase.cs b/src/Umbraco.Infrastructure/Cache/RepositoryCachePolicyBase.cs index 900ff02921a8..ee92db3150d8 100644 --- a/src/Umbraco.Infrastructure/Cache/RepositoryCachePolicyBase.cs +++ b/src/Umbraco.Infrastructure/Cache/RepositoryCachePolicyBase.cs @@ -1,72 +1,73 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Scoping; +using IScope = Umbraco.Cms.Infrastructure.Scoping.IScope; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// A base class for repository cache policies. +/// +/// The type of the entity. +/// The type of the identifier. +public abstract class RepositoryCachePolicyBase : IRepositoryCachePolicy + where TEntity : class, IEntity { - /// - /// A base class for repository cache policies. - /// - /// The type of the entity. - /// The type of the identifier. - public abstract class RepositoryCachePolicyBase : IRepositoryCachePolicy - where TEntity : class, IEntity - { - private readonly IAppPolicyCache _globalCache; - private readonly IScopeAccessor _scopeAccessor; + private readonly IAppPolicyCache _globalCache; + private readonly IScopeAccessor _scopeAccessor; - protected RepositoryCachePolicyBase(IAppPolicyCache globalCache, IScopeAccessor scopeAccessor) - { - _globalCache = globalCache ?? throw new ArgumentNullException(nameof(globalCache)); - _scopeAccessor = scopeAccessor ?? throw new ArgumentNullException(nameof(scopeAccessor)); - } + protected RepositoryCachePolicyBase(IAppPolicyCache globalCache, IScopeAccessor scopeAccessor) + { + _globalCache = globalCache ?? throw new ArgumentNullException(nameof(globalCache)); + _scopeAccessor = scopeAccessor ?? throw new ArgumentNullException(nameof(scopeAccessor)); + } - protected IAppPolicyCache Cache + protected IAppPolicyCache Cache + { + get { - get + IScope? ambientScope = _scopeAccessor.AmbientScope; + switch (ambientScope?.RepositoryCacheMode) { - var ambientScope = _scopeAccessor.AmbientScope; - switch (ambientScope?.RepositoryCacheMode) - { - case RepositoryCacheMode.Default: - return _globalCache; - case RepositoryCacheMode.Scoped: - return ambientScope.IsolatedCaches.GetOrCreate(); - case RepositoryCacheMode.None: - return NoAppCache.Instance; - default: - throw new NotSupportedException($"Repository cache mode {ambientScope?.RepositoryCacheMode} is not supported."); - } + case RepositoryCacheMode.Default: + return _globalCache; + case RepositoryCacheMode.Scoped: + return ambientScope.IsolatedCaches.GetOrCreate(); + case RepositoryCacheMode.None: + return NoAppCache.Instance; + default: + throw new NotSupportedException( + $"Repository cache mode {ambientScope?.RepositoryCacheMode} is not supported."); } } + } - /// - public abstract TEntity? Get(TId? id, Func performGet, Func?> performGetAll); + /// + public abstract TEntity? Get(TId? id, Func performGet, + Func?> performGetAll); - /// - public abstract TEntity? GetCached(TId id); + /// + public abstract TEntity? GetCached(TId id); - /// - public abstract bool Exists(TId id, Func performExists, Func?> performGetAll); + /// + public abstract bool Exists(TId id, Func performExists, + Func?> performGetAll); - /// - public abstract void Create(TEntity entity, Action persistNew); + /// + public abstract void Create(TEntity entity, Action persistNew); - /// - public abstract void Update(TEntity entity, Action persistUpdated); + /// + public abstract void Update(TEntity entity, Action persistUpdated); - /// - public abstract void Delete(TEntity entity, Action persistDeleted); + /// + public abstract void Delete(TEntity entity, Action persistDeleted); - /// - public abstract TEntity[] GetAll(TId[]? ids, Func?> performGetAll); + /// + public abstract TEntity[] GetAll(TId[]? ids, Func?> performGetAll); - /// - public abstract void ClearAll(); - } + /// + public abstract void ClearAll(); } diff --git a/src/Umbraco.Infrastructure/Cache/SingleItemsOnlyRepositoryCachePolicy.cs b/src/Umbraco.Infrastructure/Cache/SingleItemsOnlyRepositoryCachePolicy.cs index d23960c90328..cf5978b77a84 100644 --- a/src/Umbraco.Infrastructure/Cache/SingleItemsOnlyRepositoryCachePolicy.cs +++ b/src/Umbraco.Infrastructure/Cache/SingleItemsOnlyRepositoryCachePolicy.cs @@ -2,31 +2,33 @@ // See LICENSE for more details. using Umbraco.Cms.Core.Models.Entities; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Scoping; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// Represents a special policy that does not cache the result of GetAll. +/// +/// The type of the entity. +/// The type of the identifier. +/// +/// +/// Overrides the default repository cache policy and does not writes the result of GetAll +/// to cache, but only the result of individual Gets. It does read the cache for GetAll, though. +/// +/// Used by DictionaryRepository. +/// +internal class SingleItemsOnlyRepositoryCachePolicy : DefaultRepositoryCachePolicy + where TEntity : class, IEntity { - /// - /// Represents a special policy that does not cache the result of GetAll. - /// - /// The type of the entity. - /// The type of the identifier. - /// - /// Overrides the default repository cache policy and does not writes the result of GetAll - /// to cache, but only the result of individual Gets. It does read the cache for GetAll, though. - /// Used by DictionaryRepository. - /// - internal class SingleItemsOnlyRepositoryCachePolicy : DefaultRepositoryCachePolicy - where TEntity : class, IEntity + public SingleItemsOnlyRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, + RepositoryCachePolicyOptions options) + : base(cache, scopeAccessor, options) { - public SingleItemsOnlyRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, RepositoryCachePolicyOptions options) - : base(cache, scopeAccessor, options) - { } + } - protected override void InsertEntities(TId[]? ids, TEntity[]? entities) - { - // nop - } + protected override void InsertEntities(TId[]? ids, TEntity[]? entities) + { + // nop } } diff --git a/src/Umbraco.Infrastructure/Configuration/JsonConfigManipulator.cs b/src/Umbraco.Infrastructure/Configuration/JsonConfigManipulator.cs index 48c0540c67de..1503031aea12 100644 --- a/src/Umbraco.Infrastructure/Configuration/JsonConfigManipulator.cs +++ b/src/Umbraco.Infrastructure/Configuration/JsonConfigManipulator.cs @@ -1,5 +1,3 @@ -using System; -using System.IO; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.Json; using Microsoft.Extensions.DependencyInjection; @@ -10,305 +8,313 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.Configuration +namespace Umbraco.Cms.Core.Configuration; + +public class JsonConfigManipulator : IConfigManipulator { - public class JsonConfigManipulator : IConfigManipulator + private readonly IConfiguration _configuration; + private readonly object _locker = new(); + private readonly ILogger _logger; + + [Obsolete] + public JsonConfigManipulator(IConfiguration configuration) + : this(configuration, StaticServiceProvider.Instance.GetRequiredService>()) { - private readonly IConfiguration _configuration; - private readonly ILogger _logger; - private readonly object _locker = new object(); - - [Obsolete] - public JsonConfigManipulator(IConfiguration configuration) - : this(configuration, StaticServiceProvider.Instance.GetRequiredService>()) - { } - - public JsonConfigManipulator( - IConfiguration configuration, - ILogger logger) - { - _configuration = configuration; - _logger = logger; - } + } - public string UmbracoConnectionPath { get; } = $"ConnectionStrings:{Cms.Core.Constants.System.UmbracoConnectionName}"; - public void RemoveConnectionString() - { - var provider = GetJsonConfigurationProvider(UmbracoConnectionPath); + public JsonConfigManipulator( + IConfiguration configuration, + ILogger logger) + { + _configuration = configuration; + _logger = logger; + } - var json = GetJson(provider); - if (json is null) - { - _logger.LogWarning("Failed to remove connection string from JSON configuration."); - return; - } + public string UmbracoConnectionPath { get; } = $"ConnectionStrings:{Constants.System.UmbracoConnectionName}"; - RemoveJsonKey(json, UmbracoConnectionPath); + public void RemoveConnectionString() + { + JsonConfigurationProvider provider = GetJsonConfigurationProvider(UmbracoConnectionPath); - SaveJson(provider, json); + JObject? json = GetJson(provider); + if (json is null) + { + _logger.LogWarning("Failed to remove connection string from JSON configuration."); + return; } - public void SaveConnectionString(string connectionString, string? providerName) - { - var provider = GetJsonConfigurationProvider(); + RemoveJsonKey(json, UmbracoConnectionPath); - var json = GetJson(provider); - if (json is null) - { - _logger.LogWarning("Failed to save connection string in JSON configuration."); - return; - } - - var item = GetConnectionItem(connectionString, providerName); + SaveJson(provider, json); + } - if (item is not null) - { - json?.Merge(item, new JsonMergeSettings()); - } + public void SaveConnectionString(string connectionString, string? providerName) + { + JsonConfigurationProvider provider = GetJsonConfigurationProvider(); - SaveJson(provider, json); + JObject? json = GetJson(provider); + if (json is null) + { + _logger.LogWarning("Failed to save connection string in JSON configuration."); + return; } + JToken? item = GetConnectionItem(connectionString, providerName); - public void SaveConfigValue(string key, object value) + if (item is not null) { - var provider = GetJsonConfigurationProvider(); + json?.Merge(item, new JsonMergeSettings()); + } - var json = GetJson(provider); - if (json is null) - { - _logger.LogWarning("Failed to save configuration key \"{Key}\" in JSON configuration.", key); - return; - } + SaveJson(provider, json); + } - JToken? token = json; - foreach (var propertyName in key.Split(new[] { ':' })) - { - if (token is null) - break; - token = CaseSelectPropertyValues(token, propertyName); - } - if (token is null) - return; + public void SaveConfigValue(string key, object value) + { + JsonConfigurationProvider provider = GetJsonConfigurationProvider(); - var writer = new JTokenWriter(); - writer.WriteValue(value); + JObject? json = GetJson(provider); + if (json is null) + { + _logger.LogWarning("Failed to save configuration key \"{Key}\" in JSON configuration.", key); + return; + } - if (writer.Token is not null) + JToken? token = json; + foreach (var propertyName in key.Split(new[] {':'})) + { + if (token is null) { - token.Replace(writer.Token); + break; } - SaveJson(provider, json); - + token = CaseSelectPropertyValues(token, propertyName); } - public void SaveDisableRedirectUrlTracking(bool disable) + if (token is null) { - var provider = GetJsonConfigurationProvider(); + return; + } - var json = GetJson(provider); - if (json is null) - { - _logger.LogWarning("Failed to save enabled/disabled state for redirect URL tracking in JSON configuration."); - return; - } + var writer = new JTokenWriter(); + writer.WriteValue(value); - var item = GetDisableRedirectUrlItem(disable); + if (writer.Token is not null) + { + token.Replace(writer.Token); + } - if (item is not null) - { - json?.Merge(item, new JsonMergeSettings()); - } + SaveJson(provider, json); + } - SaveJson(provider, json); - } + public void SaveDisableRedirectUrlTracking(bool disable) + { + JsonConfigurationProvider provider = GetJsonConfigurationProvider(); - public void SetGlobalId(string id) + JObject? json = GetJson(provider); + if (json is null) { - var provider = GetJsonConfigurationProvider(); + _logger.LogWarning( + "Failed to save enabled/disabled state for redirect URL tracking in JSON configuration."); + return; + } - var json = GetJson(provider); - if (json is null) - { - _logger.LogWarning("Failed to save global identifier in JSON configuration."); - return; - } + JToken? item = GetDisableRedirectUrlItem(disable); - var item = GetGlobalIdItem(id); + if (item is not null) + { + json?.Merge(item, new JsonMergeSettings()); + } - if (item is not null) - { - json?.Merge(item, new JsonMergeSettings()); - } + SaveJson(provider, json); + } - SaveJson(provider, json); - } + public void SetGlobalId(string id) + { + JsonConfigurationProvider provider = GetJsonConfigurationProvider(); - private object? GetGlobalIdItem(string id) + JObject? json = GetJson(provider); + if (json is null) { - JTokenWriter writer = new JTokenWriter(); - - writer.WriteStartObject(); - writer.WritePropertyName("Umbraco"); - writer.WriteStartObject(); - writer.WritePropertyName("CMS"); - writer.WriteStartObject(); - writer.WritePropertyName("Global"); - writer.WriteStartObject(); - writer.WritePropertyName("Id"); - writer.WriteValue(id); - writer.WriteEndObject(); - writer.WriteEndObject(); - writer.WriteEndObject(); - writer.WriteEndObject(); - - return writer.Token; + _logger.LogWarning("Failed to save global identifier in JSON configuration."); + return; } - private JToken? GetDisableRedirectUrlItem(bool value) - { - JTokenWriter writer = new JTokenWriter(); - - writer.WriteStartObject(); - writer.WritePropertyName("Umbraco"); - writer.WriteStartObject(); - writer.WritePropertyName("CMS"); - writer.WriteStartObject(); - writer.WritePropertyName("WebRouting"); - writer.WriteStartObject(); - writer.WritePropertyName("DisableRedirectUrlTracking"); - writer.WriteValue(value); - writer.WriteEndObject(); - writer.WriteEndObject(); - writer.WriteEndObject(); - writer.WriteEndObject(); - - return writer.Token; - } + var item = GetGlobalIdItem(id); - private JToken? GetConnectionItem(string connectionString, string? providerName) + if (item is not null) { - JTokenWriter writer = new JTokenWriter(); - - writer.WriteStartObject(); - writer.WritePropertyName("ConnectionStrings"); - writer.WriteStartObject(); - writer.WritePropertyName(Constants.System.UmbracoConnectionName); - writer.WriteValue(connectionString); - writer.WritePropertyName($"{Constants.System.UmbracoConnectionName}{ConnectionStrings.ProviderNamePostfix}"); - writer.WriteValue(providerName); - writer.WriteEndObject(); - writer.WriteEndObject(); - - return writer.Token; + json?.Merge(item, new JsonMergeSettings()); } - private static void RemoveJsonKey(JObject? json, string key) - { - JToken? token = json; - foreach (var propertyName in key.Split(new[] { ':' })) - { - token = CaseSelectPropertyValues(token, propertyName); - } + SaveJson(provider, json); + } + + private object? GetGlobalIdItem(string id) + { + var writer = new JTokenWriter(); + + writer.WriteStartObject(); + writer.WritePropertyName("Umbraco"); + writer.WriteStartObject(); + writer.WritePropertyName("CMS"); + writer.WriteStartObject(); + writer.WritePropertyName("Global"); + writer.WriteStartObject(); + writer.WritePropertyName("Id"); + writer.WriteValue(id); + writer.WriteEndObject(); + writer.WriteEndObject(); + writer.WriteEndObject(); + writer.WriteEndObject(); + + return writer.Token; + } + + private JToken? GetDisableRedirectUrlItem(bool value) + { + var writer = new JTokenWriter(); + + writer.WriteStartObject(); + writer.WritePropertyName("Umbraco"); + writer.WriteStartObject(); + writer.WritePropertyName("CMS"); + writer.WriteStartObject(); + writer.WritePropertyName("WebRouting"); + writer.WriteStartObject(); + writer.WritePropertyName("DisableRedirectUrlTracking"); + writer.WriteValue(value); + writer.WriteEndObject(); + writer.WriteEndObject(); + writer.WriteEndObject(); + writer.WriteEndObject(); + + return writer.Token; + } - token?.Parent?.Remove(); + private JToken? GetConnectionItem(string connectionString, string? providerName) + { + var writer = new JTokenWriter(); + + writer.WriteStartObject(); + writer.WritePropertyName("ConnectionStrings"); + writer.WriteStartObject(); + writer.WritePropertyName(Constants.System.UmbracoConnectionName); + writer.WriteValue(connectionString); + writer.WritePropertyName($"{Constants.System.UmbracoConnectionName}{ConnectionStrings.ProviderNamePostfix}"); + writer.WriteValue(providerName); + writer.WriteEndObject(); + writer.WriteEndObject(); + + return writer.Token; + } + + private static void RemoveJsonKey(JObject? json, string key) + { + JToken? token = json; + foreach (var propertyName in key.Split(new[] {':'})) + { + token = CaseSelectPropertyValues(token, propertyName); } - private void SaveJson(JsonConfigurationProvider provider, JObject? json) + token?.Parent?.Remove(); + } + + private void SaveJson(JsonConfigurationProvider provider, JObject? json) + { + lock (_locker) { - lock (_locker) + if (provider.Source.FileProvider is PhysicalFileProvider physicalFileProvider) { - if (provider.Source.FileProvider is PhysicalFileProvider physicalFileProvider) - { - var jsonFilePath = Path.Combine(physicalFileProvider.Root, provider.Source.Path); + var jsonFilePath = Path.Combine(physicalFileProvider.Root, provider.Source.Path); - try - { - using (var sw = new StreamWriter(jsonFilePath, false)) - using (var jsonTextWriter = new JsonTextWriter(sw) - { - Formatting = Formatting.Indented, - }) - { - json?.WriteTo(jsonTextWriter); - } - } - catch (IOException exception) + try + { + using (var sw = new StreamWriter(jsonFilePath, false)) + using (var jsonTextWriter = new JsonTextWriter(sw) {Formatting = Formatting.Indented}) { - _logger.LogWarning(exception, "JSON configuration could not be written: {path}", jsonFilePath); + json?.WriteTo(jsonTextWriter); } } + catch (IOException exception) + { + _logger.LogWarning(exception, "JSON configuration could not be written: {path}", jsonFilePath); + } } } + } - private JObject? GetJson(JsonConfigurationProvider provider) + private JObject? GetJson(JsonConfigurationProvider provider) + { + lock (_locker) { - lock (_locker) + if (provider.Source.FileProvider is not PhysicalFileProvider physicalFileProvider) { - if (provider.Source.FileProvider is not PhysicalFileProvider physicalFileProvider) - { - return null; - } + return null; + } - var jsonFilePath = Path.Combine(physicalFileProvider.Root, provider.Source.Path); + var jsonFilePath = Path.Combine(physicalFileProvider.Root, provider.Source.Path); - try - { - var serializer = new JsonSerializer(); - using var sr = new StreamReader(jsonFilePath); - using var jsonTextReader = new JsonTextReader(sr); - return serializer.Deserialize(jsonTextReader); - } - catch (IOException exception) - { - _logger.LogWarning(exception, "JSON configuration could not be read: {path}", jsonFilePath); - return null; - } + try + { + var serializer = new JsonSerializer(); + using var sr = new StreamReader(jsonFilePath); + using var jsonTextReader = new JsonTextReader(sr); + return serializer.Deserialize(jsonTextReader); + } + catch (IOException exception) + { + _logger.LogWarning(exception, "JSON configuration could not be read: {path}", jsonFilePath); + return null; } } + } - private JsonConfigurationProvider GetJsonConfigurationProvider(string? requiredKey = null) + private JsonConfigurationProvider GetJsonConfigurationProvider(string? requiredKey = null) + { + if (_configuration is IConfigurationRoot configurationRoot) { - if (_configuration is IConfigurationRoot configurationRoot) + foreach (IConfigurationProvider? provider in configurationRoot.Providers) { - foreach (var provider in configurationRoot.Providers) + if (provider is JsonConfigurationProvider jsonConfigurationProvider) { - if (provider is JsonConfigurationProvider jsonConfigurationProvider) + if (requiredKey is null || provider.TryGet(requiredKey, out _)) { - if (requiredKey is null || provider.TryGet(requiredKey, out _)) - { - return jsonConfigurationProvider; - } + return jsonConfigurationProvider; } } } - throw new InvalidOperationException("Could not find a writable json config source"); } - /// - /// Returns the property value when case insensative - /// - /// - /// This method is required because keys are case insensative in IConfiguration. - /// JObject[..] do not support case insensative and JObject.Property(...) do not return a new JObject. - /// - private static JToken? CaseSelectPropertyValues(JToken? token, string name) + throw new InvalidOperationException("Could not find a writable json config source"); + } + + /// + /// Returns the property value when case insensative + /// + /// + /// This method is required because keys are case insensative in IConfiguration. + /// JObject[..] do not support case insensative and JObject.Property(...) do not return a new JObject. + /// + private static JToken? CaseSelectPropertyValues(JToken? token, string name) + { + if (token is JObject obj) { - if (token is JObject obj) + foreach (JProperty property in obj.Properties()) { + if (name is null) + { + return property.Value; + } - foreach (var property in obj.Properties()) + if (string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase)) { - if (name is null) - return property.Value; - if (string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase)) - return property.Value; + return property.Value; } } - return null; } + return null; } } diff --git a/src/Umbraco.Infrastructure/Configuration/NCronTabParser.cs b/src/Umbraco.Infrastructure/Configuration/NCronTabParser.cs index fe2114163661..09cd2282da62 100644 --- a/src/Umbraco.Infrastructure/Configuration/NCronTabParser.cs +++ b/src/Umbraco.Infrastructure/Configuration/NCronTabParser.cs @@ -1,24 +1,20 @@ -using System; using NCrontab; -using Umbraco.Cms.Core.Configuration; -namespace Umbraco.Cms.Core.Configuration +namespace Umbraco.Cms.Core.Configuration; + +public class NCronTabParser : ICronTabParser { - public class NCronTabParser : ICronTabParser + public bool IsValidCronTab(string cronTab) { - public bool IsValidCronTab(string cronTab) - { - var result = CrontabSchedule.TryParse(cronTab); + var result = CrontabSchedule.TryParse(cronTab); - return !(result is null); - } + return !(result is null); + } - public DateTime GetNextOccurrence(string cronTab, DateTime time) - { - var result = CrontabSchedule.Parse(cronTab); + public DateTime GetNextOccurrence(string cronTab, DateTime time) + { + var result = CrontabSchedule.Parse(cronTab); - return result.GetNextOccurrence(time); - } + return result.GetNextOccurrence(time); } - } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Collections.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Collections.cs index 7091b89cadca..094bbe58f6e0 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Collections.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Collections.cs @@ -3,30 +3,28 @@ using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Mappers; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods to the class. +/// +public static partial class UmbracoBuilderExtensions { /// - /// Provides extension methods to the class. + /// Gets the mappers collection builder. /// - public static partial class UmbracoBuilderExtensions - { - /// - /// Gets the mappers collection builder. - /// - /// The builder. - public static MapperCollectionBuilder? Mappers(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - public static NPocoMapperCollectionBuilder? NPocoMappers(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); + /// The builder. + public static MapperCollectionBuilder? Mappers(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + public static NPocoMapperCollectionBuilder? NPocoMappers(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); - /// - /// Gets the package migration plans collection builder. - /// - /// The builder. - public static PackageMigrationPlanCollectionBuilder? PackageMigrationPlans(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - } + /// + /// Gets the package migration plans collection builder. + /// + /// The builder. + public static PackageMigrationPlanCollectionBuilder? PackageMigrationPlans(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 05eda0e94d14..798c13109a14 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Serilog; +using SixLabors.ImageSharp; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration; @@ -55,335 +56,343 @@ using Umbraco.Cms.Infrastructure.Serialization; using Umbraco.Cms.Infrastructure.Services.Implement; using Umbraco.Extensions; +using IScopeProvider = Umbraco.Cms.Infrastructure.Scoping.IScopeProvider; -namespace Umbraco.Cms.Infrastructure.DependencyInjection +namespace Umbraco.Cms.Infrastructure.DependencyInjection; + +public static partial class UmbracoBuilderExtensions { - public static partial class UmbracoBuilderExtensions + /// + /// Adds all core Umbraco services required to run which may be replaced later in the pipeline + /// + public static IUmbracoBuilder AddCoreInitialServices(this IUmbracoBuilder builder) { - /// - /// Adds all core Umbraco services required to run which may be replaced later in the pipeline - /// - public static IUmbracoBuilder AddCoreInitialServices(this IUmbracoBuilder builder) - { - builder - .AddMainDom() - .AddLogging(); + builder + .AddMainDom() + .AddLogging(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(factory => factory.GetRequiredService().SqlContext); - builder.NPocoMappers()?.Add(); - builder.PackageMigrationPlans()?.Add(() => builder.TypeLoader.GetPackageMigrationPlans()); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(factory => factory.GetRequiredService().SqlContext); + builder.NPocoMappers()?.Add(); + builder.PackageMigrationPlans()?.Add(() => builder.TypeLoader.GetPackageMigrationPlans()); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.AddNotificationAsyncHandler(); - builder.AddNotificationAsyncHandler(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.AddNotificationAsyncHandler(); + builder.AddNotificationAsyncHandler(); - // composers - builder - .AddRepositories() - .AddServices() - .AddCoreMappingProfiles() - .AddFileSystems() - .AddWebAssets(); + // composers + builder + .AddRepositories() + .AddServices() + .AddCoreMappingProfiles() + .AddFileSystems() + .AddWebAssets(); - // register persistence mappers - required by database factory so needs to be done here - // means the only place the collection can be modified is in a runtime - afterwards it - // has been frozen and it is too late - builder.Mappers()?.AddCoreMappers(); + // register persistence mappers - required by database factory so needs to be done here + // means the only place the collection can be modified is in a runtime - afterwards it + // has been frozen and it is too late + builder.Mappers()?.AddCoreMappers(); - // register the scope provider - builder.Services.AddSingleton(); // implements IScopeProvider, IScopeAccessor - builder.Services.AddSingleton(f => f.GetRequiredService()); - builder.Services.AddSingleton(f => f.GetRequiredService()); - builder.Services.AddSingleton(f => f.GetRequiredService()); - builder.Services.AddSingleton(f => f.GetRequiredService()); + // register the scope provider + builder.Services.AddSingleton(); // implements IScopeProvider, IScopeAccessor + builder.Services.AddSingleton(f => f.GetRequiredService()); + builder.Services.AddSingleton(f => f.GetRequiredService()); + builder.Services.AddSingleton(f => f.GetRequiredService()); + builder.Services.AddSingleton(f => f.GetRequiredService()); - builder.Services.AddScoped(); + builder.Services.AddScoped(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); - // register database builder - // *not* a singleton, don't want to keep it around - builder.Services.AddTransient(); + // register database builder + // *not* a singleton, don't want to keep it around + builder.Services.AddTransient(); - // register manifest parser, will be injected in collection builders where needed - builder.Services.AddSingleton(); + // register manifest parser, will be injected in collection builders where needed + builder.Services.AddSingleton(); - // register the manifest filter collection builder (collection is empty by default) - builder.ManifestFilters(); + // register the manifest filter collection builder (collection is empty by default) + builder.ManifestFilters(); - builder.MediaUrlGenerators()? - .Add() - .Add(); + builder.MediaUrlGenerators()? + .Add() + .Add(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); - builder.Services.AddSingleton(factory - => new DefaultShortStringHelper(new DefaultShortStringHelperConfig().WithDefault(factory.GetRequiredService>().CurrentValue))); + builder.Services.AddSingleton(factory + => new DefaultShortStringHelper(new DefaultShortStringHelperConfig().WithDefault( + factory.GetRequiredService>().CurrentValue))); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(factory => new MigrationBuilder(factory)); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(factory => new MigrationBuilder(factory)); - builder.AddPreValueMigrators(); + builder.AddPreValueMigrators(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); - // register the published snapshot accessor - the "current" published snapshot is in the umbraco context - builder.Services.AddSingleton(); + // register the published snapshot accessor - the "current" published snapshot is in the umbraco context + builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); - // Config manipulator - builder.Services.AddSingleton(); + // Config manipulator + builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); - // both TinyMceValueConverter (in Core) and RteMacroRenderingValueConverter (in Web) will be - // discovered when CoreBootManager configures the converters. We will remove the basic one defined - // in core so that the more enhanced version is active. - builder.PropertyValueConverters()? - .Remove(); + // both TinyMceValueConverter (in Core) and RteMacroRenderingValueConverter (in Web) will be + // discovered when CoreBootManager configures the converters. We will remove the basic one defined + // in core so that the more enhanced version is active. + builder.PropertyValueConverters()? + .Remove(); - // register *all* checks, except those marked [HideFromTypeFinder] of course - builder.Services.AddSingleton(); + // register *all* checks, except those marked [HideFromTypeFinder] of course + builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); - builder.Services.AddScoped(); + builder.Services.AddScoped(); - // replace - builder.Services.AddSingleton( - services => new EmailSender( - services.GetRequiredService>(), - services.GetRequiredService>(), - services.GetRequiredService(), - services.GetService>(), - services.GetService>())); + // replace + builder.Services.AddSingleton( + services => new EmailSender( + services.GetRequiredService>(), + services.GetRequiredService>(), + services.GetRequiredService(), + services.GetService>(), + services.GetService>())); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); - builder.Services.AddScoped(); + builder.Services.AddScoped(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(sp => - new PublishedContentQueryAccessor(sp.GetRequiredService()) - ); - builder.Services.AddScoped(factory => - { - var umbCtx = factory.GetRequiredService(); - var umbracoContext = umbCtx.GetRequiredUmbracoContext(); - return new PublishedContentQuery(umbracoContext.PublishedSnapshot, factory.GetRequiredService(), factory.GetRequiredService()); - }); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(sp => + new PublishedContentQueryAccessor(sp.GetRequiredService()) + ); + builder.Services.AddScoped(factory => + { + IUmbracoContextAccessor umbCtx = factory.GetRequiredService(); + IUmbracoContext umbracoContext = umbCtx.GetRequiredUmbracoContext(); + return new PublishedContentQuery(umbracoContext.PublishedSnapshot, + factory.GetRequiredService(), factory.GetRequiredService()); + }); - // register accessors for cultures - builder.Services.AddSingleton(); + // register accessors for cultures + builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); - // Add default ImageSharp configuration and service implementations - builder.Services.AddSingleton(SixLabors.ImageSharp.Configuration.Default); - builder.Services.AddSingleton(); + // Add default ImageSharp configuration and service implementations + builder.Services.AddSingleton(Configuration.Default); + builder.Services.AddSingleton(); - builder.Services.AddTransient(); - builder.AddInstaller(); + builder.Services.AddTransient(); + builder.AddInstaller(); - // Services required to run background jobs (with out the handler) - builder.Services.AddSingleton(); - return builder; - } + // Services required to run background jobs (with out the handler) + builder.Services.AddSingleton(); + return builder; + } - /// - /// Adds logging requirements for Umbraco - /// - private static IUmbracoBuilder AddLogging(this IUmbracoBuilder builder) - { - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - return builder; - } - - private static IUmbracoBuilder AddMainDom(this IUmbracoBuilder builder) + /// + /// Adds logging requirements for Umbraco + /// + private static IUmbracoBuilder AddLogging(this IUmbracoBuilder builder) + { + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + return builder; + } + + private static IUmbracoBuilder AddMainDom(this IUmbracoBuilder builder) + { + builder.Services.AddSingleton(); + builder.Services.AddSingleton(factory => { - builder.Services.AddSingleton(); - builder.Services.AddSingleton(factory => + IOptions globalSettings = factory.GetRequiredService>(); + IOptionsMonitor connectionStrings = + factory.GetRequiredService>(); + IHostingEnvironment hostingEnvironment = factory.GetRequiredService(); + + IDbProviderFactoryCreator dbCreator = factory.GetRequiredService(); + DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory = + factory.GetRequiredService(); + ILoggerFactory loggerFactory = factory.GetRequiredService(); + NPocoMapperCollection npocoMappers = factory.GetRequiredService(); + IMainDomKeyGenerator mainDomKeyGenerator = factory.GetRequiredService(); + + switch (globalSettings.Value.MainDomLock) { - var globalSettings = factory.GetRequiredService>(); - var connectionStrings = factory.GetRequiredService>(); - var hostingEnvironment = factory.GetRequiredService(); - - var dbCreator = factory.GetRequiredService(); - var databaseSchemaCreatorFactory = factory.GetRequiredService(); - var loggerFactory = factory.GetRequiredService(); - var npocoMappers = factory.GetRequiredService(); - var mainDomKeyGenerator = factory.GetRequiredService(); - - switch (globalSettings.Value.MainDomLock) - { - case "SqlMainDomLock": - return new SqlMainDomLock( - loggerFactory, - globalSettings, - connectionStrings, - dbCreator, - mainDomKeyGenerator, - databaseSchemaCreatorFactory, - npocoMappers); - - case "MainDomSemaphoreLock": - return new MainDomSemaphoreLock(loggerFactory.CreateLogger(), hostingEnvironment); - - case "FileSystemMainDomLock": - default: - return new FileSystemMainDomLock(loggerFactory.CreateLogger(), mainDomKeyGenerator, hostingEnvironment, factory.GetRequiredService>()); - } - }); - - return builder; - } - - - private static IUmbracoBuilder AddPreValueMigrators(this IUmbracoBuilder builder) - { - builder.WithCollectionBuilder()? - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append(); - - return builder; - } - - public static IUmbracoBuilder AddLogViewer(this IUmbracoBuilder builder) - { - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.SetLogViewer(); - builder.Services.AddSingleton(factory => new SerilogJsonLogViewer(factory.GetRequiredService>(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - Log.Logger)); - - return builder; - } - - public static IUmbracoBuilder AddCoreNotifications(this IUmbracoBuilder builder) - { - // add handlers for sending user notifications (i.e. emails) - builder.Services.AddSingleton(); - builder - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler(); - - // add handlers for building content relations - builder - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler(); - - // add notification handlers for property editors - builder - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler(); - - // add notification handlers for redirect tracking - builder - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler(); - - // Add notification handlers for DistributedCache - builder - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - ; - // add notification handlers for auditing - builder - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler(); - - return builder; - } + case "SqlMainDomLock": + return new SqlMainDomLock( + loggerFactory, + globalSettings, + connectionStrings, + dbCreator, + mainDomKeyGenerator, + databaseSchemaCreatorFactory, + npocoMappers); + + case "MainDomSemaphoreLock": + return new MainDomSemaphoreLock(loggerFactory.CreateLogger(), + hostingEnvironment); + + case "FileSystemMainDomLock": + default: + return new FileSystemMainDomLock(loggerFactory.CreateLogger(), + mainDomKeyGenerator, hostingEnvironment, + factory.GetRequiredService>()); + } + }); + + return builder; + } + + + private static IUmbracoBuilder AddPreValueMigrators(this IUmbracoBuilder builder) + { + builder.WithCollectionBuilder()? + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append(); + + return builder; + } + + public static IUmbracoBuilder AddLogViewer(this IUmbracoBuilder builder) + { + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.SetLogViewer(); + builder.Services.AddSingleton(factory => new SerilogJsonLogViewer( + factory.GetRequiredService>(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + Log.Logger)); + + return builder; + } + + public static IUmbracoBuilder AddCoreNotifications(this IUmbracoBuilder builder) + { + // add handlers for sending user notifications (i.e. emails) + builder.Services.AddSingleton(); + builder + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler(); + + // add handlers for building content relations + builder + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler(); + + // add notification handlers for property editors + builder + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler(); + + // add notification handlers for redirect tracking + builder + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler(); + + // Add notification handlers for DistributedCache + builder + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + ; + // add notification handlers for auditing + builder + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler(); + + return builder; } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.DistributedCache.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.DistributedCache.cs index 71ea85d80f85..35afe2358bf0 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.DistributedCache.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.DistributedCache.cs @@ -1,4 +1,3 @@ -using System; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.DependencyInjection; @@ -7,96 +6,99 @@ using Umbraco.Cms.Infrastructure.Sync; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.DependencyInjection +namespace Umbraco.Cms.Infrastructure.DependencyInjection; + +/// +/// Provides extension methods to the class. +/// +public static partial class UmbracoBuilderExtensions { /// - /// Provides extension methods to the class. + /// Adds distributed cache support /// - public static partial class UmbracoBuilderExtensions + /// + /// This is still required for websites that are not load balancing because this ensures that sites hosted + /// with managed hosts like IIS/etc... work correctly when AppDomains are running in parallel. + /// + public static IUmbracoBuilder AddDistributedCache(this IUmbracoBuilder builder) { - /// - /// Adds distributed cache support - /// - /// - /// This is still required for websites that are not load balancing because this ensures that sites hosted - /// with managed hosts like IIS/etc... work correctly when AppDomains are running in parallel. - /// - public static IUmbracoBuilder AddDistributedCache(this IUmbracoBuilder builder) - { - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.SetServerMessenger(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - return builder; - } + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.SetServerMessenger(); + builder + .AddNotificationHandler(); + builder.AddNotificationHandler(); + return builder; + } - /// - /// Sets the server registrar. - /// - /// The type of the server registrar. - /// The builder. - public static IUmbracoBuilder SetServerRegistrar(this IUmbracoBuilder builder) - where T : class, IServerRoleAccessor - { - builder.Services.AddUnique(); - return builder; - } + /// + /// Sets the server registrar. + /// + /// The type of the server registrar. + /// The builder. + public static IUmbracoBuilder SetServerRegistrar(this IUmbracoBuilder builder) + where T : class, IServerRoleAccessor + { + builder.Services.AddUnique(); + return builder; + } - /// - /// Sets the server registrar. - /// - /// The builder. - /// A function creating a server registrar. - public static IUmbracoBuilder SetServerRegistrar(this IUmbracoBuilder builder, Func factory) - { - builder.Services.AddUnique(factory); - return builder; - } + /// + /// Sets the server registrar. + /// + /// The builder. + /// A function creating a server registrar. + public static IUmbracoBuilder SetServerRegistrar(this IUmbracoBuilder builder, + Func factory) + { + builder.Services.AddUnique(factory); + return builder; + } - /// - /// Sets the server registrar. - /// - /// The builder. - /// A server registrar. - public static IUmbracoBuilder SetServerRegistrar(this IUmbracoBuilder builder, IServerRoleAccessor registrar) - { - builder.Services.AddUnique(registrar); - return builder; - } + /// + /// Sets the server registrar. + /// + /// The builder. + /// A server registrar. + public static IUmbracoBuilder SetServerRegistrar(this IUmbracoBuilder builder, IServerRoleAccessor registrar) + { + builder.Services.AddUnique(registrar); + return builder; + } - /// - /// Sets the server messenger. - /// - /// The type of the server registrar. - /// The builder. - public static IUmbracoBuilder SetServerMessenger(this IUmbracoBuilder builder) - where T : class, IServerMessenger - { - builder.Services.AddUnique(); - return builder; - } + /// + /// Sets the server messenger. + /// + /// The type of the server registrar. + /// The builder. + public static IUmbracoBuilder SetServerMessenger(this IUmbracoBuilder builder) + where T : class, IServerMessenger + { + builder.Services.AddUnique(); + return builder; + } - /// - /// Sets the server messenger. - /// - /// The builder. - /// A function creating a server messenger. - public static IUmbracoBuilder SetServerMessenger(this IUmbracoBuilder builder, Func factory) - { - builder.Services.AddUnique(factory); - return builder; - } + /// + /// Sets the server messenger. + /// + /// The builder. + /// A function creating a server messenger. + public static IUmbracoBuilder SetServerMessenger(this IUmbracoBuilder builder, + Func factory) + { + builder.Services.AddUnique(factory); + return builder; + } - /// - /// Sets the server messenger. - /// - /// The builder. - /// A server messenger. - public static IUmbracoBuilder SetServerMessenger(this IUmbracoBuilder builder, IServerMessenger registrar) - { - builder.Services.AddUnique(registrar); - return builder; - } + /// + /// Sets the server messenger. + /// + /// The builder. + /// A server messenger. + public static IUmbracoBuilder SetServerMessenger(this IUmbracoBuilder builder, IServerMessenger registrar) + { + builder.Services.AddUnique(registrar); + return builder; } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Examine.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Examine.cs index da31a8df39dc..aabadc5197d4 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Examine.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Examine.cs @@ -1,7 +1,5 @@ using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.PropertyEditors; @@ -12,55 +10,54 @@ using Umbraco.Cms.Infrastructure.Search; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.DependencyInjection +namespace Umbraco.Cms.Infrastructure.DependencyInjection; + +/// +/// Provides extension methods to the class. +/// +public static partial class UmbracoBuilderExtensions { - /// - /// Provides extension methods to the class. - /// - public static partial class UmbracoBuilderExtensions + public static IUmbracoBuilder AddExamine(this IUmbracoBuilder builder) { - public static IUmbracoBuilder AddExamine(this IUmbracoBuilder builder) - { - // populators are not a collection: one cannot remove ours, and can only add more - // the container can inject IEnumerable and get them all - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + // populators are not a collection: one cannot remove ours, and can only add more + // the container can inject IEnumerable and get them all + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(factory => - new ContentValueSetBuilder( - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - true)); - builder.Services.AddUnique(factory => - new ContentValueSetBuilder( - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - false)); - builder.Services.AddUnique, MediaValueSetBuilder>(); - builder.Services.AddUnique, MemberValueSetBuilder>(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(factory => + new ContentValueSetBuilder( + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + true)); + builder.Services.AddUnique(factory => + new ContentValueSetBuilder( + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + false)); + builder.Services.AddUnique, MediaValueSetBuilder>(); + builder.Services.AddUnique, MemberValueSetBuilder>(); + builder.Services.AddSingleton(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); - builder.AddNotificationHandler(); + builder.AddNotificationHandler(); - return builder; - } + return builder; } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs index 310ae0b30296..e9564b02639a 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs @@ -3,58 +3,59 @@ using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.IO.MediaPathSchemes; using Umbraco.Extensions; -using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; -namespace Umbraco.Cms.Infrastructure.DependencyInjection +namespace Umbraco.Cms.Infrastructure.DependencyInjection; + +public static partial class UmbracoBuilderExtensions { - public static partial class UmbracoBuilderExtensions + /* + * HOW TO REPLACE THE MEDIA UNDERLYING FILESYSTEM + * ---------------------------------------------- + * + * Create an implementation of IFileSystem and register it as the underlying filesystem for + * MediaFileSystem with the following extension on composition. + * + * builder.SetMediaFileSystem(factory => FactoryMethodToReturnYourImplementation()) + * + * WHAT IS SHADOWING + * ----------------- + * + * Shadowing is the technology used for Deploy to implement some sort of + * transaction-management on top of filesystems. The plumbing explained above, + * compared to creating your own physical filesystem, ensures that your filesystem + * would participate into such transactions. + * + */ + + internal static IUmbracoBuilder AddFileSystems(this IUmbracoBuilder builder) { - /* - * HOW TO REPLACE THE MEDIA UNDERLYING FILESYSTEM - * ---------------------------------------------- - * - * Create an implementation of IFileSystem and register it as the underlying filesystem for - * MediaFileSystem with the following extension on composition. - * - * builder.SetMediaFileSystem(factory => FactoryMethodToReturnYourImplementation()) - * - * WHAT IS SHADOWING - * ----------------- - * - * Shadowing is the technology used for Deploy to implement some sort of - * transaction-management on top of filesystems. The plumbing explained above, - * compared to creating your own physical filesystem, ensures that your filesystem - * would participate into such transactions. - * - */ - - internal static IUmbracoBuilder AddFileSystems(this IUmbracoBuilder builder) - { - // register FileSystems, which manages all filesystems - builder.Services.AddSingleton(); + // register FileSystems, which manages all filesystems + builder.Services.AddSingleton(); - // register the scheme for media paths - builder.Services.AddUnique(); + // register the scheme for media paths + builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); - builder.SetMediaFileSystem(factory => - { - IIOHelper ioHelper = factory.GetRequiredService(); - IHostingEnvironment hostingEnvironment = factory.GetRequiredService(); - ILogger logger = factory.GetRequiredService>(); - GlobalSettings globalSettings = factory.GetRequiredService>().Value; + builder.SetMediaFileSystem(factory => + { + IIOHelper ioHelper = factory.GetRequiredService(); + IHostingEnvironment hostingEnvironment = factory.GetRequiredService(); + ILogger logger = factory.GetRequiredService>(); + GlobalSettings globalSettings = factory.GetRequiredService>().Value; - var rootPath = Path.IsPathRooted(globalSettings.UmbracoMediaPhysicalRootPath) ? globalSettings.UmbracoMediaPhysicalRootPath : hostingEnvironment.MapPathWebRoot(globalSettings.UmbracoMediaPhysicalRootPath); - var rootUrl = hostingEnvironment.ToAbsolute(globalSettings.UmbracoMediaPath); - return new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, rootPath, rootUrl); - }); + var rootPath = Path.IsPathRooted(globalSettings.UmbracoMediaPhysicalRootPath) + ? globalSettings.UmbracoMediaPhysicalRootPath + : hostingEnvironment.MapPathWebRoot(globalSettings.UmbracoMediaPhysicalRootPath); + var rootUrl = hostingEnvironment.ToAbsolute(globalSettings.UmbracoMediaPath); + return new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, rootPath, rootUrl); + }); - return builder; - } + return builder; } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs index d750eb15e0d1..c3aa291fb789 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs @@ -7,39 +7,37 @@ using Umbraco.Cms.Core.Telemetry; using Umbraco.Cms.Infrastructure.Install; using Umbraco.Cms.Infrastructure.Install.InstallSteps; -using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.DependencyInjection +namespace Umbraco.Cms.Infrastructure.DependencyInjection; + +public static partial class UmbracoBuilderExtensions { - public static partial class UmbracoBuilderExtensions + /// + /// Adds the services for the Umbraco installer + /// + internal static IUmbracoBuilder AddInstaller(this IUmbracoBuilder builder) { - /// - /// Adds the services for the Umbraco installer - /// - internal static IUmbracoBuilder AddInstaller(this IUmbracoBuilder builder) + // register the installer steps + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(provider => { - // register the installer steps - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(provider => - { - return new TelemetryIdentifierStep( - provider.GetRequiredService>(), - provider.GetRequiredService()); - }); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); + return new TelemetryIdentifierStep( + provider.GetRequiredService>(), + provider.GetRequiredService()); + }); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); - builder.Services.AddScoped(); + builder.Services.AddScoped(); - builder.Services.AddTransient(); - builder.Services.AddSingleton(); + builder.Services.AddTransient(); + builder.Services.AddSingleton(); - builder.Services.AddTransient(); + builder.Services.AddTransient(); - return builder; - } + return builder; } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.MappingProfiles.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.MappingProfiles.cs index e1d58a32ebda..a14b198a746e 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.MappingProfiles.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.MappingProfiles.cs @@ -5,40 +5,39 @@ using Umbraco.Cms.Core.Security; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.DependencyInjection +namespace Umbraco.Cms.Infrastructure.DependencyInjection; + +public static partial class UmbracoBuilderExtensions { - public static partial class UmbracoBuilderExtensions + /// + /// Registers the core Umbraco mapper definitions + /// + public static IUmbracoBuilder AddCoreMappingProfiles(this IUmbracoBuilder builder) { - /// - /// Registers the core Umbraco mapper definitions - /// - public static IUmbracoBuilder AddCoreMappingProfiles(this IUmbracoBuilder builder) - { - builder.Services.AddUnique(); + builder.Services.AddUnique(); - builder.WithCollectionBuilder()? - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add(); + builder.WithCollectionBuilder()? + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); - return builder; - } + return builder; } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs index f1d3e38ac2e8..e965cef8ed91 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs @@ -1,74 +1,74 @@ using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence.Repositories; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.DependencyInjection +namespace Umbraco.Cms.Infrastructure.DependencyInjection; + +/// +/// Composes repositories. +/// +public static partial class UmbracoBuilderExtensions { /// - /// Composes repositories. + /// Adds the Umbraco repositories /// - public static partial class UmbracoBuilderExtensions + internal static IUmbracoBuilder AddRepositories(this IUmbracoBuilder builder) { - /// - /// Adds the Umbraco repositories - /// - internal static IUmbracoBuilder AddRepositories(this IUmbracoBuilder builder) - { - // repositories - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddMultipleUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(factory => factory.GetRequiredService()); - builder.Services.AddUnique(factory => factory.GetRequiredService()); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); + // repositories + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddMultipleUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(factory => + factory.GetRequiredService()); + builder.Services.AddUnique(factory => + factory.GetRequiredService()); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); - return builder; - } + return builder; } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs index 7e0802d558ad..1616de999ac7 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -8,7 +7,7 @@ using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Cms.Core.Extensions; +using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Packaging; using Umbraco.Cms.Core.PropertyEditors; @@ -26,93 +25,93 @@ using Umbraco.Cms.Infrastructure.Templates; using Umbraco.Extensions; using CacheInstructionService = Umbraco.Cms.Core.Services.Implement.CacheInstructionService; -using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; -namespace Umbraco.Cms.Infrastructure.DependencyInjection +namespace Umbraco.Cms.Infrastructure.DependencyInjection; + +public static partial class UmbracoBuilderExtensions { - public static partial class UmbracoBuilderExtensions + /// + /// Adds Umbraco services + /// + internal static IUmbracoBuilder AddServices(this IUmbracoBuilder builder) { - /// - /// Adds Umbraco services - /// - internal static IUmbracoBuilder AddServices(this IUmbracoBuilder builder) - { - // register the service context - builder.Services.AddSingleton(); + // register the service context + builder.Services.AddSingleton(); - // register the special idk map - builder.Services.AddUnique(); + // register the special idk map + builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddTransient(CreateLocalizedTextServiceFileSourcesFactory); - builder.Services.AddUnique(factory => CreatePackageRepository(factory, "createdPackages.config")); - builder.Services.AddUnique(); - builder.Services.AddSingleton(CreatePackageDataInstallation); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddTransient(); - builder.Services.AddUnique(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddTransient(CreateLocalizedTextServiceFileSourcesFactory); + builder.Services.AddUnique(factory => CreatePackageRepository(factory, "createdPackages.config")); + builder.Services.AddUnique(); + builder.Services.AddSingleton(CreatePackageDataInstallation); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddTransient(); + builder.Services.AddUnique(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); - return builder; - } + return builder; + } - private static PackagesRepository CreatePackageRepository(IServiceProvider factory, string packageRepoFileName) - => new PackagesRepository( - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService>(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - packageRepoFileName); + private static PackagesRepository CreatePackageRepository(IServiceProvider factory, string packageRepoFileName) + => new( + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService>(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + packageRepoFileName); - // Factory registration is only required because of ambiguous constructor - private static PackageDataInstallation CreatePackageDataInstallation(IServiceProvider factory) - => new PackageDataInstallation( - factory.GetRequiredService(), - factory.GetRequiredService>(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService()); + // Factory registration is only required because of ambiguous constructor + private static PackageDataInstallation CreatePackageDataInstallation(IServiceProvider factory) + => new( + factory.GetRequiredService(), + factory.GetRequiredService>(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService()); - private static LocalizedTextServiceFileSources CreateLocalizedTextServiceFileSourcesFactory(IServiceProvider container) - { - var hostingEnvironment = container.GetRequiredService(); - var subPath = WebPath.Combine(Constants.SystemDirectories.Umbraco, "config", "lang"); - var mainLangFolder = new DirectoryInfo(hostingEnvironment.MapPathContentRoot(subPath)); + private static LocalizedTextServiceFileSources CreateLocalizedTextServiceFileSourcesFactory( + IServiceProvider container) + { + IHostingEnvironment hostingEnvironment = container.GetRequiredService(); + var subPath = WebPath.Combine(Constants.SystemDirectories.Umbraco, "config", "lang"); + var mainLangFolder = new DirectoryInfo(hostingEnvironment.MapPathContentRoot(subPath)); - return new LocalizedTextServiceFileSources( - container.GetRequiredService>(), - container.GetRequiredService(), - mainLangFolder, - container.GetServices(), - new EmbeddedFileProvider(typeof(IAssemblyProvider).Assembly, "Umbraco.Cms.Core.EmbeddedResources.Lang").GetDirectoryContents(string.Empty)); - } + return new LocalizedTextServiceFileSources( + container.GetRequiredService>(), + container.GetRequiredService(), + mainLangFolder, + container.GetServices(), + new EmbeddedFileProvider(typeof(IAssemblyProvider).Assembly, "Umbraco.Cms.Core.EmbeddedResources.Lang") + .GetDirectoryContents(string.Empty)); } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.TelemetryProviders.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.TelemetryProviders.cs index f0ab1ec344ac..3927bb2d69a1 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.TelemetryProviders.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.TelemetryProviders.cs @@ -3,23 +3,22 @@ using Umbraco.Cms.Infrastructure.Telemetry.Interfaces; using Umbraco.Cms.Infrastructure.Telemetry.Providers; -namespace Umbraco.Cms.Infrastructure.DependencyInjection +namespace Umbraco.Cms.Infrastructure.DependencyInjection; + +public static class UmbracoBuilder_TelemetryProviders { - public static class UmbracoBuilder_TelemetryProviders + public static IUmbracoBuilder AddTelemetryProviders(this IUmbracoBuilder builder) { - public static IUmbracoBuilder AddTelemetryProviders(this IUmbracoBuilder builder) - { - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - return builder; - } + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + return builder; } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Uniques.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Uniques.cs index e3839e152b9a..df5dea081c5e 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Uniques.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Uniques.cs @@ -1,4 +1,3 @@ -using System; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Dictionary; @@ -8,208 +7,212 @@ using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.DependencyInjection +namespace Umbraco.Cms.Infrastructure.DependencyInjection; + +/// +/// Provides extension methods to the class. +/// +public static partial class UmbracoBuilderExtensions { /// - /// Provides extension methods to the class. + /// Sets the culture dictionary factory. /// - public static partial class UmbracoBuilderExtensions + /// The type of the factory. + /// The builder. + public static IUmbracoBuilder SetCultureDictionaryFactory(this IUmbracoBuilder builder) + where T : class, ICultureDictionaryFactory { - /// - /// Sets the culture dictionary factory. - /// - /// The type of the factory. - /// The builder. - public static IUmbracoBuilder SetCultureDictionaryFactory(this IUmbracoBuilder builder) - where T : class, ICultureDictionaryFactory - { - builder.Services.AddUnique(); - return builder; - } + builder.Services.AddUnique(); + return builder; + } - /// - /// Sets the default view content provider - /// - /// The type of the provider. - /// The builder. - /// - public static IUmbracoBuilder SetDefaultViewContentProvider(this IUmbracoBuilder builder) - where T : class, IDefaultViewContentProvider - { - builder.Services.AddUnique(); - return builder; - } + /// + /// Sets the default view content provider + /// + /// The type of the provider. + /// The builder. + /// + public static IUmbracoBuilder SetDefaultViewContentProvider(this IUmbracoBuilder builder) + where T : class, IDefaultViewContentProvider + { + builder.Services.AddUnique(); + return builder; + } - /// - /// Sets the culture dictionary factory. - /// - /// The builder. - /// A function creating a culture dictionary factory. - public static IUmbracoBuilder SetCultureDictionaryFactory(this IUmbracoBuilder builder, Func factory) - { - builder.Services.AddUnique(factory); - return builder; - } + /// + /// Sets the culture dictionary factory. + /// + /// The builder. + /// A function creating a culture dictionary factory. + public static IUmbracoBuilder SetCultureDictionaryFactory(this IUmbracoBuilder builder, + Func factory) + { + builder.Services.AddUnique(factory); + return builder; + } - /// - /// Sets the culture dictionary factory. - /// - /// The builder. - /// A factory. - public static IUmbracoBuilder SetCultureDictionaryFactory(this IUmbracoBuilder builder, ICultureDictionaryFactory factory) - { - builder.Services.AddUnique(factory); - return builder; - } + /// + /// Sets the culture dictionary factory. + /// + /// The builder. + /// A factory. + public static IUmbracoBuilder SetCultureDictionaryFactory(this IUmbracoBuilder builder, + ICultureDictionaryFactory factory) + { + builder.Services.AddUnique(factory); + return builder; + } - /// - /// Sets the published content model factory. - /// - /// The type of the factory. - /// The builder. - public static IUmbracoBuilder SetPublishedContentModelFactory(this IUmbracoBuilder builder) - where T : class, IPublishedModelFactory - { - builder.Services.AddUnique(); - return builder; - } + /// + /// Sets the published content model factory. + /// + /// The type of the factory. + /// The builder. + public static IUmbracoBuilder SetPublishedContentModelFactory(this IUmbracoBuilder builder) + where T : class, IPublishedModelFactory + { + builder.Services.AddUnique(); + return builder; + } - /// - /// Sets the published content model factory. - /// - /// The builder. - /// A function creating a published content model factory. - public static IUmbracoBuilder SetPublishedContentModelFactory(this IUmbracoBuilder builder, Func factory) - { - builder.Services.AddUnique(factory); - return builder; - } + /// + /// Sets the published content model factory. + /// + /// The builder. + /// A function creating a published content model factory. + public static IUmbracoBuilder SetPublishedContentModelFactory(this IUmbracoBuilder builder, + Func factory) + { + builder.Services.AddUnique(factory); + return builder; + } - /// - /// Sets the published content model factory. - /// - /// The builder. - /// A published content model factory. - public static IUmbracoBuilder SetPublishedContentModelFactory(this IUmbracoBuilder builder, IPublishedModelFactory factory) - { - builder.Services.AddUnique(factory); - return builder; - } + /// + /// Sets the published content model factory. + /// + /// The builder. + /// A published content model factory. + public static IUmbracoBuilder SetPublishedContentModelFactory(this IUmbracoBuilder builder, + IPublishedModelFactory factory) + { + builder.Services.AddUnique(factory); + return builder; + } - /// - /// Sets the short string helper. - /// - /// The type of the short string helper. - /// The builder. - public static IUmbracoBuilder SetShortStringHelper(this IUmbracoBuilder builder) - where T : class, IShortStringHelper - { - builder.Services.AddUnique(); - return builder; - } + /// + /// Sets the short string helper. + /// + /// The type of the short string helper. + /// The builder. + public static IUmbracoBuilder SetShortStringHelper(this IUmbracoBuilder builder) + where T : class, IShortStringHelper + { + builder.Services.AddUnique(); + return builder; + } - /// - /// Sets the short string helper. - /// - /// The builder. - /// A function creating a short string helper. - public static IUmbracoBuilder SetShortStringHelper(this IUmbracoBuilder builder, Func factory) - { - builder.Services.AddUnique(factory); - return builder; - } + /// + /// Sets the short string helper. + /// + /// The builder. + /// A function creating a short string helper. + public static IUmbracoBuilder SetShortStringHelper(this IUmbracoBuilder builder, + Func factory) + { + builder.Services.AddUnique(factory); + return builder; + } - /// - /// Sets the short string helper. - /// - /// A builder. - /// A short string helper. - public static IUmbracoBuilder SetShortStringHelper(this IUmbracoBuilder builder, IShortStringHelper helper) - { - builder.Services.AddUnique(helper); - return builder; - } + /// + /// Sets the short string helper. + /// + /// A builder. + /// A short string helper. + public static IUmbracoBuilder SetShortStringHelper(this IUmbracoBuilder builder, IShortStringHelper helper) + { + builder.Services.AddUnique(helper); + return builder; + } - /// - /// Sets the filesystem used by the MediaFileManager - /// - /// A builder. - /// Factory method to create an IFileSystem implementation used in the MediaFileManager - public static IUmbracoBuilder SetMediaFileSystem(this IUmbracoBuilder builder, - Func filesystemFactory) + /// + /// Sets the filesystem used by the MediaFileManager + /// + /// A builder. + /// Factory method to create an IFileSystem implementation used in the MediaFileManager + public static IUmbracoBuilder SetMediaFileSystem(this IUmbracoBuilder builder, + Func filesystemFactory) + { + builder.Services.AddUnique( + provider => + { + IFileSystem filesystem = filesystemFactory(provider); + // We need to use the Filesystems to create a shadow wrapper, + // because shadow wrapper requires the IsScoped delegate from the FileSystems. + // This is used by the scope provider when taking control of the filesystems. + FileSystems fileSystems = provider.GetRequiredService(); + IFileSystem shadow = fileSystems.CreateShadowWrapper(filesystem, "media"); + + return provider.CreateInstance(shadow); + }); + return builder; + } + + /// + /// Register FileSystems with a method to configure the . + /// + /// A builder. + /// Method that configures the . + /// Throws exception if is null. + /// Throws exception if full path can't be resolved successfully. + public static IUmbracoBuilder ConfigureFileSystems(this IUmbracoBuilder builder, + Action configure) + { + if (configure == null) { - builder.Services.AddUnique( - provider => - { - IFileSystem filesystem = filesystemFactory(provider); - // We need to use the Filesystems to create a shadow wrapper, - // because shadow wrapper requires the IsScoped delegate from the FileSystems. - // This is used by the scope provider when taking control of the filesystems. - FileSystems fileSystems = provider.GetRequiredService(); - IFileSystem shadow = fileSystems.CreateShadowWrapper(filesystem, "media"); - - return provider.CreateInstance(shadow); - }); - return builder; + throw new ArgumentNullException(nameof(configure)); } - /// - /// Register FileSystems with a method to configure the . - /// - /// A builder. - /// Method that configures the . - /// Throws exception if is null. - /// Throws exception if full path can't be resolved successfully. - public static IUmbracoBuilder ConfigureFileSystems(this IUmbracoBuilder builder, - Action configure) - { - if (configure == null) + builder.Services.AddUnique( + provider => { - throw new ArgumentNullException(nameof(configure)); - } - - builder.Services.AddUnique( - provider => - { - FileSystems fileSystems = provider.CreateInstance(); - configure(provider, fileSystems); - return fileSystems; - }); - return builder; - } + FileSystems fileSystems = provider.CreateInstance(); + configure(provider, fileSystems); + return fileSystems; + }); + return builder; + } - /// - /// Sets the log viewer. - /// - /// The type of the log viewer. - /// The builder. - public static IUmbracoBuilder SetLogViewer(this IUmbracoBuilder builder) - where T : class, ILogViewer - { - builder.Services.AddUnique(); - return builder; - } + /// + /// Sets the log viewer. + /// + /// The type of the log viewer. + /// The builder. + public static IUmbracoBuilder SetLogViewer(this IUmbracoBuilder builder) + where T : class, ILogViewer + { + builder.Services.AddUnique(); + return builder; + } - /// - /// Sets the log viewer. - /// - /// The builder. - /// A function creating a log viewer. - public static IUmbracoBuilder SetLogViewer(this IUmbracoBuilder builder, Func factory) - { - builder.Services.AddUnique(factory); - return builder; - } + /// + /// Sets the log viewer. + /// + /// The builder. + /// A function creating a log viewer. + public static IUmbracoBuilder SetLogViewer(this IUmbracoBuilder builder, Func factory) + { + builder.Services.AddUnique(factory); + return builder; + } - /// - /// Sets the log viewer. - /// - /// A builder. - /// A log viewer. - public static IUmbracoBuilder SetLogViewer(this IUmbracoBuilder builder, ILogViewer viewer) - { - builder.Services.AddUnique(viewer); - return builder; - } + /// + /// Sets the log viewer. + /// + /// A builder. + /// A log viewer. + public static IUmbracoBuilder SetLogViewer(this IUmbracoBuilder builder, ILogViewer viewer) + { + builder.Services.AddUnique(viewer); + return builder; } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.WebAssets.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.WebAssets.cs index 53e638997bb6..d788f26f8868 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.WebAssets.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.WebAssets.cs @@ -1,16 +1,14 @@ using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Cms.Core.Events; using Umbraco.Cms.Infrastructure.WebAssets; -namespace Umbraco.Cms.Infrastructure.DependencyInjection +namespace Umbraco.Cms.Infrastructure.DependencyInjection; + +public static partial class UmbracoBuilderExtensions { - public static partial class UmbracoBuilderExtensions + internal static IUmbracoBuilder AddWebAssets(this IUmbracoBuilder builder) { - internal static IUmbracoBuilder AddWebAssets(this IUmbracoBuilder builder) - { - builder.Services.AddSingleton(); - return builder; - } + builder.Services.AddSingleton(); + return builder; } } diff --git a/src/Umbraco.Infrastructure/Deploy/IGridCellValueConnector.cs b/src/Umbraco.Infrastructure/Deploy/IGridCellValueConnector.cs index 40c0b98474ca..deeef2e8dd18 100644 --- a/src/Umbraco.Infrastructure/Deploy/IGridCellValueConnector.cs +++ b/src/Umbraco.Infrastructure/Deploy/IGridCellValueConnector.cs @@ -1,38 +1,44 @@ -using System.Collections.Generic; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Deploy +namespace Umbraco.Cms.Core.Deploy; + +/// +/// Defines methods that can convert a grid cell value to / from an environment-agnostic string. +/// +/// +/// Grid cell values may contain values such as content identifiers, that would be local +/// to one environment, and need to be converted in order to be deployed. +/// +public interface IGridCellValueConnector { /// - /// Defines methods that can convert a grid cell value to / from an environment-agnostic string. + /// Gets a value indicating whether the connector supports a specified grid editor view. /// - /// Grid cell values may contain values such as content identifiers, that would be local - /// to one environment, and need to be converted in order to be deployed. - public interface IGridCellValueConnector - { - /// - /// Gets a value indicating whether the connector supports a specified grid editor view. - /// - /// The grid editor view. It needs to be the view instead of the alias as the view is really what identifies what kind of connector should be used. Alias can be anything and you can have multiple different aliases using the same kind of view. - /// A value indicating whether the connector supports the grid editor view. - /// Note that can be string.Empty to indicate the "default" connector. - bool IsConnector(string view); - - /// - /// Gets the value to be deployed from the control value as a string. - /// - /// The control containing the value. - /// The dependencies of the property. - /// The grid cell value to be deployed. - /// Note that - string GetValue(GridValue.GridControl gridControl, ICollection dependencies); + /// + /// The grid editor view. It needs to be the view instead of the alias as the view is really what + /// identifies what kind of connector should be used. Alias can be anything and you can have multiple different aliases + /// using the same kind of view. + /// + /// A value indicating whether the connector supports the grid editor view. + /// Note that can be string.Empty to indicate the "default" connector. + bool IsConnector(string view); - /// - /// Allows you to modify the value of a control being deployed. - /// - /// The control being deployed. - /// Follows the pattern of the property value connectors (). The SetValue method is used to modify the value of the . + /// + /// Gets the value to be deployed from the control value as a string. + /// + /// The control containing the value. + /// The dependencies of the property. + /// The grid cell value to be deployed. + /// Note that + string GetValue(GridValue.GridControl gridControl, ICollection dependencies); - void SetValue(GridValue.GridControl gridControl); - } + /// + /// Allows you to modify the value of a control being deployed. + /// + /// The control being deployed. + /// + /// Follows the pattern of the property value connectors (). The SetValue method is + /// used to modify the value of the . + /// + void SetValue(GridValue.GridControl gridControl); } diff --git a/src/Umbraco.Infrastructure/DistributedLocking/DefaultDistributedLockingMechanismFactory.cs b/src/Umbraco.Infrastructure/DistributedLocking/DefaultDistributedLockingMechanismFactory.cs index 7916de399661..b44a7c993b87 100644 --- a/src/Umbraco.Infrastructure/DistributedLocking/DefaultDistributedLockingMechanismFactory.cs +++ b/src/Umbraco.Infrastructure/DistributedLocking/DefaultDistributedLockingMechanismFactory.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DistributedLocking; @@ -10,12 +6,12 @@ namespace Umbraco.Cms.Infrastructure.DistributedLocking; public class DefaultDistributedLockingMechanismFactory : IDistributedLockingMechanismFactory { - private object _lock = new(); - private bool _initialized; - private IDistributedLockingMechanism _distributedLockingMechanism = null!; + private readonly IEnumerable _distributedLockingMechanisms; private readonly IOptionsMonitor _globalSettings; - private readonly IEnumerable _distributedLockingMechanisms; + private IDistributedLockingMechanism _distributedLockingMechanism = null!; + private bool _initialized; + private object _lock = new(); public DefaultDistributedLockingMechanismFactory( IOptionsMonitor globalSettings, @@ -49,7 +45,8 @@ private IDistributedLockingMechanism Initialize() if (value == null) { - throw new InvalidOperationException($"Couldn't find DistributedLockingMechanism specified by global config: {configured}"); + throw new InvalidOperationException( + $"Couldn't find DistributedLockingMechanism specified by global config: {configured}"); } } @@ -59,6 +56,6 @@ private IDistributedLockingMechanism Initialize() return defaultMechanism; } - throw new InvalidOperationException($"Couldn't find an appropriate default distributed locking mechanism."); + throw new InvalidOperationException("Couldn't find an appropriate default distributed locking mechanism."); } } diff --git a/src/Umbraco.Infrastructure/Events/MigrationEventArgs.cs b/src/Umbraco.Infrastructure/Events/MigrationEventArgs.cs index 23bfed0cd9c4..ebf7b89121f5 100644 --- a/src/Umbraco.Infrastructure/Events/MigrationEventArgs.cs +++ b/src/Umbraco.Infrastructure/Events/MigrationEventArgs.cs @@ -1,94 +1,113 @@ -using System; -using System.Collections.Generic; -using Umbraco.Cms.Core.Semver; +using Umbraco.Cms.Core.Semver; using Umbraco.Cms.Infrastructure.Migrations; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +public class MigrationEventArgs : CancellableObjectEventArgs>, IEquatable { - public class MigrationEventArgs : CancellableObjectEventArgs>, IEquatable + public MigrationEventArgs(IList migrationTypes, SemVersion configuredVersion, SemVersion targetVersion, + string productName, bool canCancel) + : this(migrationTypes, null, configuredVersion, targetVersion, productName, canCancel) + { + } + + internal MigrationEventArgs(IList migrationTypes, IMigrationContext? migrationContext, + SemVersion configuredVersion, SemVersion targetVersion, string productName, bool canCancel) + : base(migrationTypes, canCancel) + { + MigrationContext = migrationContext; + ConfiguredSemVersion = configuredVersion; + TargetSemVersion = targetVersion; + ProductName = productName; + } + + public MigrationEventArgs(IList migrationTypes, SemVersion configuredVersion, SemVersion targetVersion, + string productName) + : this(migrationTypes, null, configuredVersion, targetVersion, productName, false) { - public MigrationEventArgs(IList migrationTypes, SemVersion configuredVersion, SemVersion targetVersion, string productName, bool canCancel) - : this(migrationTypes, null, configuredVersion, targetVersion, productName, canCancel) - { } + } + + /// + /// Returns all migrations that were used in the migration runner + /// + public IList? MigrationsTypes => EventObject; + + /// + /// Gets the origin version of the migration, i.e. the one that is currently installed. + /// + public SemVersion ConfiguredSemVersion { get; } + + /// + /// Gets the target version of the migration. + /// + public SemVersion TargetSemVersion { get; } - internal MigrationEventArgs(IList migrationTypes, IMigrationContext? migrationContext, SemVersion configuredVersion, SemVersion targetVersion, string productName, bool canCancel) - : base(migrationTypes, canCancel) + /// + /// Gets the product name. + /// + public string ProductName { get; } + + /// + /// Gets the migration context. + /// + /// Is only available after migrations have run, for post-migrations. + internal IMigrationContext? MigrationContext { get; } + + public bool Equals(MigrationEventArgs? other) + { + if (ReferenceEquals(null, other)) { - MigrationContext = migrationContext; - ConfiguredSemVersion = configuredVersion; - TargetSemVersion = targetVersion; - ProductName = productName; + return false; } - public MigrationEventArgs(IList migrationTypes, SemVersion configuredVersion, SemVersion targetVersion, string productName) - : this(migrationTypes, null, configuredVersion, targetVersion, productName, false) - { } - - /// - /// Returns all migrations that were used in the migration runner - /// - public IList? MigrationsTypes => EventObject; - - /// - /// Gets the origin version of the migration, i.e. the one that is currently installed. - /// - public SemVersion ConfiguredSemVersion { get; } - - /// - /// Gets the target version of the migration. - /// - public SemVersion TargetSemVersion { get; } - - /// - /// Gets the product name. - /// - public string ProductName { get; } - - /// - /// Gets the migration context. - /// - /// Is only available after migrations have run, for post-migrations. - internal IMigrationContext? MigrationContext { get; } - - public bool Equals(MigrationEventArgs? other) + if (ReferenceEquals(this, other)) { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return base.Equals(other) && ConfiguredSemVersion.Equals(other.ConfiguredSemVersion) && (MigrationContext?.Equals(other.MigrationContext) ?? false) && string.Equals(ProductName, other.ProductName) && TargetSemVersion.Equals(other.TargetSemVersion); + return true; } - public override bool Equals(object? obj) + return base.Equals(other) && ConfiguredSemVersion.Equals(other.ConfiguredSemVersion) && + (MigrationContext?.Equals(other.MigrationContext) ?? false) && + string.Equals(ProductName, other.ProductName) && TargetSemVersion.Equals(other.TargetSemVersion); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((MigrationEventArgs) obj); + return false; } - public override int GetHashCode() + if (ReferenceEquals(this, obj)) { - unchecked - { - int hashCode = base.GetHashCode(); - hashCode = (hashCode * 397) ^ ConfiguredSemVersion.GetHashCode(); - if (MigrationContext is not null) - { - hashCode = (hashCode * 397) ^ MigrationContext.GetHashCode(); - } - hashCode = (hashCode * 397) ^ ProductName.GetHashCode(); - hashCode = (hashCode * 397) ^ TargetSemVersion.GetHashCode(); - return hashCode; - } + return true; } - public static bool operator ==(MigrationEventArgs left, MigrationEventArgs right) + if (obj.GetType() != GetType()) { - return Equals(left, right); + return false; } - public static bool operator !=(MigrationEventArgs left, MigrationEventArgs right) + return Equals((MigrationEventArgs)obj); + } + + public override int GetHashCode() + { + unchecked { - return !Equals(left, right); + var hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ ConfiguredSemVersion.GetHashCode(); + if (MigrationContext is not null) + { + hashCode = (hashCode * 397) ^ MigrationContext.GetHashCode(); + } + + hashCode = (hashCode * 397) ^ ProductName.GetHashCode(); + hashCode = (hashCode * 397) ^ TargetSemVersion.GetHashCode(); + return hashCode; } } + + public static bool operator ==(MigrationEventArgs left, MigrationEventArgs right) => Equals(left, right); + + public static bool operator !=(MigrationEventArgs left, MigrationEventArgs right) => !Equals(left, right); } diff --git a/src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs b/src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs index b06248c79e10..aa6d9e061d9d 100644 --- a/src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs +++ b/src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs @@ -2,156 +2,165 @@ // See LICENSE for more details. using System.Globalization; -using System.Linq; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; +using IScope = Umbraco.Cms.Infrastructure.Scoping.IScope; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +// TODO: lots of duplicate code in this one, refactor +public sealed class RelateOnTrashNotificationHandler : + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler { - // TODO: lots of duplicate code in this one, refactor - public sealed class RelateOnTrashNotificationHandler : - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler + private readonly IAuditService _auditService; + private readonly IEntityService _entityService; + private readonly IRelationService _relationService; + private readonly IScopeProvider _scopeProvider; + private readonly ILocalizedTextService _textService; + + public RelateOnTrashNotificationHandler( + IRelationService relationService, + IEntityService entityService, + ILocalizedTextService textService, + IAuditService auditService, + IScopeProvider scopeProvider) { - private readonly IRelationService _relationService; - private readonly IEntityService _entityService; - private readonly ILocalizedTextService _textService; - private readonly IAuditService _auditService; - private readonly IScopeProvider _scopeProvider; - - public RelateOnTrashNotificationHandler( - IRelationService relationService, - IEntityService entityService, - ILocalizedTextService textService, - IAuditService auditService, - IScopeProvider scopeProvider) - { - _relationService = relationService; - _entityService = entityService; - _textService = textService; - _auditService = auditService; - _scopeProvider = scopeProvider; - } + _relationService = relationService; + _entityService = entityService; + _textService = textService; + _auditService = auditService; + _scopeProvider = scopeProvider; + } - public void Handle(ContentMovedNotification notification) + public void Handle(ContentMovedNotification notification) + { + foreach (MoveEventInfo item in notification.MoveInfoCollection.Where(x => + x.OriginalPath.Contains(Constants.System.RecycleBinContentString))) { - foreach (var item in notification.MoveInfoCollection.Where(x => x.OriginalPath.Contains(Constants.System.RecycleBinContentString))) - { - const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias; - var relations = _relationService.GetByChildId(item.Entity.Id); + const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias; + IEnumerable relations = _relationService.GetByChildId(item.Entity.Id); - foreach (var relation in relations.Where(x => x.RelationType.Alias.InvariantEquals(relationTypeAlias))) - { - _relationService.Delete(relation); - } + foreach (IRelation relation in + relations.Where(x => x.RelationType.Alias.InvariantEquals(relationTypeAlias))) + { + _relationService.Delete(relation); } } + } - public void Handle(ContentMovedToRecycleBinNotification notification) + public void Handle(ContentMovedToRecycleBinNotification notification) + { + using (IScope scope = _scopeProvider.CreateScope()) { - using (var scope = _scopeProvider.CreateScope()) + const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias; + IRelationType? relationType = _relationService.GetRelationTypeByAlias(relationTypeAlias); + + // check that the relation-type exists, if not, then recreate it + if (relationType == null) { - const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias; - var relationType = _relationService.GetRelationTypeByAlias(relationTypeAlias); + Guid documentObjectType = Constants.ObjectTypes.Document; + const string relationTypeName = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteName; - // check that the relation-type exists, if not, then recreate it - if (relationType == null) - { - var documentObjectType = Constants.ObjectTypes.Document; - const string relationTypeName = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteName; + relationType = new RelationType(relationTypeName, relationTypeAlias, false, documentObjectType, + documentObjectType, false); + _relationService.Save(relationType); + } - relationType = new RelationType(relationTypeName, relationTypeAlias, false, documentObjectType, documentObjectType, false); - _relationService.Save(relationType); - } + foreach (MoveEventInfo item in notification.MoveInfoCollection) + { + IList originalPath = item.OriginalPath.ToDelimitedList(); + var originalParentId = originalPath.Count > 2 + ? int.Parse(originalPath[originalPath.Count - 2], CultureInfo.InvariantCulture) + : Constants.System.Root; - foreach (var item in notification.MoveInfoCollection) + //before we can create this relation, we need to ensure that the original parent still exists which + //may not be the case if the encompassing transaction also deleted it when this item was moved to the bin + + if (_entityService.Exists(originalParentId)) { - var originalPath = item.OriginalPath.ToDelimitedList(); - var originalParentId = originalPath.Count > 2 - ? int.Parse(originalPath[originalPath.Count - 2], CultureInfo.InvariantCulture) - : Constants.System.Root; - - //before we can create this relation, we need to ensure that the original parent still exists which - //may not be the case if the encompassing transaction also deleted it when this item was moved to the bin - - if (_entityService.Exists(originalParentId)) - { - // Add a relation for the item being deleted, so that we can know the original parent for if we need to restore later - var relation = _relationService.GetByParentAndChildId(originalParentId, item.Entity.Id, relationType) ?? new Relation(originalParentId, item.Entity.Id, relationType); - _relationService.Save(relation); - - _auditService.Add(AuditType.Delete, - item.Entity.WriterId, - item.Entity.Id, - ObjectTypes.GetName(UmbracoObjectTypes.Document), - string.Format(_textService.Localize("recycleBin","contentTrashed"), item.Entity.Id, originalParentId) - ); - } + // Add a relation for the item being deleted, so that we can know the original parent for if we need to restore later + IRelation relation = + _relationService.GetByParentAndChildId(originalParentId, item.Entity.Id, relationType) ?? + new Relation(originalParentId, item.Entity.Id, relationType); + _relationService.Save(relation); + + _auditService.Add(AuditType.Delete, + item.Entity.WriterId, + item.Entity.Id, + UmbracoObjectTypes.Document.GetName(), + string.Format(_textService.Localize("recycleBin", "contentTrashed"), item.Entity.Id, + originalParentId) + ); } - - scope.Complete(); } + + scope.Complete(); } + } - public void Handle(MediaMovedNotification notification) + public void Handle(MediaMovedNotification notification) + { + foreach (MoveEventInfo item in notification.MoveInfoCollection.Where(x => + x.OriginalPath.Contains(Constants.System.RecycleBinMediaString))) { - foreach (var item in notification.MoveInfoCollection.Where(x => x.OriginalPath.Contains(Constants.System.RecycleBinMediaString))) + const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias; + IEnumerable relations = _relationService.GetByChildId(item.Entity.Id); + foreach (IRelation relation in + relations.Where(x => x.RelationType.Alias.InvariantEquals(relationTypeAlias))) { - const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias; - var relations = _relationService.GetByChildId(item.Entity.Id); - foreach (var relation in relations.Where(x => x.RelationType.Alias.InvariantEquals(relationTypeAlias))) - { - _relationService.Delete(relation); - } + _relationService.Delete(relation); } - } + } - public void Handle(MediaMovedToRecycleBinNotification notification) + public void Handle(MediaMovedToRecycleBinNotification notification) + { + using (IScope scope = _scopeProvider.CreateScope()) { - using (var scope = _scopeProvider.CreateScope()) + const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias; + IRelationType? relationType = _relationService.GetRelationTypeByAlias(relationTypeAlias); + // check that the relation-type exists, if not, then recreate it + if (relationType == null) { - const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias; - var relationType = _relationService.GetRelationTypeByAlias(relationTypeAlias); - // check that the relation-type exists, if not, then recreate it - if (relationType == null) - { - var documentObjectType = Constants.ObjectTypes.Document; - const string relationTypeName = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName; - relationType = new RelationType(relationTypeName, relationTypeAlias, false, documentObjectType, documentObjectType, false); - _relationService.Save(relationType); - } + Guid documentObjectType = Constants.ObjectTypes.Document; + const string relationTypeName = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName; + relationType = new RelationType(relationTypeName, relationTypeAlias, false, documentObjectType, + documentObjectType, false); + _relationService.Save(relationType); + } - foreach (var item in notification.MoveInfoCollection) + foreach (MoveEventInfo item in notification.MoveInfoCollection) + { + IList originalPath = item.OriginalPath.ToDelimitedList(); + var originalParentId = originalPath.Count > 2 + ? int.Parse(originalPath[originalPath.Count - 2], CultureInfo.InvariantCulture) + : Constants.System.Root; + //before we can create this relation, we need to ensure that the original parent still exists which + //may not be the case if the encompassing transaction also deleted it when this item was moved to the bin + if (_entityService.Exists(originalParentId)) { - var originalPath = item.OriginalPath.ToDelimitedList(); - var originalParentId = originalPath.Count > 2 - ? int.Parse(originalPath[originalPath.Count - 2], CultureInfo.InvariantCulture) - : Constants.System.Root; - //before we can create this relation, we need to ensure that the original parent still exists which - //may not be the case if the encompassing transaction also deleted it when this item was moved to the bin - if (_entityService.Exists(originalParentId)) - { - // Add a relation for the item being deleted, so that we can know the original parent for if we need to restore later - var relation = _relationService.GetByParentAndChildId(originalParentId, item.Entity.Id, relationType) ?? new Relation(originalParentId, item.Entity.Id, relationType); - _relationService.Save(relation); - _auditService.Add(AuditType.Delete, - item.Entity.CreatorId, - item.Entity.Id, - ObjectTypes.GetName(UmbracoObjectTypes.Media), - string.Format(_textService.Localize("recycleBin","mediaTrashed"), item.Entity.Id, originalParentId) - ); - } + // Add a relation for the item being deleted, so that we can know the original parent for if we need to restore later + IRelation relation = + _relationService.GetByParentAndChildId(originalParentId, item.Entity.Id, relationType) ?? + new Relation(originalParentId, item.Entity.Id, relationType); + _relationService.Save(relation); + _auditService.Add(AuditType.Delete, + item.Entity.CreatorId, + item.Entity.Id, + UmbracoObjectTypes.Media.GetName(), + string.Format(_textService.Localize("recycleBin", "mediaTrashed"), item.Entity.Id, + originalParentId) + ); } - - scope.Complete(); } + scope.Complete(); } } } diff --git a/src/Umbraco.Infrastructure/Examine/BaseValueSetBuilder.cs b/src/Umbraco.Infrastructure/Examine/BaseValueSetBuilder.cs index ceb203c15c59..119c1c74c97f 100644 --- a/src/Umbraco.Infrastructure/Examine/BaseValueSetBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/BaseValueSetBuilder.cs @@ -1,70 +1,88 @@ -using System.Collections.Generic; -using Examine; +using Examine; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +public abstract class BaseValueSetBuilder : IValueSetBuilder + where TContent : IContentBase { - /// - public abstract class BaseValueSetBuilder : IValueSetBuilder - where TContent : IContentBase + private readonly PropertyEditorCollection _propertyEditors; + + protected BaseValueSetBuilder(PropertyEditorCollection propertyEditors, bool publishedValuesOnly) { - protected bool PublishedValuesOnly { get; } - private readonly PropertyEditorCollection _propertyEditors; + PublishedValuesOnly = publishedValuesOnly; + _propertyEditors = propertyEditors ?? throw new ArgumentNullException(nameof(propertyEditors)); + } - protected BaseValueSetBuilder(PropertyEditorCollection propertyEditors, bool publishedValuesOnly) - { - PublishedValuesOnly = publishedValuesOnly; - _propertyEditors = propertyEditors ?? throw new System.ArgumentNullException(nameof(propertyEditors)); - } + protected bool PublishedValuesOnly { get; } - /// - public abstract IEnumerable GetValueSets(params TContent[] content); + /// + public abstract IEnumerable GetValueSets(params TContent[] content); - protected void AddPropertyValue(IProperty property, string? culture, string? segment, IDictionary>? values) + protected void AddPropertyValue(IProperty property, string? culture, string? segment, + IDictionary>? values) + { + IDataEditor? editor = _propertyEditors[property.PropertyType.PropertyEditorAlias]; + if (editor == null) { - var editor = _propertyEditors[property.PropertyType.PropertyEditorAlias]; - if (editor == null) return; + return; + } - var indexVals = editor.PropertyIndexValueFactory.GetIndexValues(property, culture, segment, PublishedValuesOnly); - foreach (var keyVal in indexVals) + IEnumerable>> indexVals = + editor.PropertyIndexValueFactory.GetIndexValues(property, culture, segment, PublishedValuesOnly); + foreach (KeyValuePair> keyVal in indexVals) + { + if (keyVal.Key.IsNullOrWhiteSpace()) { - if (keyVal.Key.IsNullOrWhiteSpace()) continue; + continue; + } - var cultureSuffix = culture == null ? string.Empty : "_" + culture; + var cultureSuffix = culture == null ? string.Empty : "_" + culture; - foreach (var val in keyVal.Value) + foreach (var val in keyVal.Value) + { + switch (val) { - switch (val) + //only add the value if its not null or empty (we'll check for string explicitly here too) + case null: + continue; + case string strVal: { - //only add the value if its not null or empty (we'll check for string explicitly here too) - case null: + if (strVal.IsNullOrWhiteSpace()) + { continue; - case string strVal: - { - if (strVal.IsNullOrWhiteSpace()) continue; - var key = $"{keyVal.Key}{cultureSuffix}"; - if (values?.TryGetValue(key, out var v) ?? false) - values[key] = new List(v) { val }.ToArray(); - else - values?.Add($"{keyVal.Key}{cultureSuffix}", val.Yield()); - } - break; - default: - { - var key = $"{keyVal.Key}{cultureSuffix}"; - if (values?.TryGetValue(key, out var v) ?? false) - values[key] = new List(v) { val }.ToArray(); - else - values?.Add($"{keyVal.Key}{cultureSuffix}", val.Yield()); - } + } - break; + var key = $"{keyVal.Key}{cultureSuffix}"; + if (values?.TryGetValue(key, out IEnumerable? v) ?? false) + { + values[key] = new List(v) {val}.ToArray(); + } + else + { + values?.Add($"{keyVal.Key}{cultureSuffix}", val.Yield()); + } } + break; + default: + { + var key = $"{keyVal.Key}{cultureSuffix}"; + if (values?.TryGetValue(key, out IEnumerable? v) ?? false) + { + values[key] = new List(v) {val}.ToArray(); + } + else + { + values?.Add($"{keyVal.Key}{cultureSuffix}", val.Yield()); + } + } + + break; } } } } - } diff --git a/src/Umbraco.Infrastructure/Examine/ContentIndexPopulator.cs b/src/Umbraco.Infrastructure/Examine/ContentIndexPopulator.cs index c16370aff85d..dde36117e1b5 100644 --- a/src/Umbraco.Infrastructure/Examine/ContentIndexPopulator.cs +++ b/src/Umbraco.Infrastructure/Examine/ContentIndexPopulator.cs @@ -1,175 +1,171 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Examine; using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Performs the data lookups required to rebuild a content index +/// +public class ContentIndexPopulator : IndexPopulator { + private readonly IContentService _contentService; + private readonly IValueSetBuilder _contentValueSetBuilder; + private readonly ILogger _logger; + private readonly int? _parentId; + + private readonly bool _publishedValuesOnly; + private readonly IUmbracoDatabaseFactory _umbracoDatabaseFactory; + + /// + /// This is a static query, it's parameters don't change so store statically + /// + private IQuery? _publishedQuery; + /// - /// Performs the data lookups required to rebuild a content index + /// Default constructor to lookup all content data /// - public class ContentIndexPopulator : IndexPopulator + /// + /// + /// + public ContentIndexPopulator( + ILogger logger, + IContentService contentService, + IUmbracoDatabaseFactory umbracoDatabaseFactory, + IContentValueSetBuilder contentValueSetBuilder) + : this(logger, false, null, contentService, umbracoDatabaseFactory, contentValueSetBuilder) { - private readonly IContentService _contentService; - private readonly IUmbracoDatabaseFactory _umbracoDatabaseFactory; - private readonly IValueSetBuilder _contentValueSetBuilder; - - /// - /// This is a static query, it's parameters don't change so store statically - /// - private IQuery? _publishedQuery; - private IQuery PublishedQuery => _publishedQuery ??= _umbracoDatabaseFactory.SqlContext.Query().Where(x => x.Published); - - private readonly bool _publishedValuesOnly; - private readonly int? _parentId; - private readonly ILogger _logger; - - /// - /// Default constructor to lookup all content data - /// - /// - /// - /// - public ContentIndexPopulator( - ILogger logger, - IContentService contentService, - IUmbracoDatabaseFactory umbracoDatabaseFactory, - IContentValueSetBuilder contentValueSetBuilder) - : this(logger, false, null, contentService, umbracoDatabaseFactory, contentValueSetBuilder) + } + + /// + /// Optional constructor allowing specifying custom query parameters + /// + public ContentIndexPopulator( + ILogger logger, + bool publishedValuesOnly, + int? parentId, + IContentService contentService, + IUmbracoDatabaseFactory umbracoDatabaseFactory, + IValueSetBuilder contentValueSetBuilder) + { + _contentService = contentService ?? throw new ArgumentNullException(nameof(contentService)); + _umbracoDatabaseFactory = + umbracoDatabaseFactory ?? throw new ArgumentNullException(nameof(umbracoDatabaseFactory)); + _contentValueSetBuilder = + contentValueSetBuilder ?? throw new ArgumentNullException(nameof(contentValueSetBuilder)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _publishedValuesOnly = publishedValuesOnly; + _parentId = parentId; + } + + private IQuery PublishedQuery => _publishedQuery ??= + _umbracoDatabaseFactory.SqlContext.Query().Where(x => x.Published); + + public override bool IsRegistered(IUmbracoContentIndex index) => + // check if it should populate based on published values + _publishedValuesOnly == index.PublishedValuesOnly; + + protected override void PopulateIndexes(IReadOnlyList indexes) + { + if (indexes.Count == 0) { + _logger.LogDebug( + $"{nameof(PopulateIndexes)} called with no indexes to populate. Typically means no index is registered with this populator."); + return; } - /// - /// Optional constructor allowing specifying custom query parameters - /// - public ContentIndexPopulator( - ILogger logger, - bool publishedValuesOnly, - int? parentId, - IContentService contentService, - IUmbracoDatabaseFactory umbracoDatabaseFactory, - IValueSetBuilder contentValueSetBuilder) + const int pageSize = 10000; + var pageIndex = 0; + + var contentParentId = -1; + if (_parentId.HasValue && _parentId.Value > 0) { - _contentService = contentService ?? throw new ArgumentNullException(nameof(contentService)); - _umbracoDatabaseFactory = umbracoDatabaseFactory ?? throw new ArgumentNullException(nameof(umbracoDatabaseFactory)); - _contentValueSetBuilder = contentValueSetBuilder ?? throw new ArgumentNullException(nameof(contentValueSetBuilder)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _publishedValuesOnly = publishedValuesOnly; - _parentId = parentId; + contentParentId = _parentId.Value; } - public override bool IsRegistered(IUmbracoContentIndex index) + if (_publishedValuesOnly) { - // check if it should populate based on published values - return _publishedValuesOnly == index.PublishedValuesOnly; + IndexPublishedContent(contentParentId, pageIndex, pageSize, indexes); } - - protected override void PopulateIndexes(IReadOnlyList indexes) + else { - if (indexes.Count == 0) - { - _logger.LogDebug($"{nameof(PopulateIndexes)} called with no indexes to populate. Typically means no index is registered with this populator."); - return; - } - - const int pageSize = 10000; - var pageIndex = 0; - - var contentParentId = -1; - if (_parentId.HasValue && _parentId.Value > 0) - { - contentParentId = _parentId.Value; - } - - if (_publishedValuesOnly) - { - IndexPublishedContent(contentParentId, pageIndex, pageSize, indexes); - } - else - { - IndexAllContent(contentParentId, pageIndex, pageSize, indexes); - } + IndexAllContent(contentParentId, pageIndex, pageSize, indexes); } + } + + protected void IndexAllContent(int contentParentId, int pageIndex, int pageSize, IReadOnlyList indexes) + { + IContent[] content; - protected void IndexAllContent(int contentParentId, int pageIndex, int pageSize, IReadOnlyList indexes) + do { - IContent[] content; + content = _contentService.GetPagedDescendants(contentParentId, pageIndex, pageSize, out _).ToArray(); - do + if (content.Length > 0) { - content = _contentService.GetPagedDescendants(contentParentId, pageIndex, pageSize, out _).ToArray(); + var valueSets = _contentValueSetBuilder.GetValueSets(content).ToList(); - if (content.Length > 0) + // ReSharper disable once PossibleMultipleEnumeration + foreach (IIndex index in indexes) { - var valueSets = _contentValueSetBuilder.GetValueSets(content).ToList(); - - // ReSharper disable once PossibleMultipleEnumeration - foreach (var index in indexes) - { - index.IndexItems(valueSets); - } + index.IndexItems(valueSets); } + } - pageIndex++; - } while (content.Length == pageSize); - } + pageIndex++; + } while (content.Length == pageSize); + } + + protected void IndexPublishedContent(int contentParentId, int pageIndex, int pageSize, + IReadOnlyList indexes) + { + IContent[] content; - protected void IndexPublishedContent(int contentParentId, int pageIndex, int pageSize, - IReadOnlyList indexes) + var publishedPages = new HashSet(); + + do { - IContent[] content; + //add the published filter + //note: We will filter for published variants in the validator + content = _contentService.GetPagedDescendants(contentParentId, pageIndex, pageSize, out _, PublishedQuery, + Ordering.By("Path")).ToArray(); - var publishedPages = new HashSet(); - do + if (content.Length > 0) { - //add the published filter - //note: We will filter for published variants in the validator - content = _contentService.GetPagedDescendants(contentParentId, pageIndex, pageSize, out _, PublishedQuery, - Ordering.By("Path", Direction.Ascending)).ToArray(); - + var indexableContent = new List(); - if (content.Length > 0) + foreach (IContent item in content) { - var indexableContent = new List(); - - foreach (var item in content) + if (item.Level == 1) { - if (item.Level == 1) + // first level pages are always published so no need to filter them + indexableContent.Add(item); + publishedPages.Add(item.Id); + } + else + { + if (publishedPages.Contains(item.ParentId)) { - // first level pages are always published so no need to filter them - indexableContent.Add(item); + // only index when parent is published publishedPages.Add(item.Id); - } - else - { - if (publishedPages.Contains(item.ParentId)) - { - // only index when parent is published - publishedPages.Add(item.Id); - indexableContent.Add(item); - } + indexableContent.Add(item); } } + } - var valueSets = _contentValueSetBuilder.GetValueSets(indexableContent.ToArray()).ToList(); + var valueSets = _contentValueSetBuilder.GetValueSets(indexableContent.ToArray()).ToList(); - foreach (IIndex index in indexes) - { - index.IndexItems(valueSets); - } + foreach (IIndex index in indexes) + { + index.IndexItems(valueSets); } + } - pageIndex++; - } while (content.Length == pageSize); - } + pageIndex++; + } while (content.Length == pageSize); } - - } diff --git a/src/Umbraco.Infrastructure/Examine/ContentValueSetBuilder.cs b/src/Umbraco.Infrastructure/Examine/ContentValueSetBuilder.cs index f42293a7a10e..7885343f14da 100644 --- a/src/Umbraco.Infrastructure/Examine/ContentValueSetBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/ContentValueSetBuilder.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; -using System.Linq; -using Examine; +using Examine; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.PropertyEditors; @@ -8,128 +6,144 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; +using IScope = Umbraco.Cms.Infrastructure.Scoping.IScope; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Builds s for items +/// +public class ContentValueSetBuilder : BaseValueSetBuilder, IContentValueSetBuilder, + IPublishedContentValueSetBuilder { - /// - /// Builds s for items - /// - public class ContentValueSetBuilder : BaseValueSetBuilder, IContentValueSetBuilder, IPublishedContentValueSetBuilder + private readonly IScopeProvider _scopeProvider; + + private readonly IShortStringHelper _shortStringHelper; + private readonly UrlSegmentProviderCollection _urlSegmentProviders; + private readonly IUserService _userService; + + public ContentValueSetBuilder(PropertyEditorCollection propertyEditors, + UrlSegmentProviderCollection urlSegmentProviders, + IUserService userService, + IShortStringHelper shortStringHelper, + IScopeProvider scopeProvider, + bool publishedValuesOnly) + : base(propertyEditors, publishedValuesOnly) { - private readonly UrlSegmentProviderCollection _urlSegmentProviders; - private readonly IUserService _userService; - private readonly IScopeProvider _scopeProvider; + _urlSegmentProviders = urlSegmentProviders; + _userService = userService; + _shortStringHelper = shortStringHelper; + _scopeProvider = scopeProvider; + } - private readonly IShortStringHelper _shortStringHelper; + /// + public override IEnumerable GetValueSets(params IContent[] content) + { + Dictionary creatorIds; + Dictionary writerIds; - public ContentValueSetBuilder(PropertyEditorCollection propertyEditors, - UrlSegmentProviderCollection urlSegmentProviders, - IUserService userService, - IShortStringHelper shortStringHelper, - IScopeProvider scopeProvider, - bool publishedValuesOnly) - : base(propertyEditors, publishedValuesOnly) + // We can lookup all of the creator/writer names at once which can save some + // processing below instead of one by one. + using (IScope scope = _scopeProvider.CreateScope()) { - _urlSegmentProviders = urlSegmentProviders; - _userService = userService; - _shortStringHelper = shortStringHelper; - _scopeProvider = scopeProvider; + creatorIds = _userService.GetProfilesById(content.Select(x => x.CreatorId).ToArray()) + .ToDictionary(x => x.Id, x => x); + writerIds = _userService.GetProfilesById(content.Select(x => x.WriterId).ToArray()) + .ToDictionary(x => x.Id, x => x); + scope.Complete(); } - /// - public override IEnumerable GetValueSets(params IContent[] content) - { - Dictionary creatorIds; - Dictionary writerIds; - - // We can lookup all of the creator/writer names at once which can save some - // processing below instead of one by one. - using (var scope = _scopeProvider.CreateScope()) - { - creatorIds = _userService.GetProfilesById(content.Select(x => x.CreatorId).ToArray()) - .ToDictionary(x => x.Id, x => x); - writerIds = _userService.GetProfilesById(content.Select(x => x.WriterId).ToArray()) - .ToDictionary(x => x.Id, x => x); - scope.Complete(); - } + return GetValueSetsEnumerable(content, creatorIds, writerIds); + } - return GetValueSetsEnumerable(content, creatorIds, writerIds); - } + private IEnumerable GetValueSetsEnumerable(IContent[] content, Dictionary creatorIds, + Dictionary writerIds) + { + // TODO: There is a lot of boxing going on here and ultimately all values will be boxed by Lucene anyways + // but I wonder if there's a way to reduce the boxing that we have to do or if it will matter in the end since + // Lucene will do it no matter what? One idea was to create a `FieldValue` struct which would contain `object`, `object[]`, `ValueType` and `ValueType[]` + // references and then each array is an array of `FieldValue[]` and values are assigned accordingly. Not sure if it will make a difference or not. - private IEnumerable GetValueSetsEnumerable(IContent[] content, Dictionary creatorIds, Dictionary writerIds) + foreach (IContent c in content) { - // TODO: There is a lot of boxing going on here and ultimately all values will be boxed by Lucene anyways - // but I wonder if there's a way to reduce the boxing that we have to do or if it will matter in the end since - // Lucene will do it no matter what? One idea was to create a `FieldValue` struct which would contain `object`, `object[]`, `ValueType` and `ValueType[]` - // references and then each array is an array of `FieldValue[]` and values are assigned accordingly. Not sure if it will make a difference or not. + var isVariant = c.ContentType.VariesByCulture(); - foreach (var c in content) + var urlValue = c.GetUrlSegment(_shortStringHelper, _urlSegmentProviders); //Always add invariant urlName + var values = new Dictionary> { - var isVariant = c.ContentType.VariesByCulture(); - - var urlValue = c.GetUrlSegment(_shortStringHelper, _urlSegmentProviders); //Always add invariant urlName - var values = new Dictionary> + {"icon", c.ContentType.Icon?.Yield() ?? Enumerable.Empty()}, + { + UmbracoExamineFieldNames.PublishedFieldName, new object[] {c.Published ? "y" : "n"} + }, //Always add invariant published value + {"id", new object[] {c.Id}}, + {UmbracoExamineFieldNames.NodeKeyFieldName, new object[] {c.Key}}, + {"parentID", new object[] {c.Level > 1 ? c.ParentId : -1}}, + {"level", new object[] {c.Level}}, + {"creatorID", new object[] {c.CreatorId}}, + {"sortOrder", new object[] {c.SortOrder}}, + {"createDate", new object[] {c.CreateDate}}, //Always add invariant createDate + {"updateDate", new object[] {c.UpdateDate}}, //Always add invariant updateDate { - {"icon", c.ContentType.Icon?.Yield() ?? Enumerable.Empty()}, - {UmbracoExamineFieldNames.PublishedFieldName, new object[] {c.Published ? "y" : "n"}}, //Always add invariant published value - {"id", new object[] {c.Id}}, - {UmbracoExamineFieldNames.NodeKeyFieldName, new object[] {c.Key}}, - {"parentID", new object[] {c.Level > 1 ? c.ParentId : -1}}, - {"level", new object[] {c.Level}}, - {"creatorID", new object[] {c.CreatorId}}, - {"sortOrder", new object[] {c.SortOrder}}, - {"createDate", new object[] {c.CreateDate}}, //Always add invariant createDate - {"updateDate", new object[] {c.UpdateDate}}, //Always add invariant updateDate - {UmbracoExamineFieldNames.NodeNameFieldName, (PublishedValuesOnly //Always add invariant nodeName + UmbracoExamineFieldNames.NodeNameFieldName, (PublishedValuesOnly //Always add invariant nodeName ? c.PublishName?.Yield() - : c.Name?.Yield()) ?? Enumerable.Empty()}, - {"urlName", urlValue?.Yield() ?? Enumerable.Empty()}, //Always add invariant urlName - {"path", c.Path?.Yield() ?? Enumerable.Empty()}, - {"nodeType", c.ContentType.Id.ToString().Yield() ?? Enumerable.Empty()}, - {"creatorName", (creatorIds.TryGetValue(c.CreatorId, out var creatorProfile) ? creatorProfile.Name! : "??").Yield() }, - {"writerName", (writerIds.TryGetValue(c.WriterId, out var writerProfile) ? writerProfile.Name! : "??").Yield() }, - {"writerID", new object[] {c.WriterId}}, - {"templateID", new object[] {c.TemplateId ?? 0}}, - {UmbracoExamineFieldNames.VariesByCultureFieldName, new object[] {"n"}}, - }; - - if (isVariant) + : c.Name?.Yield()) ?? Enumerable.Empty() + }, + {"urlName", urlValue?.Yield() ?? Enumerable.Empty()}, //Always add invariant urlName + {"path", c.Path?.Yield() ?? Enumerable.Empty()}, + {"nodeType", c.ContentType.Id.ToString().Yield() ?? Enumerable.Empty()}, + { + "creatorName", + (creatorIds.TryGetValue(c.CreatorId, out IProfile? creatorProfile) ? creatorProfile.Name! : "??") + .Yield() + }, { - values[UmbracoExamineFieldNames.VariesByCultureFieldName] = new object[] { "y" }; + "writerName", + (writerIds.TryGetValue(c.WriterId, out IProfile? writerProfile) ? writerProfile.Name! : "??") + .Yield() + }, + {"writerID", new object[] {c.WriterId}}, + {"templateID", new object[] {c.TemplateId ?? 0}}, + {UmbracoExamineFieldNames.VariesByCultureFieldName, new object[] {"n"}} + }; - foreach (var culture in c.AvailableCultures) - { - var variantUrl = c.GetUrlSegment(_shortStringHelper, _urlSegmentProviders, culture); - var lowerCulture = culture.ToLowerInvariant(); - values[$"urlName_{lowerCulture}"] = variantUrl?.Yield() ?? Enumerable.Empty(); - values[$"nodeName_{lowerCulture}"] = (PublishedValuesOnly - ? c.GetPublishName(culture)?.Yield() - : c.GetCultureName(culture)?.Yield()) ?? Enumerable.Empty(); - values[$"{UmbracoExamineFieldNames.PublishedFieldName}_{lowerCulture}"] = (c.IsCulturePublished(culture) ? "y" : "n").Yield(); - values[$"updateDate_{lowerCulture}"] = (PublishedValuesOnly - ? c.GetPublishDate(culture) - : c.GetUpdateDate(culture))?.Yield() ?? Enumerable.Empty(); - } + if (isVariant) + { + values[UmbracoExamineFieldNames.VariesByCultureFieldName] = new object[] {"y"}; + + foreach (var culture in c.AvailableCultures) + { + var variantUrl = c.GetUrlSegment(_shortStringHelper, _urlSegmentProviders, culture); + var lowerCulture = culture.ToLowerInvariant(); + values[$"urlName_{lowerCulture}"] = variantUrl?.Yield() ?? Enumerable.Empty(); + values[$"nodeName_{lowerCulture}"] = (PublishedValuesOnly + ? c.GetPublishName(culture)?.Yield() + : c.GetCultureName(culture)?.Yield()) ?? Enumerable.Empty(); + values[$"{UmbracoExamineFieldNames.PublishedFieldName}_{lowerCulture}"] = + (c.IsCulturePublished(culture) ? "y" : "n").Yield(); + values[$"updateDate_{lowerCulture}"] = (PublishedValuesOnly + ? c.GetPublishDate(culture) + : c.GetUpdateDate(culture))?.Yield() ?? Enumerable.Empty(); } + } - foreach (var property in c.Properties) + foreach (IProperty property in c.Properties) + { + if (!property.PropertyType.VariesByCulture()) { - if (!property.PropertyType.VariesByCulture()) - { - AddPropertyValue(property, null, null, values); - } - else + AddPropertyValue(property, null, null, values); + } + else + { + foreach (var culture in c.AvailableCultures) { - foreach (var culture in c.AvailableCultures) - AddPropertyValue(property, culture.ToLowerInvariant(), null, values); + AddPropertyValue(property, culture.ToLowerInvariant(), null, values); } } + } - var vs = new ValueSet(c.Id.ToInvariantString(), IndexTypes.Content, c.ContentType.Alias, values); + var vs = new ValueSet(c.Id.ToInvariantString(), IndexTypes.Content, c.ContentType.Alias, values); - yield return vs; - } + yield return vs; } } - } diff --git a/src/Umbraco.Infrastructure/Examine/ContentValueSetValidator.cs b/src/Umbraco.Infrastructure/Examine/ContentValueSetValidator.cs index ed57184cb849..54301af460c8 100644 --- a/src/Umbraco.Infrastructure/Examine/ContentValueSetValidator.cs +++ b/src/Umbraco.Infrastructure/Examine/ContentValueSetValidator.cs @@ -1,161 +1,192 @@ -using System.Collections.Generic; using System.Globalization; -using System.Linq; using Examine; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -using Constants = Umbraco.Cms.Core.Constants; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Used to validate a ValueSet for content/media - based on permissions, parent id, etc.... +/// +public class ContentValueSetValidator : ValueSetValidator, IContentValueSetValidator { - /// - /// Used to validate a ValueSet for content/media - based on permissions, parent id, etc.... - /// - public class ContentValueSetValidator : ValueSetValidator, IContentValueSetValidator + private const string PathKey = "path"; + private static readonly IEnumerable ValidCategories = new[] {IndexTypes.Content, IndexTypes.Media}; + private readonly IPublicAccessService? _publicAccessService; + private readonly IScopeProvider? _scopeProvider; + + // used for tests + public ContentValueSetValidator(bool publishedValuesOnly, int? parentId = null, + IEnumerable? includeItemTypes = null, IEnumerable? excludeItemTypes = null) + : this(publishedValuesOnly, true, null, null, parentId, includeItemTypes, excludeItemTypes) + { + } + + public ContentValueSetValidator(bool publishedValuesOnly, bool supportProtectedContent, + IPublicAccessService? publicAccessService, + IScopeProvider? scopeProvider, + int? parentId = null, + IEnumerable? includeItemTypes = null, IEnumerable? excludeItemTypes = null) + : base(includeItemTypes, excludeItemTypes, null, null) { - private readonly IPublicAccessService? _publicAccessService; - private readonly IScopeProvider? _scopeProvider; - private const string PathKey = "path"; - private static readonly IEnumerable ValidCategories = new[] { IndexTypes.Content, IndexTypes.Media }; - protected override IEnumerable ValidIndexCategories => ValidCategories; + PublishedValuesOnly = publishedValuesOnly; + SupportProtectedContent = supportProtectedContent; + ParentId = parentId; + _publicAccessService = publicAccessService; + _scopeProvider = scopeProvider; + } + + protected override IEnumerable ValidIndexCategories => ValidCategories; - public bool PublishedValuesOnly { get; } - public bool SupportProtectedContent { get; } - public int? ParentId { get; } + public bool PublishedValuesOnly { get; } + public bool SupportProtectedContent { get; } + public int? ParentId { get; } - public bool ValidatePath(string path, string category) + public bool ValidatePath(string path, string category) + { + //check if this document is a descendent of the parent + if (ParentId.HasValue && ParentId.Value > 0) { - //check if this document is a descendent of the parent - if (ParentId.HasValue && ParentId.Value > 0) + // we cannot return FAILED here because we need the value set to get into the indexer and then deal with it from there + // because we need to remove anything that doesn't pass by parent Id in the cases that umbraco data is moved to an illegal parent. + if (!path.Contains(string.Concat(",", ParentId.Value.ToString(CultureInfo.InvariantCulture), ","))) { - // we cannot return FAILED here because we need the value set to get into the indexer and then deal with it from there - // because we need to remove anything that doesn't pass by parent Id in the cases that umbraco data is moved to an illegal parent. - if (!path.Contains(string.Concat(",", ParentId.Value.ToString(CultureInfo.InvariantCulture), ","))) - return false; + return false; } - - return true; } - public bool ValidateRecycleBin(string path, string category) - { - var recycleBinId = category == IndexTypes.Content ? Constants.System.RecycleBinContentString : Constants.System.RecycleBinMediaString; + return true; + } - //check for recycle bin - if (PublishedValuesOnly) + public bool ValidateRecycleBin(string path, string category) + { + var recycleBinId = category == IndexTypes.Content + ? Constants.System.RecycleBinContentString + : Constants.System.RecycleBinMediaString; + + //check for recycle bin + if (PublishedValuesOnly) + { + if (path.Contains(string.Concat(",", recycleBinId, ","))) { - if (path.Contains(string.Concat(",", recycleBinId, ","))) - return false; + return false; } - return true; } - public bool ValidateProtectedContent(string path, string category) + return true; + } + + public bool ValidateProtectedContent(string path, string category) + { + if (category == IndexTypes.Content && !SupportProtectedContent) { - if (category == IndexTypes.Content && !SupportProtectedContent) + //if the service is null we can't look this up so we'll return false + if (_publicAccessService == null || _scopeProvider == null) { - //if the service is null we can't look this up so we'll return false - if (_publicAccessService == null || _scopeProvider == null) - { - return false; - } + return false; + } - // explicit scope since we may be in a background thread - using (_scopeProvider.CreateScope(autoComplete: true)) + // explicit scope since we may be in a background thread + using (_scopeProvider.CreateScope(autoComplete: true)) + { + if (_publicAccessService.IsProtected(path).Success) { - if (_publicAccessService.IsProtected(path).Success) - { - return false; - } + return false; } } - - return true; } - // used for tests - public ContentValueSetValidator(bool publishedValuesOnly, int? parentId = null, - IEnumerable? includeItemTypes = null, IEnumerable? excludeItemTypes = null) - : this(publishedValuesOnly, true, null, null, parentId, includeItemTypes, excludeItemTypes) - { - } + return true; + } - public ContentValueSetValidator(bool publishedValuesOnly, bool supportProtectedContent, - IPublicAccessService? publicAccessService, - IScopeProvider? scopeProvider, - int? parentId = null, - IEnumerable? includeItemTypes = null, IEnumerable? excludeItemTypes = null) - : base(includeItemTypes, excludeItemTypes, null, null) + public override ValueSetValidationResult Validate(ValueSet valueSet) + { + ValueSetValidationResult baseValidate = base.Validate(valueSet); + valueSet = baseValidate.ValueSet; + if (baseValidate.Status == ValueSetValidationStatus.Failed) { - PublishedValuesOnly = publishedValuesOnly; - SupportProtectedContent = supportProtectedContent; - ParentId = parentId; - _publicAccessService = publicAccessService; - _scopeProvider = scopeProvider; + return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); } - public override ValueSetValidationResult Validate(ValueSet valueSet) + var isFiltered = baseValidate.Status == ValueSetValidationStatus.Filtered; + + var filteredValues = valueSet.Values.ToDictionary(x => x.Key, x => x.Value.ToList()); + //check for published content + if (valueSet.Category == IndexTypes.Content && PublishedValuesOnly) { - var baseValidate = base.Validate(valueSet); - valueSet = baseValidate.ValueSet; - if (baseValidate.Status == ValueSetValidationStatus.Failed) + if (!valueSet.Values.TryGetValue(UmbracoExamineFieldNames.PublishedFieldName, + out IReadOnlyList? published)) + { return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); + } - var isFiltered = baseValidate.Status == ValueSetValidationStatus.Filtered; - - var filteredValues = valueSet.Values.ToDictionary(x => x.Key, x => x.Value.ToList()); - //check for published content - if (valueSet.Category == IndexTypes.Content && PublishedValuesOnly) + if (!published[0].Equals("y")) { - if (!valueSet.Values.TryGetValue(UmbracoExamineFieldNames.PublishedFieldName, out var published)) - { - return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); - } - - if (!published[0].Equals("y")) - { - return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); - } + return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); + } - //deal with variants, if there are unpublished variants than we need to remove them from the value set - if (valueSet.Values.TryGetValue(UmbracoExamineFieldNames.VariesByCultureFieldName, out var variesByCulture) - && variesByCulture.Count > 0 && variesByCulture[0].Equals("y")) + //deal with variants, if there are unpublished variants than we need to remove them from the value set + if (valueSet.Values.TryGetValue(UmbracoExamineFieldNames.VariesByCultureFieldName, + out IReadOnlyList? variesByCulture) + && variesByCulture.Count > 0 && variesByCulture[0].Equals("y")) + { + //so this valueset is for a content that varies by culture, now check for non-published cultures and remove those values + foreach (KeyValuePair> publishField in valueSet.Values + .Where(x => x.Key.StartsWith($"{UmbracoExamineFieldNames.PublishedFieldName}_")).ToList()) { - //so this valueset is for a content that varies by culture, now check for non-published cultures and remove those values - foreach (var publishField in valueSet.Values.Where(x => x.Key.StartsWith($"{UmbracoExamineFieldNames.PublishedFieldName}_")).ToList()) + if (publishField.Value.Count <= 0 || !publishField.Value[0].Equals("y")) { - if (publishField.Value.Count <= 0 || !publishField.Value[0].Equals("y")) + //this culture is not published, so remove all of these culture values + var cultureSuffix = publishField.Key.Substring(publishField.Key.LastIndexOf('_')); + foreach (KeyValuePair> cultureField in valueSet.Values + .Where(x => x.Key.InvariantEndsWith(cultureSuffix)).ToList()) { - //this culture is not published, so remove all of these culture values - var cultureSuffix = publishField.Key.Substring(publishField.Key.LastIndexOf('_')); - foreach (var cultureField in valueSet.Values.Where(x => x.Key.InvariantEndsWith(cultureSuffix)).ToList()) - { - filteredValues.Remove(cultureField.Key); - isFiltered = true; - } + filteredValues.Remove(cultureField.Key); + isFiltered = true; } } } } + } - //must have a 'path' - if (!valueSet.Values.TryGetValue(PathKey, out var pathValues)) return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); - if (pathValues.Count == 0) return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); - if (pathValues[0] == null) return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); - if (pathValues[0].ToString().IsNullOrWhiteSpace()) return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); - var path = pathValues[0].ToString(); - - var filteredValueSet = new ValueSet(valueSet.Id, valueSet.Category, valueSet.ItemType, filteredValues.ToDictionary(x=>x.Key, x=> (IEnumerable)x.Value)); - // We need to validate the path of the content based on ParentId, protected content and recycle bin rules. - // We cannot return FAILED here because we need the value set to get into the indexer and then deal with it from there - // because we need to remove anything that doesn't pass by protected content in the cases that umbraco data is moved to an illegal parent. - if (!ValidatePath(path!, valueSet.Category) - || !ValidateRecycleBin(path!, valueSet.Category) - || !ValidateProtectedContent(path!, valueSet.Category)) - return new ValueSetValidationResult(ValueSetValidationStatus.Filtered, filteredValueSet); - - return new ValueSetValidationResult(isFiltered ? ValueSetValidationStatus.Filtered : ValueSetValidationStatus.Valid, filteredValueSet); + //must have a 'path' + if (!valueSet.Values.TryGetValue(PathKey, out IReadOnlyList? pathValues)) + { + return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); } + + if (pathValues.Count == 0) + { + return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); + } + + if (pathValues[0] == null) + { + return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); + } + + if (pathValues[0].ToString().IsNullOrWhiteSpace()) + { + return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); + } + + var path = pathValues[0].ToString(); + + var filteredValueSet = new ValueSet(valueSet.Id, valueSet.Category, valueSet.ItemType, + filteredValues.ToDictionary(x => x.Key, x => (IEnumerable)x.Value)); + // We need to validate the path of the content based on ParentId, protected content and recycle bin rules. + // We cannot return FAILED here because we need the value set to get into the indexer and then deal with it from there + // because we need to remove anything that doesn't pass by protected content in the cases that umbraco data is moved to an illegal parent. + if (!ValidatePath(path!, valueSet.Category) + || !ValidateRecycleBin(path!, valueSet.Category) + || !ValidateProtectedContent(path!, valueSet.Category)) + { + return new ValueSetValidationResult(ValueSetValidationStatus.Filtered, filteredValueSet); + } + + return new ValueSetValidationResult( + isFiltered ? ValueSetValidationStatus.Filtered : ValueSetValidationStatus.Valid, filteredValueSet); } } diff --git a/src/Umbraco.Infrastructure/Examine/ExamineExtensions.cs b/src/Umbraco.Infrastructure/Examine/ExamineExtensions.cs index 076353a990b0..00726290c02a 100644 --- a/src/Umbraco.Infrastructure/Examine/ExamineExtensions.cs +++ b/src/Umbraco.Infrastructure/Examine/ExamineExtensions.cs @@ -1,94 +1,101 @@ -using System; -using System.Collections.Generic; using System.Globalization; using Examine; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Infrastructure.Examine; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Extension methods for Examine. +/// +public static class ExamineExtensions { /// - /// Extension methods for Examine. + /// Creates an containing all content from the + /// . /// - public static class ExamineExtensions + /// The search results. + /// The cache to fetch the content from. + /// + /// An containing all content. + /// + /// cache + /// + /// Search results are skipped if it can't be fetched from the by its integer id. + /// + public static IEnumerable ToPublishedSearchResults(this IEnumerable results, + IPublishedCache? cache) { - /// - /// Creates an containing all content from the . - /// - /// The search results. - /// The cache to fetch the content from. - /// - /// An containing all content. - /// - /// cache - /// - /// Search results are skipped if it can't be fetched from the by its integer id. - /// - public static IEnumerable ToPublishedSearchResults(this IEnumerable results, IPublishedCache? cache) + if (cache == null) { - if (cache == null) throw new ArgumentNullException(nameof(cache)); + throw new ArgumentNullException(nameof(cache)); + } - var publishedSearchResults = new List(); + var publishedSearchResults = new List(); - foreach (var result in results) + foreach (ISearchResult result in results) + { + if (int.TryParse(result.Id, NumberStyles.Integer, CultureInfo.InvariantCulture, out var contentId) && + cache.GetById(contentId) is IPublishedContent content) { - if (int.TryParse(result.Id, NumberStyles.Integer, CultureInfo.InvariantCulture, out var contentId) && - cache.GetById(contentId) is IPublishedContent content) - { - publishedSearchResults.Add(new PublishedSearchResult(content, result.Score)); - } + publishedSearchResults.Add(new PublishedSearchResult(content, result.Score)); } - - return publishedSearchResults; } - /// - /// Creates an containing all content, media or members from the . - /// - /// The search results. - /// The snapshot. - /// - /// An containing all content, media or members. - /// - /// snapshot - /// - /// Search results are skipped if it can't be fetched from the respective cache by its integer id. - /// - public static IEnumerable ToPublishedSearchResults(this IEnumerable results, IPublishedSnapshot snapshot) + return publishedSearchResults; + } + + /// + /// Creates an containing all content, media or members from the + /// . + /// + /// The search results. + /// The snapshot. + /// + /// An containing all content, media or members. + /// + /// snapshot + /// + /// Search results are skipped if it can't be fetched from the respective cache by its integer id. + /// + public static IEnumerable ToPublishedSearchResults(this IEnumerable results, + IPublishedSnapshot snapshot) + { + if (snapshot == null) { - if (snapshot == null) throw new ArgumentNullException(nameof(snapshot)); + throw new ArgumentNullException(nameof(snapshot)); + } - var publishedSearchResults = new List(); + var publishedSearchResults = new List(); - foreach (var result in results) + foreach (ISearchResult result in results) + { + if (int.TryParse(result.Id, NumberStyles.Integer, CultureInfo.InvariantCulture, out var contentId) && + result.Values.TryGetValue(ExamineFieldNames.CategoryFieldName, out var indexType)) { - if (int.TryParse(result.Id, NumberStyles.Integer, CultureInfo.InvariantCulture, out var contentId) && - result.Values.TryGetValue(ExamineFieldNames.CategoryFieldName, out var indexType)) + IPublishedContent? content; + switch (indexType) { - IPublishedContent? content; - switch (indexType) - { - case IndexTypes.Content: - content = snapshot.Content?.GetById(contentId); - break; - case IndexTypes.Media: - content = snapshot.Media?.GetById(contentId); - break; - case IndexTypes.Member: - throw new NotSupportedException("Cannot convert search results to member instances"); - default: - continue; - } + case IndexTypes.Content: + content = snapshot.Content?.GetById(contentId); + break; + case IndexTypes.Media: + content = snapshot.Media?.GetById(contentId); + break; + case IndexTypes.Member: + throw new NotSupportedException("Cannot convert search results to member instances"); + default: + continue; + } - if (content != null) - { - publishedSearchResults.Add(new PublishedSearchResult(content, result.Score)); - } + if (content != null) + { + publishedSearchResults.Add(new PublishedSearchResult(content, result.Score)); } } - - return publishedSearchResults; } + + return publishedSearchResults; } } diff --git a/src/Umbraco.Infrastructure/Examine/ExamineIndexModel.cs b/src/Umbraco.Infrastructure/Examine/ExamineIndexModel.cs index bb5a4f5a4676..7ed2b9a7c48d 100644 --- a/src/Umbraco.Infrastructure/Examine/ExamineIndexModel.cs +++ b/src/Umbraco.Infrastructure/Examine/ExamineIndexModel.cs @@ -1,25 +1,18 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Infrastructure.Examine -{ - [DataContract(Name = "indexer", Namespace = "")] - public class ExamineIndexModel - { - [DataMember(Name = "name")] - public string? Name { get; set; } +namespace Umbraco.Cms.Infrastructure.Examine; - [DataMember(Name = "healthStatus")] - public string? HealthStatus { get; set; } +[DataContract(Name = "indexer", Namespace = "")] +public class ExamineIndexModel +{ + [DataMember(Name = "name")] public string? Name { get; set; } - [DataMember(Name = "isHealthy")] - public bool IsHealthy => HealthStatus == "Healthy"; + [DataMember(Name = "healthStatus")] public string? HealthStatus { get; set; } - [DataMember(Name = "providerProperties")] - public IReadOnlyDictionary? ProviderProperties { get; set; } + [DataMember(Name = "isHealthy")] public bool IsHealthy => HealthStatus == "Healthy"; - [DataMember(Name = "canRebuild")] - public bool CanRebuild { get; set; } + [DataMember(Name = "providerProperties")] + public IReadOnlyDictionary? ProviderProperties { get; set; } - } + [DataMember(Name = "canRebuild")] public bool CanRebuild { get; set; } } diff --git a/src/Umbraco.Infrastructure/Examine/ExamineIndexRebuilder.cs b/src/Umbraco.Infrastructure/Examine/ExamineIndexRebuilder.cs index ef6d3619706a..a1c70d0ec3a6 100644 --- a/src/Umbraco.Infrastructure/Examine/ExamineIndexRebuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/ExamineIndexRebuilder.cs @@ -1,11 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using Examine; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; @@ -13,203 +8,205 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.HostedServices; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +public class ExamineIndexRebuilder : IIndexRebuilder { - public class ExamineIndexRebuilder : IIndexRebuilder + private readonly IBackgroundTaskQueue _backgroundTaskQueue; + private readonly IExamineManager _examineManager; + private readonly ILogger _logger; + private readonly IMainDom _mainDom; + private readonly IEnumerable _populators; + private readonly object _rebuildLocker = new(); + private readonly IRuntimeState _runtimeState; + + /// + /// Initializes a new instance of the class. + /// + public ExamineIndexRebuilder( + IMainDom mainDom, + IRuntimeState runtimeState, + ILogger logger, + IExamineManager examineManager, + IEnumerable populators, + IBackgroundTaskQueue backgroundTaskQueue) { - private readonly IBackgroundTaskQueue _backgroundTaskQueue; - private readonly IMainDom _mainDom; - private readonly IRuntimeState _runtimeState; - private readonly ILogger _logger; - private readonly IExamineManager _examineManager; - private readonly IEnumerable _populators; - private readonly object _rebuildLocker = new(); - - /// - /// Initializes a new instance of the class. - /// - public ExamineIndexRebuilder( - IMainDom mainDom, - IRuntimeState runtimeState, - ILogger logger, - IExamineManager examineManager, - IEnumerable populators, - IBackgroundTaskQueue backgroundTaskQueue) + _mainDom = mainDom; + _runtimeState = runtimeState; + _logger = logger; + _examineManager = examineManager; + _populators = populators; + _backgroundTaskQueue = backgroundTaskQueue; + } + + public bool CanRebuild(string indexName) + { + if (!_examineManager.TryGetIndex(indexName, out IIndex index)) { - _mainDom = mainDom; - _runtimeState = runtimeState; - _logger = logger; - _examineManager = examineManager; - _populators = populators; - _backgroundTaskQueue = backgroundTaskQueue; + throw new InvalidOperationException("No index found by name " + indexName); } - public bool CanRebuild(string indexName) + return _populators.Any(x => x.IsRegistered(index)); + } + + public virtual void RebuildIndex(string indexName, TimeSpan? delay = null, bool useBackgroundThread = true) + { + if (delay == null) { - if (!_examineManager.TryGetIndex(indexName, out IIndex index)) - { - throw new InvalidOperationException("No index found by name " + indexName); - } + delay = TimeSpan.Zero; + } - return _populators.Any(x => x.IsRegistered(index)); + if (!CanRun()) + { + return; } - public virtual void RebuildIndex(string indexName, TimeSpan? delay = null, bool useBackgroundThread = true) + if (useBackgroundThread) { - if (delay == null) - { - delay = TimeSpan.Zero; - } + _logger.LogInformation("Starting async background thread for rebuilding index {indexName}.", indexName); - if (!CanRun()) - { - return; - } + _backgroundTaskQueue.QueueBackgroundWorkItem( + cancellationToken => Task.Run(() => RebuildIndex(indexName, delay.Value, cancellationToken))); + } + else + { + RebuildIndex(indexName, delay.Value, CancellationToken.None); + } + } - if (useBackgroundThread) - { - _logger.LogInformation("Starting async background thread for rebuilding index {indexName}.",indexName); + public virtual void RebuildIndexes(bool onlyEmptyIndexes, TimeSpan? delay = null, bool useBackgroundThread = true) + { + if (delay == null) + { + delay = TimeSpan.Zero; + } - _backgroundTaskQueue.QueueBackgroundWorkItem( - cancellationToken => Task.Run(() => RebuildIndex(indexName, delay.Value, cancellationToken))); - } - else - { - RebuildIndex(indexName, delay.Value, CancellationToken.None); - } + if (!CanRun()) + { + return; } - public virtual void RebuildIndexes(bool onlyEmptyIndexes, TimeSpan? delay = null, bool useBackgroundThread = true) + if (useBackgroundThread) { - if (delay == null) - { - delay = TimeSpan.Zero; - } + _logger.LogDebug($"Queuing background job for {nameof(RebuildIndexes)}."); - if (!CanRun()) - { - return; - } + _backgroundTaskQueue.QueueBackgroundWorkItem( + cancellationToken => + { + // This is a fire/forget task spawned by the background thread queue (which means we + // don't need to worry about ExecutionContext flowing). + Task.Run(() => RebuildIndexes(onlyEmptyIndexes, delay.Value, cancellationToken)); - if (useBackgroundThread) - { - _logger.LogDebug($"Queuing background job for {nameof(RebuildIndexes)}."); + // immediately return so the queue isn't waiting. + return Task.CompletedTask; + }); + } + else + { + RebuildIndexes(onlyEmptyIndexes, delay.Value, CancellationToken.None); + } + } - _backgroundTaskQueue.QueueBackgroundWorkItem( - cancellationToken => - { - // This is a fire/forget task spawned by the background thread queue (which means we - // don't need to worry about ExecutionContext flowing). - Task.Run(() => RebuildIndexes(onlyEmptyIndexes, delay.Value, cancellationToken)); + private bool CanRun() => _mainDom.IsMainDom && _runtimeState.Level == RuntimeLevel.Run; - // immediately return so the queue isn't waiting. - return Task.CompletedTask; - }); - } - else - { - RebuildIndexes(onlyEmptyIndexes, delay.Value, CancellationToken.None); - } + private void RebuildIndex(string indexName, TimeSpan delay, CancellationToken cancellationToken) + { + if (delay > TimeSpan.Zero) + { + Thread.Sleep(delay); } - private bool CanRun() => _mainDom.IsMainDom && _runtimeState.Level == RuntimeLevel.Run; - - private void RebuildIndex(string indexName, TimeSpan delay, CancellationToken cancellationToken) + try { - if (delay > TimeSpan.Zero) + if (!Monitor.TryEnter(_rebuildLocker)) { - Thread.Sleep(delay); + _logger.LogWarning( + "Call was made to RebuildIndexes but the task runner for rebuilding is already running"); } - - try + else { - if (!Monitor.TryEnter(_rebuildLocker)) + if (!_examineManager.TryGetIndex(indexName, out IIndex index)) { - _logger.LogWarning("Call was made to RebuildIndexes but the task runner for rebuilding is already running"); + throw new InvalidOperationException($"No index found with name {indexName}"); } - else + + index.CreateIndex(); // clear the index + foreach (IIndexPopulator populator in _populators) { - if (!_examineManager.TryGetIndex(indexName, out IIndex index)) + if (cancellationToken.IsCancellationRequested) { - throw new InvalidOperationException($"No index found with name {indexName}"); + return; } - index.CreateIndex(); // clear the index - foreach (IIndexPopulator populator in _populators) - { - if (cancellationToken.IsCancellationRequested) - { - return; - } - - populator.Populate(index); - } + populator.Populate(index); } } - finally + } + finally + { + if (Monitor.IsEntered(_rebuildLocker)) { - if (Monitor.IsEntered(_rebuildLocker)) - { - Monitor.Exit(_rebuildLocker); - } + Monitor.Exit(_rebuildLocker); } } + } - private void RebuildIndexes(bool onlyEmptyIndexes, TimeSpan delay, CancellationToken cancellationToken) + private void RebuildIndexes(bool onlyEmptyIndexes, TimeSpan delay, CancellationToken cancellationToken) + { + if (delay > TimeSpan.Zero) { - if (delay > TimeSpan.Zero) + Thread.Sleep(delay); + } + + try + { + if (!Monitor.TryEnter(_rebuildLocker)) { - Thread.Sleep(delay); + _logger.LogWarning( + $"Call was made to {nameof(RebuildIndexes)} but the task runner for rebuilding is already running"); } - - try + else { - if (!Monitor.TryEnter(_rebuildLocker)) + // If an index exists but it has zero docs we'll consider it empty and rebuild + IIndex[] indexes = (onlyEmptyIndexes + ? _examineManager.Indexes.Where(x => + !x.IndexExists() || (x is IIndexStats stats && stats.GetDocumentCount() == 0)) + : _examineManager.Indexes).ToArray(); + + if (indexes.Length == 0) { - _logger.LogWarning($"Call was made to {nameof(RebuildIndexes)} but the task runner for rebuilding is already running"); + return; } - else + + foreach (IIndex index in indexes) { - // If an index exists but it has zero docs we'll consider it empty and rebuild - IIndex[] indexes = (onlyEmptyIndexes - ? _examineManager.Indexes.Where(x => !x.IndexExists() || (x is IIndexStats stats && stats.GetDocumentCount() == 0)) - : _examineManager.Indexes).ToArray(); + index.CreateIndex(); // clear the index + } - if (indexes.Length == 0) + // run each populator over the indexes + foreach (IIndexPopulator populator in _populators) + { + if (cancellationToken.IsCancellationRequested) { return; } - foreach (IIndex index in indexes) + try { - index.CreateIndex(); // clear the index + populator.Populate(indexes); } - - // run each populator over the indexes - foreach (IIndexPopulator populator in _populators) + catch (Exception e) { - if (cancellationToken.IsCancellationRequested) - { - return; - } - - try - { - populator.Populate(indexes); - } - catch (Exception e) - { - _logger.LogError(e, "Index populating failed for populator {Populator}", populator.GetType()); - } + _logger.LogError(e, "Index populating failed for populator {Populator}", populator.GetType()); } } } - finally + } + finally + { + if (Monitor.IsEntered(_rebuildLocker)) { - if (Monitor.IsEntered(_rebuildLocker)) - { - Monitor.Exit(_rebuildLocker); - } + Monitor.Exit(_rebuildLocker); } } } diff --git a/src/Umbraco.Infrastructure/Examine/ExamineSearcherModel.cs b/src/Umbraco.Infrastructure/Examine/ExamineSearcherModel.cs index 1fd30de319bb..d7195a7c596f 100644 --- a/src/Umbraco.Infrastructure/Examine/ExamineSearcherModel.cs +++ b/src/Umbraco.Infrastructure/Examine/ExamineSearcherModel.cs @@ -1,17 +1,9 @@ using System.Runtime.Serialization; -namespace Umbraco.Cms.Infrastructure.Examine -{ - [DataContract(Name = "searcher", Namespace = "")] - public class ExamineSearcherModel - { - public ExamineSearcherModel() - { - } - - [DataMember(Name = "name")] - public string? Name { get; set; } - - } +namespace Umbraco.Cms.Infrastructure.Examine; +[DataContract(Name = "searcher", Namespace = "")] +public class ExamineSearcherModel +{ + [DataMember(Name = "name")] public string? Name { get; set; } } diff --git a/src/Umbraco.Infrastructure/Examine/ExamineUmbracoIndexingHandler.cs b/src/Umbraco.Infrastructure/Examine/ExamineUmbracoIndexingHandler.cs index c8a07f619333..61eca5a64e26 100644 --- a/src/Umbraco.Infrastructure/Examine/ExamineUmbracoIndexingHandler.cs +++ b/src/Umbraco.Infrastructure/Examine/ExamineUmbracoIndexingHandler.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; -using System.Threading.Tasks; using Examine; using Examine.Search; using Microsoft.Extensions.Logging; @@ -14,425 +10,454 @@ using Umbraco.Cms.Infrastructure.Search; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Indexing handler for Examine indexes +/// +internal class ExamineUmbracoIndexingHandler : IUmbracoIndexingHandler { - /// - /// Indexing handler for Examine indexes - /// - internal class ExamineUmbracoIndexingHandler : IUmbracoIndexingHandler + // the default enlist priority is 100 + // enlist with a lower priority to ensure that anything "default" runs after us + // but greater that SafeXmlReaderWriter priority which is 60 + private const int EnlistPriority = 80; + private readonly IBackgroundTaskQueue _backgroundTaskQueue; + private readonly IContentValueSetBuilder _contentValueSetBuilder; + private readonly Lazy _enabled; + private readonly IExamineManager _examineManager; + private readonly ILogger _logger; + private readonly IMainDom _mainDom; + private readonly IValueSetBuilder _mediaValueSetBuilder; + private readonly IValueSetBuilder _memberValueSetBuilder; + private readonly IProfilingLogger _profilingLogger; + private readonly IPublishedContentValueSetBuilder _publishedContentValueSetBuilder; + private readonly ICoreScopeProvider _scopeProvider; + + public ExamineUmbracoIndexingHandler( + IMainDom mainDom, + ILogger logger, + IProfilingLogger profilingLogger, + ICoreScopeProvider scopeProvider, + IExamineManager examineManager, + IBackgroundTaskQueue backgroundTaskQueue, + IContentValueSetBuilder contentValueSetBuilder, + IPublishedContentValueSetBuilder publishedContentValueSetBuilder, + IValueSetBuilder mediaValueSetBuilder, + IValueSetBuilder memberValueSetBuilder) { - // the default enlist priority is 100 - // enlist with a lower priority to ensure that anything "default" runs after us - // but greater that SafeXmlReaderWriter priority which is 60 - private const int EnlistPriority = 80; - private readonly IMainDom _mainDom; - private readonly ILogger _logger; - private readonly IProfilingLogger _profilingLogger; - private readonly ICoreScopeProvider _scopeProvider; - private readonly IExamineManager _examineManager; - private readonly IBackgroundTaskQueue _backgroundTaskQueue; - private readonly IContentValueSetBuilder _contentValueSetBuilder; - private readonly IPublishedContentValueSetBuilder _publishedContentValueSetBuilder; - private readonly IValueSetBuilder _mediaValueSetBuilder; - private readonly IValueSetBuilder _memberValueSetBuilder; - private readonly Lazy _enabled; - - public ExamineUmbracoIndexingHandler( - IMainDom mainDom, - ILogger logger, - IProfilingLogger profilingLogger, - ICoreScopeProvider scopeProvider, - IExamineManager examineManager, - IBackgroundTaskQueue backgroundTaskQueue, - IContentValueSetBuilder contentValueSetBuilder, - IPublishedContentValueSetBuilder publishedContentValueSetBuilder, - IValueSetBuilder mediaValueSetBuilder, - IValueSetBuilder memberValueSetBuilder) + _mainDom = mainDom; + _logger = logger; + _profilingLogger = profilingLogger; + _scopeProvider = scopeProvider; + _examineManager = examineManager; + _backgroundTaskQueue = backgroundTaskQueue; + _contentValueSetBuilder = contentValueSetBuilder; + _publishedContentValueSetBuilder = publishedContentValueSetBuilder; + _mediaValueSetBuilder = mediaValueSetBuilder; + _memberValueSetBuilder = memberValueSetBuilder; + _enabled = new Lazy(IsEnabled); + } + + /// + public bool Enabled => _enabled.Value; + + /// + public void DeleteIndexForEntity(int entityId, bool keepIfUnpublished) + { + var actions = DeferedActions.Get(_scopeProvider); + if (actions != null) { - _mainDom = mainDom; - _logger = logger; - _profilingLogger = profilingLogger; - _scopeProvider = scopeProvider; - _examineManager = examineManager; - _backgroundTaskQueue = backgroundTaskQueue; - _contentValueSetBuilder = contentValueSetBuilder; - _publishedContentValueSetBuilder = publishedContentValueSetBuilder; - _mediaValueSetBuilder = mediaValueSetBuilder; - _memberValueSetBuilder = memberValueSetBuilder; - _enabled = new Lazy(IsEnabled); + actions.Add(new DeferedDeleteIndex(this, entityId, keepIfUnpublished)); } - - /// - /// Used to lazily check if Examine Index handling is enabled - /// - /// - private bool IsEnabled() + else { - //let's deal with shutting down Examine with MainDom - var examineShutdownRegistered = _mainDom.Register(release: () => - { - using (_profilingLogger.TraceDuration("Examine shutting down")) - { - _examineManager.Dispose(); - } - }); - - if (!examineShutdownRegistered) - { - _logger.LogInformation("Examine shutdown not registered, this AppDomain is not the MainDom, Examine will be disabled"); - - //if we could not register the shutdown examine ourselves, it means we are not maindom! in this case all of examine should be disabled! - Suspendable.ExamineEvents.SuspendIndexers(_logger); - return false; //exit, do not continue - } - - _logger.LogDebug("Examine shutdown registered with MainDom"); - - var registeredIndexers = _examineManager.Indexes.OfType().Count(x => x.EnableDefaultEventHandler); - - _logger.LogInformation("Adding examine event handlers for {RegisteredIndexers} index providers.", registeredIndexers); - - // don't bind event handlers if we're not suppose to listen - if (registeredIndexers == 0) - { - return false; - } - - return true; + DeferedDeleteIndex.Execute(this, entityId, keepIfUnpublished); } + } - /// - public bool Enabled => _enabled.Value; - - /// - public void DeleteIndexForEntity(int entityId, bool keepIfUnpublished) + /// + public void DeleteIndexForEntities(IReadOnlyCollection entityIds, bool keepIfUnpublished) + { + var actions = DeferedActions.Get(_scopeProvider); + if (actions != null) { - var actions = DeferedActions.Get(_scopeProvider); - if (actions != null) - { - actions.Add(new DeferedDeleteIndex(this, entityId, keepIfUnpublished)); - } - else - { - DeferedDeleteIndex.Execute(this, entityId, keepIfUnpublished); - } + actions.Add(new DeferedDeleteIndex(this, entityIds, keepIfUnpublished)); } - - /// - public void DeleteIndexForEntities(IReadOnlyCollection entityIds, bool keepIfUnpublished) + else { - var actions = DeferedActions.Get(_scopeProvider); - if (actions != null) - { - actions.Add(new DeferedDeleteIndex(this, entityIds, keepIfUnpublished)); - } - else - { - DeferedDeleteIndex.Execute(this, entityIds, keepIfUnpublished); - } + DeferedDeleteIndex.Execute(this, entityIds, keepIfUnpublished); } + } - /// - public void ReIndexForContent(IContent sender, bool isPublished) + /// + public void ReIndexForContent(IContent sender, bool isPublished) + { + var actions = DeferedActions.Get(_scopeProvider); + if (actions != null) { - var actions = DeferedActions.Get(_scopeProvider); - if (actions != null) - { - actions.Add(new DeferedReIndexForContent(_backgroundTaskQueue, this, sender, isPublished)); - } - else - { - DeferedReIndexForContent.Execute(_backgroundTaskQueue, this, sender, isPublished); - } + actions.Add(new DeferedReIndexForContent(_backgroundTaskQueue, this, sender, isPublished)); } - - /// - public void ReIndexForMedia(IMedia sender, bool isPublished) + else { - var actions = DeferedActions.Get(_scopeProvider); - if (actions != null) - { - actions.Add(new DeferedReIndexForMedia(_backgroundTaskQueue, this, sender, isPublished)); - } - else - { - DeferedReIndexForMedia.Execute(_backgroundTaskQueue, this, sender, isPublished); - } + DeferedReIndexForContent.Execute(_backgroundTaskQueue, this, sender, isPublished); } + } - /// - public void ReIndexForMember(IMember member) + /// + public void ReIndexForMedia(IMedia sender, bool isPublished) + { + var actions = DeferedActions.Get(_scopeProvider); + if (actions != null) { - var actions = DeferedActions.Get(_scopeProvider); - if (actions != null) - { - actions.Add(new DeferedReIndexForMember(_backgroundTaskQueue, this, member)); - } - else - { - DeferedReIndexForMember.Execute(_backgroundTaskQueue, this, member); - } + actions.Add(new DeferedReIndexForMedia(_backgroundTaskQueue, this, sender, isPublished)); + } + else + { + DeferedReIndexForMedia.Execute(_backgroundTaskQueue, this, sender, isPublished); } + } - /// - public void DeleteDocumentsForContentTypes(IReadOnlyCollection removedContentTypes) + /// + public void ReIndexForMember(IMember member) + { + var actions = DeferedActions.Get(_scopeProvider); + if (actions != null) { - const int pageSize = 500; + actions.Add(new DeferedReIndexForMember(_backgroundTaskQueue, this, member)); + } + else + { + DeferedReIndexForMember.Execute(_backgroundTaskQueue, this, member); + } + } - //Delete all content of this content/media/member type that is in any content indexer by looking up matched examine docs - foreach (var id in removedContentTypes) + /// + public void DeleteDocumentsForContentTypes(IReadOnlyCollection removedContentTypes) + { + const int pageSize = 500; + + //Delete all content of this content/media/member type that is in any content indexer by looking up matched examine docs + foreach (var id in removedContentTypes) + { + foreach (IUmbracoIndex index in _examineManager.Indexes.OfType()) { - foreach (var index in _examineManager.Indexes.OfType()) + var page = 0; + var total = long.MaxValue; + while (page * pageSize < total) { - var page = 0; - var total = long.MaxValue; - while (page * pageSize < total) + //paging with examine, see https://shazwazza.com/post/paging-with-examine/ + ISearchResults? results = index.Searcher + .CreateQuery() + .Field("nodeType", id.ToInvariantString()) + .Execute(QueryOptions.SkipTake(page * pageSize, pageSize)); + total = results.TotalItemCount; + + foreach (ISearchResult item in results) { - //paging with examine, see https://shazwazza.com/post/paging-with-examine/ - var results = index.Searcher - .CreateQuery() - .Field("nodeType", id.ToInvariantString()) - .Execute(QueryOptions.SkipTake(page * pageSize, pageSize)); - total = results.TotalItemCount; - - foreach (ISearchResult item in results) + if (int.TryParse(item.Id, NumberStyles.Integer, CultureInfo.InvariantCulture, + out var contentId)) { - if (int.TryParse(item.Id, NumberStyles.Integer, CultureInfo.InvariantCulture, out int contentId)) - { - DeleteIndexForEntity(contentId, false); - } + DeleteIndexForEntity(contentId, false); } - - page++; } + + page++; } } } + } - #region Deferred Actions - private class DeferedActions + /// + /// Used to lazily check if Examine Index handling is enabled + /// + /// + private bool IsEnabled() + { + //let's deal with shutting down Examine with MainDom + var examineShutdownRegistered = _mainDom.Register(release: () => { - private readonly List _actions = new List(); - - public static DeferedActions? Get(ICoreScopeProvider scopeProvider) + using (_profilingLogger.TraceDuration("Examine shutting down")) { - IScopeContext? scopeContext = scopeProvider.Context; + _examineManager.Dispose(); + } + }); + + if (!examineShutdownRegistered) + { + _logger.LogInformation( + "Examine shutdown not registered, this AppDomain is not the MainDom, Examine will be disabled"); + + //if we could not register the shutdown examine ourselves, it means we are not maindom! in this case all of examine should be disabled! + Suspendable.ExamineEvents.SuspendIndexers(_logger); + return false; //exit, do not continue + } + + _logger.LogDebug("Examine shutdown registered with MainDom"); + + var registeredIndexers = + _examineManager.Indexes.OfType().Count(x => x.EnableDefaultEventHandler); + + _logger.LogInformation("Adding examine event handlers for {RegisteredIndexers} index providers.", + registeredIndexers); + + // don't bind event handlers if we're not suppose to listen + if (registeredIndexers == 0) + { + return false; + } + + return true; + } - return scopeContext?.Enlist("examineEvents", - () => new DeferedActions(), // creator - (completed, actions) => // action + #region Deferred Actions + + private class DeferedActions + { + private readonly List _actions = new(); + + public static DeferedActions? Get(ICoreScopeProvider scopeProvider) + { + IScopeContext? scopeContext = scopeProvider.Context; + + return scopeContext?.Enlist("examineEvents", + () => new DeferedActions(), // creator + (completed, actions) => // action + { + if (completed) { - if (completed) - { - actions?.Execute(); - } - }, EnlistPriority); - } + actions?.Execute(); + } + }, EnlistPriority); + } - public void Add(DeferedAction action) => _actions.Add(action); + public void Add(DeferedAction action) => _actions.Add(action); - private void Execute() + private void Execute() + { + foreach (DeferedAction action in _actions) { - foreach (DeferedAction action in _actions) - { - action.Execute(); - } + action.Execute(); } } + } - /// - /// An action that will execute at the end of the Scope being completed - /// - private abstract class DeferedAction + /// + /// An action that will execute at the end of the Scope being completed + /// + private abstract class DeferedAction + { + public virtual void Execute() { - public virtual void Execute() - { } } + } - /// - /// Re-indexes an item on a background thread - /// - private class DeferedReIndexForContent : DeferedAction + /// + /// Re-indexes an item on a background thread + /// + private class DeferedReIndexForContent : DeferedAction + { + private readonly IBackgroundTaskQueue _backgroundTaskQueue; + private readonly IContent _content; + private readonly ExamineUmbracoIndexingHandler _examineUmbracoIndexingHandler; + private readonly bool _isPublished; + + public DeferedReIndexForContent(IBackgroundTaskQueue backgroundTaskQueue, + ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IContent content, bool isPublished) { - private readonly IBackgroundTaskQueue _backgroundTaskQueue; - private readonly ExamineUmbracoIndexingHandler _examineUmbracoIndexingHandler; - private readonly IContent _content; - private readonly bool _isPublished; + _backgroundTaskQueue = backgroundTaskQueue; + _examineUmbracoIndexingHandler = examineUmbracoIndexingHandler; + _content = content; + _isPublished = isPublished; + } - public DeferedReIndexForContent(IBackgroundTaskQueue backgroundTaskQueue, ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IContent content, bool isPublished) - { - _backgroundTaskQueue = backgroundTaskQueue; - _examineUmbracoIndexingHandler = examineUmbracoIndexingHandler; - _content = content; - _isPublished = isPublished; - } + public override void Execute() => + Execute(_backgroundTaskQueue, _examineUmbracoIndexingHandler, _content, _isPublished); - public override void Execute() => Execute(_backgroundTaskQueue, _examineUmbracoIndexingHandler, _content, _isPublished); + public static void Execute(IBackgroundTaskQueue backgroundTaskQueue, + ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IContent content, bool isPublished) + => backgroundTaskQueue.QueueBackgroundWorkItem(cancellationToken => + { + using ICoreScope scope = + examineUmbracoIndexingHandler._scopeProvider.CreateCoreScope(autoComplete: true); - public static void Execute(IBackgroundTaskQueue backgroundTaskQueue, ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IContent content, bool isPublished) - => backgroundTaskQueue.QueueBackgroundWorkItem(cancellationToken => + // for content we have a different builder for published vs unpublished + // we don't want to build more value sets than is needed so we'll lazily build 2 one for published one for non-published + var builders = new Dictionary>> { - using ICoreScope scope = examineUmbracoIndexingHandler._scopeProvider.CreateCoreScope(autoComplete: true); - - // for content we have a different builder for published vs unpublished - // we don't want to build more value sets than is needed so we'll lazily build 2 one for published one for non-published - var builders = new Dictionary>> - { - [true] = new Lazy>(() => examineUmbracoIndexingHandler._publishedContentValueSetBuilder.GetValueSets(content).ToList()), - [false] = new Lazy>(() => examineUmbracoIndexingHandler._contentValueSetBuilder.GetValueSets(content).ToList()) - }; - - // This is only for content - so only index items for IUmbracoContentIndex (to exlude members) - foreach (IUmbracoIndex index in examineUmbracoIndexingHandler._examineManager.Indexes.OfType() - //filter the indexers - .Where(x => isPublished || !x.PublishedValuesOnly) - .Where(x => x.EnableDefaultEventHandler)) + [true] = + new(() => examineUmbracoIndexingHandler._publishedContentValueSetBuilder + .GetValueSets(content).ToList()), + [false] = new(() => + examineUmbracoIndexingHandler._contentValueSetBuilder.GetValueSets(content).ToList()) + }; + + // This is only for content - so only index items for IUmbracoContentIndex (to exlude members) + foreach (IUmbracoIndex index in examineUmbracoIndexingHandler._examineManager.Indexes + .OfType() + //filter the indexers + .Where(x => isPublished || !x.PublishedValuesOnly) + .Where(x => x.EnableDefaultEventHandler)) + { + if (cancellationToken.IsCancellationRequested) { - if (cancellationToken.IsCancellationRequested) - { - return Task.CompletedTask; - } - - List valueSet = builders[index.PublishedValuesOnly].Value; - index.IndexItems(valueSet); + return Task.CompletedTask; } - return Task.CompletedTask; - }); - } + List valueSet = builders[index.PublishedValuesOnly].Value; + index.IndexItems(valueSet); + } + + return Task.CompletedTask; + }); + } + + /// + /// Re-indexes an item on a background thread + /// + private class DeferedReIndexForMedia : DeferedAction + { + private readonly IBackgroundTaskQueue _backgroundTaskQueue; + private readonly ExamineUmbracoIndexingHandler _examineUmbracoIndexingHandler; + private readonly bool _isPublished; + private readonly IMedia _media; - /// - /// Re-indexes an item on a background thread - /// - private class DeferedReIndexForMedia : DeferedAction + public DeferedReIndexForMedia(IBackgroundTaskQueue backgroundTaskQueue, + ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IMedia media, bool isPublished) { - private readonly IBackgroundTaskQueue _backgroundTaskQueue; - private readonly ExamineUmbracoIndexingHandler _examineUmbracoIndexingHandler; - private readonly IMedia _media; - private readonly bool _isPublished; + _backgroundTaskQueue = backgroundTaskQueue; + _examineUmbracoIndexingHandler = examineUmbracoIndexingHandler; + _media = media; + _isPublished = isPublished; + } + + public override void Execute() => + Execute(_backgroundTaskQueue, _examineUmbracoIndexingHandler, _media, _isPublished); - public DeferedReIndexForMedia(IBackgroundTaskQueue backgroundTaskQueue, ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IMedia media, bool isPublished) + public static void Execute(IBackgroundTaskQueue backgroundTaskQueue, + ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IMedia media, bool isPublished) => + // perform the ValueSet lookup on a background thread + backgroundTaskQueue.QueueBackgroundWorkItem(cancellationToken => { - _backgroundTaskQueue = backgroundTaskQueue; - _examineUmbracoIndexingHandler = examineUmbracoIndexingHandler; - _media = media; - _isPublished = isPublished; - } + using ICoreScope scope = + examineUmbracoIndexingHandler._scopeProvider.CreateCoreScope(autoComplete: true); - public override void Execute() => Execute(_backgroundTaskQueue, _examineUmbracoIndexingHandler, _media, _isPublished); + var valueSet = examineUmbracoIndexingHandler._mediaValueSetBuilder.GetValueSets(media).ToList(); - public static void Execute(IBackgroundTaskQueue backgroundTaskQueue, ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IMedia media, bool isPublished) => - // perform the ValueSet lookup on a background thread - backgroundTaskQueue.QueueBackgroundWorkItem(cancellationToken => + // This is only for content - so only index items for IUmbracoContentIndex (to exlude members) + foreach (IUmbracoIndex index in examineUmbracoIndexingHandler._examineManager.Indexes + .OfType() + //filter the indexers + .Where(x => isPublished || !x.PublishedValuesOnly) + .Where(x => x.EnableDefaultEventHandler)) { - using ICoreScope scope = examineUmbracoIndexingHandler._scopeProvider.CreateCoreScope(autoComplete: true); + index.IndexItems(valueSet); + } - var valueSet = examineUmbracoIndexingHandler._mediaValueSetBuilder.GetValueSets(media).ToList(); + return Task.CompletedTask; + }); + } - // This is only for content - so only index items for IUmbracoContentIndex (to exlude members) - foreach (IUmbracoIndex index in examineUmbracoIndexingHandler._examineManager.Indexes.OfType() - //filter the indexers - .Where(x => isPublished || !x.PublishedValuesOnly) - .Where(x => x.EnableDefaultEventHandler)) - { - index.IndexItems(valueSet); - } + /// + /// Re-indexes an item on a background thread + /// + private class DeferedReIndexForMember : DeferedAction + { + private readonly IBackgroundTaskQueue _backgroundTaskQueue; + private readonly ExamineUmbracoIndexingHandler _examineUmbracoIndexingHandler; + private readonly IMember _member; - return Task.CompletedTask; - }); + public DeferedReIndexForMember(IBackgroundTaskQueue backgroundTaskQueue, + ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IMember member) + { + _examineUmbracoIndexingHandler = examineUmbracoIndexingHandler; + _member = member; + _backgroundTaskQueue = backgroundTaskQueue; } - /// - /// Re-indexes an item on a background thread - /// - private class DeferedReIndexForMember : DeferedAction - { - private readonly ExamineUmbracoIndexingHandler _examineUmbracoIndexingHandler; - private readonly IMember _member; - private readonly IBackgroundTaskQueue _backgroundTaskQueue; + public override void Execute() => Execute(_backgroundTaskQueue, _examineUmbracoIndexingHandler, _member); - public DeferedReIndexForMember(IBackgroundTaskQueue backgroundTaskQueue, ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IMember member) + public static void Execute(IBackgroundTaskQueue backgroundTaskQueue, + ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IMember member) => + // perform the ValueSet lookup on a background thread + backgroundTaskQueue.QueueBackgroundWorkItem(cancellationToken => { - _examineUmbracoIndexingHandler = examineUmbracoIndexingHandler; - _member = member; - _backgroundTaskQueue = backgroundTaskQueue; - } + using ICoreScope scope = + examineUmbracoIndexingHandler._scopeProvider.CreateCoreScope(autoComplete: true); - public override void Execute() => Execute(_backgroundTaskQueue, _examineUmbracoIndexingHandler, _member); + var valueSet = examineUmbracoIndexingHandler._memberValueSetBuilder.GetValueSets(member).ToList(); - public static void Execute(IBackgroundTaskQueue backgroundTaskQueue, ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IMember member) => - // perform the ValueSet lookup on a background thread - backgroundTaskQueue.QueueBackgroundWorkItem(cancellationToken => + // only process for IUmbracoMemberIndex (not content indexes) + foreach (IUmbracoIndex index in examineUmbracoIndexingHandler._examineManager.Indexes + .OfType() + //filter the indexers + .Where(x => x.EnableDefaultEventHandler)) { - using ICoreScope scope = examineUmbracoIndexingHandler._scopeProvider.CreateCoreScope(autoComplete: true); + index.IndexItems(valueSet); + } - var valueSet = examineUmbracoIndexingHandler._memberValueSetBuilder.GetValueSets(member).ToList(); + return Task.CompletedTask; + }); + } - // only process for IUmbracoMemberIndex (not content indexes) - foreach (IUmbracoIndex index in examineUmbracoIndexingHandler._examineManager.Indexes.OfType() - //filter the indexers - .Where(x => x.EnableDefaultEventHandler)) - { - index.IndexItems(valueSet); - } + private class DeferedDeleteIndex : DeferedAction + { + private readonly ExamineUmbracoIndexingHandler _examineUmbracoIndexingHandler; + private readonly int _id; + private readonly IReadOnlyCollection? _ids; + private readonly bool _keepIfUnpublished; - return Task.CompletedTask; - }); + public DeferedDeleteIndex(ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, int id, + bool keepIfUnpublished) + { + _examineUmbracoIndexingHandler = examineUmbracoIndexingHandler; + _id = id; + _keepIfUnpublished = keepIfUnpublished; } - private class DeferedDeleteIndex : DeferedAction + public DeferedDeleteIndex(ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, + IReadOnlyCollection ids, bool keepIfUnpublished) { - private readonly ExamineUmbracoIndexingHandler _examineUmbracoIndexingHandler; - private readonly int _id; - private readonly IReadOnlyCollection? _ids; - private readonly bool _keepIfUnpublished; - - public DeferedDeleteIndex(ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, int id, bool keepIfUnpublished) - { - _examineUmbracoIndexingHandler = examineUmbracoIndexingHandler; - _id = id; - _keepIfUnpublished = keepIfUnpublished; - } + _examineUmbracoIndexingHandler = examineUmbracoIndexingHandler; + _ids = ids; + _keepIfUnpublished = keepIfUnpublished; + } - public DeferedDeleteIndex(ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IReadOnlyCollection ids, bool keepIfUnpublished) + public override void Execute() + { + if (_ids is null) { - _examineUmbracoIndexingHandler = examineUmbracoIndexingHandler; - _ids = ids; - _keepIfUnpublished = keepIfUnpublished; + Execute(_examineUmbracoIndexingHandler, _id, _keepIfUnpublished); } - - public override void Execute() + else { - if (_ids is null) - { - Execute(_examineUmbracoIndexingHandler, _id, _keepIfUnpublished); - } - else - { - Execute(_examineUmbracoIndexingHandler, _ids, _keepIfUnpublished); - } + Execute(_examineUmbracoIndexingHandler, _ids, _keepIfUnpublished); } + } - public static void Execute(ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, int id, bool keepIfUnpublished) + public static void Execute(ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, int id, + bool keepIfUnpublished) + { + foreach (IUmbracoIndex index in examineUmbracoIndexingHandler._examineManager.Indexes + .OfType() + .Where(x => x.PublishedValuesOnly || !keepIfUnpublished) + .Where(x => x.EnableDefaultEventHandler)) { - foreach (var index in examineUmbracoIndexingHandler._examineManager.Indexes.OfType() - .Where(x => x.PublishedValuesOnly || !keepIfUnpublished) - .Where(x => x.EnableDefaultEventHandler)) - { - index.DeleteFromIndex(id.ToString(CultureInfo.InvariantCulture)); - } + index.DeleteFromIndex(id.ToString(CultureInfo.InvariantCulture)); } + } - public static void Execute(ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IReadOnlyCollection ids, bool keepIfUnpublished) + public static void Execute(ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, + IReadOnlyCollection ids, bool keepIfUnpublished) + { + foreach (IUmbracoIndex index in examineUmbracoIndexingHandler._examineManager.Indexes + .OfType() + .Where(x => x.PublishedValuesOnly || !keepIfUnpublished) + .Where(x => x.EnableDefaultEventHandler)) { - foreach (var index in examineUmbracoIndexingHandler._examineManager.Indexes.OfType() - .Where(x => x.PublishedValuesOnly || !keepIfUnpublished) - .Where(x => x.EnableDefaultEventHandler)) - { - index.DeleteFromIndex(ids.Select(x => x.ToString(CultureInfo.InvariantCulture))); - } + index.DeleteFromIndex(ids.Select(x => x.ToString(CultureInfo.InvariantCulture))); } } - #endregion } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Examine/GenericIndexDiagnostics.cs b/src/Umbraco.Infrastructure/Examine/GenericIndexDiagnostics.cs index c2bf6b002dd7..2d7ecbef0c2e 100644 --- a/src/Umbraco.Infrastructure/Examine/GenericIndexDiagnostics.cs +++ b/src/Umbraco.Infrastructure/Examine/GenericIndexDiagnostics.cs @@ -1,70 +1,69 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using System.Reflection; using Examine; using Examine.Search; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Composing; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Examine -{ +namespace Umbraco.Cms.Infrastructure.Examine; - /// - /// Used to return diagnostic data for any index - /// - public class GenericIndexDiagnostics : IIndexDiagnostics - { - private readonly IIndex _index; - private static readonly string[] s_ignoreProperties = { "Description" }; +/// +/// Used to return diagnostic data for any index +/// +public class GenericIndexDiagnostics : IIndexDiagnostics +{ + private static readonly string[] s_ignoreProperties = {"Description"}; - private readonly ISet _idOnlyFieldSet = new HashSet { "id" }; - public GenericIndexDiagnostics(IIndex index) => _index = index; + private readonly ISet _idOnlyFieldSet = new HashSet {"id"}; + private readonly IIndex _index; + public GenericIndexDiagnostics(IIndex index) => _index = index; - public int DocumentCount => -1; //unknown + public int DocumentCount => -1; //unknown - public int FieldCount => -1; //unknown + public int FieldCount => -1; //unknown - public Attempt IsHealthy() + public Attempt IsHealthy() + { + if (!_index.IndexExists()) { - if (!_index.IndexExists()) - return Attempt.Fail("Does not exist"); + return Attempt.Fail("Does not exist"); + } - try - { - var result = _index.Searcher.CreateQuery().ManagedQuery("test").SelectFields(_idOnlyFieldSet).Execute(new QueryOptions(0, 1)); - return Attempt.Succeed(); //if we can search we'll assume it's healthy - } - catch (Exception e) - { - return Attempt.Fail($"Error: {e.Message}"); - } + try + { + ISearchResults? result = _index.Searcher.CreateQuery().ManagedQuery("test").SelectFields(_idOnlyFieldSet) + .Execute(new QueryOptions(0, 1)); + return Attempt.Succeed(); //if we can search we'll assume it's healthy + } + catch (Exception e) + { + return Attempt.Fail($"Error: {e.Message}"); } + } - public long GetDocumentCount() => -1L; + public long GetDocumentCount() => -1L; - public IEnumerable GetFieldNames() => Enumerable.Empty(); + public IEnumerable GetFieldNames() => Enumerable.Empty(); - public IReadOnlyDictionary Metadata + public IReadOnlyDictionary Metadata + { + get { - get - { - var result = new Dictionary(); - - var props = TypeHelper.CachedDiscoverableProperties(_index.GetType(), mustWrite: false) - .Where(x => s_ignoreProperties.InvariantContains(x.Name) == false) - .OrderBy(x => x.Name); + var result = new Dictionary(); - foreach (var p in props) - { - var val = p.GetValue(_index, null) ?? string.Empty; + IOrderedEnumerable props = TypeHelper + .CachedDiscoverableProperties(_index.GetType(), mustWrite: false) + .Where(x => s_ignoreProperties.InvariantContains(x.Name) == false) + .OrderBy(x => x.Name); - result.Add(p.Name, val); - } + foreach (PropertyInfo p in props) + { + var val = p.GetValue(_index, null) ?? string.Empty; - return result; + result.Add(p.Name, val); } + + return result; } } } diff --git a/src/Umbraco.Infrastructure/Examine/IBackOfficeExamineSearcher.cs b/src/Umbraco.Infrastructure/Examine/IBackOfficeExamineSearcher.cs index dc01d7bc9493..6960d9c4aa56 100644 --- a/src/Umbraco.Infrastructure/Examine/IBackOfficeExamineSearcher.cs +++ b/src/Umbraco.Infrastructure/Examine/IBackOfficeExamineSearcher.cs @@ -1,17 +1,15 @@ -using System.Collections.Generic; -using Examine; +using Examine; using Umbraco.Cms.Core.Models.ContentEditing; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Used to search the back office for Examine indexed entities (Documents, Media and Members) +/// +public interface IBackOfficeExamineSearcher { - /// - /// Used to search the back office for Examine indexed entities (Documents, Media and Members) - /// - public interface IBackOfficeExamineSearcher - { - IEnumerable Search(string query, - UmbracoEntityTypes entityType, - int pageSize, - long pageIndex, out long totalFound, string? searchFrom = null, bool ignoreUserStartNodes = false); - } + IEnumerable Search(string query, + UmbracoEntityTypes entityType, + int pageSize, + long pageIndex, out long totalFound, string? searchFrom = null, bool ignoreUserStartNodes = false); } diff --git a/src/Umbraco.Infrastructure/Examine/IContentValueSetBuilder.cs b/src/Umbraco.Infrastructure/Examine/IContentValueSetBuilder.cs index af6b613e242f..f0c32478b11b 100644 --- a/src/Umbraco.Infrastructure/Examine/IContentValueSetBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/IContentValueSetBuilder.cs @@ -1,12 +1,11 @@ using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// +/// Marker interface for a builder for supporting unpublished content +/// +public interface IContentValueSetBuilder : IValueSetBuilder { - /// - /// - /// Marker interface for a builder for supporting unpublished content - /// - public interface IContentValueSetBuilder : IValueSetBuilder - { - } } diff --git a/src/Umbraco.Infrastructure/Examine/IContentValueSetValidator.cs b/src/Umbraco.Infrastructure/Examine/IContentValueSetValidator.cs index e76153f25e64..cfe815c3a1be 100644 --- a/src/Umbraco.Infrastructure/Examine/IContentValueSetValidator.cs +++ b/src/Umbraco.Infrastructure/Examine/IContentValueSetValidator.cs @@ -1,31 +1,30 @@ using Examine; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// An extended for content indexes +/// +public interface IContentValueSetValidator : IValueSetValidator { /// - /// An extended for content indexes + /// When set to true the index will only retain published values /// - public interface IContentValueSetValidator : IValueSetValidator - { - /// - /// When set to true the index will only retain published values - /// - /// - /// Any non-published values will not be put or kept in the index: - /// * Deleted, Trashed, non-published Content items - /// * non-published Variants - /// - bool PublishedValuesOnly { get; } + /// + /// Any non-published values will not be put or kept in the index: + /// * Deleted, Trashed, non-published Content items + /// * non-published Variants + /// + bool PublishedValuesOnly { get; } - /// - /// If true, protected content will be indexed otherwise it will not be put or kept in the index - /// - bool SupportProtectedContent { get; } + /// + /// If true, protected content will be indexed otherwise it will not be put or kept in the index + /// + bool SupportProtectedContent { get; } - int? ParentId { get; } + int? ParentId { get; } - bool ValidatePath(string path, string category); - bool ValidateRecycleBin(string path, string category); - bool ValidateProtectedContent(string path, string category); - } + bool ValidatePath(string path, string category); + bool ValidateRecycleBin(string path, string category); + bool ValidateProtectedContent(string path, string category); } diff --git a/src/Umbraco.Infrastructure/Examine/IIndexDiagnostics.cs b/src/Umbraco.Infrastructure/Examine/IIndexDiagnostics.cs index dd9ee632399c..5db9847c1e03 100644 --- a/src/Umbraco.Infrastructure/Examine/IIndexDiagnostics.cs +++ b/src/Umbraco.Infrastructure/Examine/IIndexDiagnostics.cs @@ -1,30 +1,26 @@ -using System.Collections.Generic; -using System.Threading.Tasks; using Examine; using Umbraco.Cms.Core; -namespace Umbraco.Cms.Infrastructure.Examine -{ +namespace Umbraco.Cms.Infrastructure.Examine; +/// +/// Exposes diagnostic information about an index +/// +public interface IIndexDiagnostics : IIndexStats +{ /// - /// Exposes diagnostic information about an index + /// A key/value collection of diagnostic properties for the index /// - public interface IIndexDiagnostics : IIndexStats - { - /// - /// If the index can be open/read - /// - /// - /// A successful attempt if it is healthy, else a failed attempt with a message if unhealthy - /// - Attempt IsHealthy(); + /// + /// Used to display in the UI + /// + IReadOnlyDictionary Metadata { get; } - /// - /// A key/value collection of diagnostic properties for the index - /// - /// - /// Used to display in the UI - /// - IReadOnlyDictionary Metadata { get; } - } + /// + /// If the index can be open/read + /// + /// + /// A successful attempt if it is healthy, else a failed attempt with a message if unhealthy + /// + Attempt IsHealthy(); } diff --git a/src/Umbraco.Infrastructure/Examine/IIndexDiagnosticsFactory.cs b/src/Umbraco.Infrastructure/Examine/IIndexDiagnosticsFactory.cs index b39ef5c3a854..0e5d9bad221e 100644 --- a/src/Umbraco.Infrastructure/Examine/IIndexDiagnosticsFactory.cs +++ b/src/Umbraco.Infrastructure/Examine/IIndexDiagnosticsFactory.cs @@ -1,13 +1,11 @@ using Examine; -namespace Umbraco.Cms.Infrastructure.Examine -{ +namespace Umbraco.Cms.Infrastructure.Examine; - /// - /// Creates for an index if it doesn't implement - /// - public interface IIndexDiagnosticsFactory - { - IIndexDiagnostics Create(IIndex index); - } +/// +/// Creates for an index if it doesn't implement +/// +public interface IIndexDiagnosticsFactory +{ + IIndexDiagnostics Create(IIndex index); } diff --git a/src/Umbraco.Infrastructure/Examine/IIndexPopulator.cs b/src/Umbraco.Infrastructure/Examine/IIndexPopulator.cs index 2089bd923a59..df622e16249b 100644 --- a/src/Umbraco.Infrastructure/Examine/IIndexPopulator.cs +++ b/src/Umbraco.Infrastructure/Examine/IIndexPopulator.cs @@ -1,20 +1,19 @@ using Examine; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +public interface IIndexPopulator { - public interface IIndexPopulator - { - /// - /// If this index is registered with this populator - /// - /// - /// - bool IsRegistered(IIndex index); + /// + /// If this index is registered with this populator + /// + /// + /// + bool IsRegistered(IIndex index); - /// - /// Populate indexers - /// - /// - void Populate(params IIndex[] indexes); - } + /// + /// Populate indexers + /// + /// + void Populate(params IIndex[] indexes); } diff --git a/src/Umbraco.Infrastructure/Examine/IIndexRebuilder.cs b/src/Umbraco.Infrastructure/Examine/IIndexRebuilder.cs index 127a20d6859b..a8506c363b19 100644 --- a/src/Umbraco.Infrastructure/Examine/IIndexRebuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/IIndexRebuilder.cs @@ -1,14 +1,8 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Examine; +namespace Umbraco.Cms.Infrastructure.Examine; -namespace Umbraco.Cms.Infrastructure.Examine +public interface IIndexRebuilder { - public interface IIndexRebuilder - { - bool CanRebuild(string indexName); - void RebuildIndex(string indexName, TimeSpan? delay = null, bool useBackgroundThread = true); - void RebuildIndexes(bool onlyEmptyIndexes, TimeSpan? delay = null, bool useBackgroundThread = true); - } + bool CanRebuild(string indexName); + void RebuildIndex(string indexName, TimeSpan? delay = null, bool useBackgroundThread = true); + void RebuildIndexes(bool onlyEmptyIndexes, TimeSpan? delay = null, bool useBackgroundThread = true); } diff --git a/src/Umbraco.Infrastructure/Examine/IPublishedContentValueSetBuilder.cs b/src/Umbraco.Infrastructure/Examine/IPublishedContentValueSetBuilder.cs index 8c5348ed46cb..21d0e09968e0 100644 --- a/src/Umbraco.Infrastructure/Examine/IPublishedContentValueSetBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/IPublishedContentValueSetBuilder.cs @@ -1,12 +1,11 @@ using Examine; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Marker interface for a builder for only published content +/// +public interface IPublishedContentValueSetBuilder : IValueSetBuilder { - /// - /// Marker interface for a builder for only published content - /// - public interface IPublishedContentValueSetBuilder : IValueSetBuilder - { - } } diff --git a/src/Umbraco.Infrastructure/Examine/IUmbracoContentIndex.cs b/src/Umbraco.Infrastructure/Examine/IUmbracoContentIndex.cs index 0735cc255d50..a47c328fc054 100644 --- a/src/Umbraco.Infrastructure/Examine/IUmbracoContentIndex.cs +++ b/src/Umbraco.Infrastructure/Examine/IUmbracoContentIndex.cs @@ -1,11 +1,8 @@ -using Examine; +namespace Umbraco.Cms.Infrastructure.Examine; -namespace Umbraco.Cms.Infrastructure.Examine +/// +/// Marker interface for indexes of Umbraco content +/// +public interface IUmbracoContentIndex : IUmbracoIndex { - /// - /// Marker interface for indexes of Umbraco content - /// - public interface IUmbracoContentIndex : IUmbracoIndex - { - } } diff --git a/src/Umbraco.Infrastructure/Examine/IUmbracoIndex.cs b/src/Umbraco.Infrastructure/Examine/IUmbracoIndex.cs index f2221e5c91e9..a94201894aa7 100644 --- a/src/Umbraco.Infrastructure/Examine/IUmbracoIndex.cs +++ b/src/Umbraco.Infrastructure/Examine/IUmbracoIndex.cs @@ -1,26 +1,24 @@ -using System.Collections.Generic; using Examine; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// A Marker interface for defining an Umbraco indexer +/// +public interface IUmbracoIndex : IIndex, IIndexStats { /// - /// A Marker interface for defining an Umbraco indexer + /// When set to true Umbraco will keep the index in sync with Umbraco data automatically /// - public interface IUmbracoIndex : IIndex, IIndexStats - { - /// - /// When set to true Umbraco will keep the index in sync with Umbraco data automatically - /// - bool EnableDefaultEventHandler { get; } + bool EnableDefaultEventHandler { get; } - /// - /// When set to true the index will only retain published values - /// - /// - /// Any non-published values will not be put or kept in the index: - /// * Deleted, Trashed, non-published Content items - /// * non-published Variants - /// - bool PublishedValuesOnly { get; } - } + /// + /// When set to true the index will only retain published values + /// + /// + /// Any non-published values will not be put or kept in the index: + /// * Deleted, Trashed, non-published Content items + /// * non-published Variants + /// + bool PublishedValuesOnly { get; } } diff --git a/src/Umbraco.Infrastructure/Examine/IUmbracoIndexConfig.cs b/src/Umbraco.Infrastructure/Examine/IUmbracoIndexConfig.cs index 83a3730b976e..04802170fc6b 100644 --- a/src/Umbraco.Infrastructure/Examine/IUmbracoIndexConfig.cs +++ b/src/Umbraco.Infrastructure/Examine/IUmbracoIndexConfig.cs @@ -1,12 +1,10 @@ using Examine; -namespace Umbraco.Cms.Infrastructure.Examine -{ - public interface IUmbracoIndexConfig - { - IContentValueSetValidator GetContentValueSetValidator(); - IContentValueSetValidator GetPublishedContentValueSetValidator(); - IValueSetValidator GetMemberValueSetValidator(); +namespace Umbraco.Cms.Infrastructure.Examine; - } +public interface IUmbracoIndexConfig +{ + IContentValueSetValidator GetContentValueSetValidator(); + IContentValueSetValidator GetPublishedContentValueSetValidator(); + IValueSetValidator GetMemberValueSetValidator(); } diff --git a/src/Umbraco.Infrastructure/Examine/IUmbracoMemberIndex.cs b/src/Umbraco.Infrastructure/Examine/IUmbracoMemberIndex.cs index 7dc07688a9e7..e914c90d3742 100644 --- a/src/Umbraco.Infrastructure/Examine/IUmbracoMemberIndex.cs +++ b/src/Umbraco.Infrastructure/Examine/IUmbracoMemberIndex.cs @@ -1,9 +1,5 @@ -using Examine; +namespace Umbraco.Cms.Infrastructure.Examine; -namespace Umbraco.Cms.Infrastructure.Examine +public interface IUmbracoMemberIndex : IUmbracoIndex { - public interface IUmbracoMemberIndex : IUmbracoIndex - { - - } } diff --git a/src/Umbraco.Infrastructure/Examine/IUmbracoTreeSearcherFields.cs b/src/Umbraco.Infrastructure/Examine/IUmbracoTreeSearcherFields.cs index fe135a82b78d..da4c13b02b72 100644 --- a/src/Umbraco.Infrastructure/Examine/IUmbracoTreeSearcherFields.cs +++ b/src/Umbraco.Infrastructure/Examine/IUmbracoTreeSearcherFields.cs @@ -1,35 +1,32 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Infrastructure.Examine; -namespace Umbraco.Cms.Infrastructure.Examine +/// +/// Used to propagate hardcoded internal Field lists +/// +public interface IUmbracoTreeSearcherFields { /// - /// Used to propagate hardcoded internal Field lists + /// The default index fields that are searched on in the back office search for umbraco content entities. /// - public interface IUmbracoTreeSearcherFields - { - /// - /// The default index fields that are searched on in the back office search for umbraco content entities. - /// - IEnumerable GetBackOfficeFields(); + IEnumerable GetBackOfficeFields(); - /// - /// The additional index fields that are searched on in the back office for member entities. - /// - IEnumerable GetBackOfficeMembersFields(); + /// + /// The additional index fields that are searched on in the back office for member entities. + /// + IEnumerable GetBackOfficeMembersFields(); - /// - /// The additional index fields that are searched on in the back office for media entities. - /// - IEnumerable GetBackOfficeMediaFields(); + /// + /// The additional index fields that are searched on in the back office for media entities. + /// + IEnumerable GetBackOfficeMediaFields(); - /// - /// The additional index fields that are searched on in the back office for document entities. - /// - IEnumerable GetBackOfficeDocumentFields(); + /// + /// The additional index fields that are searched on in the back office for document entities. + /// + IEnumerable GetBackOfficeDocumentFields(); - ISet GetBackOfficeFieldsToLoad(); - ISet GetBackOfficeMembersFieldsToLoad(); - ISet GetBackOfficeDocumentFieldsToLoad(); - ISet GetBackOfficeMediaFieldsToLoad(); - } + ISet GetBackOfficeFieldsToLoad(); + ISet GetBackOfficeMembersFieldsToLoad(); + ISet GetBackOfficeDocumentFieldsToLoad(); + ISet GetBackOfficeMediaFieldsToLoad(); } diff --git a/src/Umbraco.Infrastructure/Examine/IValueSetBuilder.cs b/src/Umbraco.Infrastructure/Examine/IValueSetBuilder.cs index 0e1d05440d81..0612194f9e0f 100644 --- a/src/Umbraco.Infrastructure/Examine/IValueSetBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/IValueSetBuilder.cs @@ -1,20 +1,17 @@ -using System.Collections.Generic; -using Examine; +using Examine; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Creates a collection of to be indexed based on a collection of +/// +/// +public interface IValueSetBuilder { /// - /// Creates a collection of to be indexed based on a collection of + /// Creates a collection of to be indexed based on a collection of /// - /// - public interface IValueSetBuilder - { - /// - /// Creates a collection of to be indexed based on a collection of - /// - /// - /// - IEnumerable GetValueSets(params T[] content); - } - + /// + /// + IEnumerable GetValueSets(params T[] content); } diff --git a/src/Umbraco.Infrastructure/Examine/IndexDiagnosticsFactory.cs b/src/Umbraco.Infrastructure/Examine/IndexDiagnosticsFactory.cs index a60a373e65e1..acaf42b4b025 100644 --- a/src/Umbraco.Infrastructure/Examine/IndexDiagnosticsFactory.cs +++ b/src/Umbraco.Infrastructure/Examine/IndexDiagnosticsFactory.cs @@ -1,20 +1,20 @@ using Examine; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Default implementation of which returns +/// for indexes that don't have an implementation +/// +public class IndexDiagnosticsFactory : IIndexDiagnosticsFactory { - /// - /// Default implementation of which returns for indexes that don't have an implementation - /// - public class IndexDiagnosticsFactory : IIndexDiagnosticsFactory + public virtual IIndexDiagnostics Create(IIndex index) { - public virtual IIndexDiagnostics Create(IIndex index) + if (index is not IIndexDiagnostics indexDiag) { - if (index is not IIndexDiagnostics indexDiag) - { - indexDiag = new GenericIndexDiagnostics(index); - } - - return indexDiag; + indexDiag = new GenericIndexDiagnostics(index); } + + return indexDiag; } } diff --git a/src/Umbraco.Infrastructure/Examine/IndexPopulator.cs b/src/Umbraco.Infrastructure/Examine/IndexPopulator.cs index d32470d875a6..4d331ccd7bc4 100644 --- a/src/Umbraco.Infrastructure/Examine/IndexPopulator.cs +++ b/src/Umbraco.Infrastructure/Examine/IndexPopulator.cs @@ -1,53 +1,45 @@ -using System.Collections.Generic; -using System.Linq; using Examine; using Umbraco.Cms.Core.Collections; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// An that is automatically associated to any index of type +/// +/// +public abstract class IndexPopulator : IndexPopulator where TIndex : IIndex { - /// - /// An that is automatically associated to any index of type - /// - /// - public abstract class IndexPopulator : IndexPopulator where TIndex : IIndex + public override bool IsRegistered(IIndex index) { - public override bool IsRegistered(IIndex index) + if (base.IsRegistered(index)) { - if (base.IsRegistered(index)) - return true; - - if (!(index is TIndex casted)) - return false; + return true; + } - return IsRegistered(casted); + if (!(index is TIndex casted)) + { + return false; } - public virtual bool IsRegistered(TIndex index) => true; + return IsRegistered(casted); } - public abstract class IndexPopulator : IIndexPopulator - { - private readonly ConcurrentHashSet _registeredIndexes = new ConcurrentHashSet(); + public virtual bool IsRegistered(TIndex index) => true; +} - public virtual bool IsRegistered(IIndex index) - { - return _registeredIndexes.Contains(index.Name); - } +public abstract class IndexPopulator : IIndexPopulator +{ + private readonly ConcurrentHashSet _registeredIndexes = new(); - /// - /// Registers an index for this populator - /// - /// - public void RegisterIndex(string indexName) - { - _registeredIndexes.Add(indexName); - } + public virtual bool IsRegistered(IIndex index) => _registeredIndexes.Contains(index.Name); - public void Populate(params IIndex[] indexes) - { - PopulateIndexes(indexes.Where(IsRegistered).ToList()); - } + public void Populate(params IIndex[] indexes) => PopulateIndexes(indexes.Where(IsRegistered).ToList()); - protected abstract void PopulateIndexes(IReadOnlyList indexes); - } + /// + /// Registers an index for this populator + /// + /// + public void RegisterIndex(string indexName) => _registeredIndexes.Add(indexName); + + protected abstract void PopulateIndexes(IReadOnlyList indexes); } diff --git a/src/Umbraco.Infrastructure/Examine/IndexTypes.cs b/src/Umbraco.Infrastructure/Examine/IndexTypes.cs index bb6edaa78b4a..98a5fec9acdc 100644 --- a/src/Umbraco.Infrastructure/Examine/IndexTypes.cs +++ b/src/Umbraco.Infrastructure/Examine/IndexTypes.cs @@ -1,33 +1,31 @@ -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// The index types stored in the Lucene Index +/// +public static class IndexTypes { /// - /// The index types stored in the Lucene Index + /// The content index type /// - public static class IndexTypes - { - - /// - /// The content index type - /// - /// - /// Is lower case because the Standard Analyzer requires lower case - /// - public const string Content = "content"; + /// + /// Is lower case because the Standard Analyzer requires lower case + /// + public const string Content = "content"; - /// - /// The media index type - /// - /// - /// Is lower case because the Standard Analyzer requires lower case - /// - public const string Media = "media"; + /// + /// The media index type + /// + /// + /// Is lower case because the Standard Analyzer requires lower case + /// + public const string Media = "media"; - /// - /// The member index type - /// - /// - /// Is lower case because the Standard Analyzer requires lower case - /// - public const string Member = "member"; - } + /// + /// The member index type + /// + /// + /// Is lower case because the Standard Analyzer requires lower case + /// + public const string Member = "member"; } diff --git a/src/Umbraco.Infrastructure/Examine/MediaIndexPopulator.cs b/src/Umbraco.Infrastructure/Examine/MediaIndexPopulator.cs index 9f6e33f8dd51..04cca52098a9 100644 --- a/src/Umbraco.Infrastructure/Examine/MediaIndexPopulator.cs +++ b/src/Umbraco.Infrastructure/Examine/MediaIndexPopulator.cs @@ -1,80 +1,81 @@ -using System.Collections.Generic; -using System.Linq; using Examine; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Performs the data lookups required to rebuild a media index +/// +public class MediaIndexPopulator : IndexPopulator { + private readonly ILogger _logger; + private readonly IMediaService _mediaService; + private readonly IValueSetBuilder _mediaValueSetBuilder; + private readonly int? _parentId; + /// - /// Performs the data lookups required to rebuild a media index + /// Default constructor to lookup all content data /// - public class MediaIndexPopulator : IndexPopulator + /// + /// + public MediaIndexPopulator(ILogger logger, IMediaService mediaService, + IValueSetBuilder mediaValueSetBuilder) + : this(logger, null, mediaService, mediaValueSetBuilder) { - private readonly ILogger _logger; - private readonly int? _parentId; - private readonly IMediaService _mediaService; - private readonly IValueSetBuilder _mediaValueSetBuilder; + } - /// - /// Default constructor to lookup all content data - /// - /// - /// - public MediaIndexPopulator(ILogger logger, IMediaService mediaService, IValueSetBuilder mediaValueSetBuilder) - : this(logger, null, mediaService, mediaValueSetBuilder) - { - } + /// + /// Optional constructor allowing specifying custom query parameters + /// + /// + /// + /// + public MediaIndexPopulator(ILogger logger, int? parentId, IMediaService mediaService, + IValueSetBuilder mediaValueSetBuilder) + { + _logger = logger; + _parentId = parentId; + _mediaService = mediaService; + _mediaValueSetBuilder = mediaValueSetBuilder; + } - /// - /// Optional constructor allowing specifying custom query parameters - /// - /// - /// - /// - public MediaIndexPopulator(ILogger logger, int? parentId, IMediaService mediaService, IValueSetBuilder mediaValueSetBuilder) + protected override void PopulateIndexes(IReadOnlyList indexes) + { + if (indexes.Count == 0) { - _logger = logger; - _parentId = parentId; - _mediaService = mediaService; - _mediaValueSetBuilder = mediaValueSetBuilder; + _logger.LogDebug( + $"{nameof(PopulateIndexes)} called with no indexes to populate. Typically means no index is registered with this populator."); + return; } - protected override void PopulateIndexes(IReadOnlyList indexes) - { - if (indexes.Count == 0) - { - _logger.LogDebug($"{nameof(PopulateIndexes)} called with no indexes to populate. Typically means no index is registered with this populator."); - return; - } + const int pageSize = 10000; + var pageIndex = 0; - const int pageSize = 10000; - var pageIndex = 0; + var mediaParentId = -1; - var mediaParentId = -1; + if (_parentId.HasValue && _parentId.Value > 0) + { + mediaParentId = _parentId.Value; + } - if (_parentId.HasValue && _parentId.Value > 0) - { - mediaParentId = _parentId.Value; - } + IMedia[] media; - IMedia[] media; + do + { + media = _mediaService.GetPagedDescendants(mediaParentId, pageIndex, pageSize, out var total).ToArray(); - do + if (media.Length > 0) { - media = _mediaService.GetPagedDescendants(mediaParentId, pageIndex, pageSize, out var total).ToArray(); - - if (media.Length > 0) + // ReSharper disable once PossibleMultipleEnumeration + foreach (IIndex index in indexes) { - // ReSharper disable once PossibleMultipleEnumeration - foreach (var index in indexes) - index.IndexItems(_mediaValueSetBuilder.GetValueSets(media)); + index.IndexItems(_mediaValueSetBuilder.GetValueSets(media)); } + } - pageIndex++; - } while (media.Length == pageSize); - } - + pageIndex++; + } while (media.Length == pageSize); } } diff --git a/src/Umbraco.Infrastructure/Examine/MediaValueSetBuilder.cs b/src/Umbraco.Infrastructure/Examine/MediaValueSetBuilder.cs index ff0c1f142c52..59b469248ebe 100644 --- a/src/Umbraco.Infrastructure/Examine/MediaValueSetBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/MediaValueSetBuilder.cs @@ -1,87 +1,76 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using Examine; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; -using Umbraco.Cms.Core.PropertyEditors.ValueConverters; -using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -using Constants = Umbraco.Cms.Core.Constants; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +public class MediaValueSetBuilder : BaseValueSetBuilder { - public class MediaValueSetBuilder : BaseValueSetBuilder - { - private readonly UrlSegmentProviderCollection _urlSegmentProviders; - private readonly MediaUrlGeneratorCollection _mediaUrlGenerators; - private readonly IUserService _userService; - private readonly IShortStringHelper _shortStringHelper; - private readonly ContentSettings _contentSettings; + private readonly ContentSettings _contentSettings; + private readonly MediaUrlGeneratorCollection _mediaUrlGenerators; + private readonly IShortStringHelper _shortStringHelper; + private readonly UrlSegmentProviderCollection _urlSegmentProviders; + private readonly IUserService _userService; - public MediaValueSetBuilder( - PropertyEditorCollection propertyEditors, - UrlSegmentProviderCollection urlSegmentProviders, - MediaUrlGeneratorCollection mediaUrlGenerators, - IUserService userService, - IShortStringHelper shortStringHelper, - IOptions contentSettings) - : base(propertyEditors, false) - { - _urlSegmentProviders = urlSegmentProviders; - _mediaUrlGenerators = mediaUrlGenerators; - _userService = userService; - _shortStringHelper = shortStringHelper; - _contentSettings = contentSettings.Value; - } + public MediaValueSetBuilder( + PropertyEditorCollection propertyEditors, + UrlSegmentProviderCollection urlSegmentProviders, + MediaUrlGeneratorCollection mediaUrlGenerators, + IUserService userService, + IShortStringHelper shortStringHelper, + IOptions contentSettings) + : base(propertyEditors, false) + { + _urlSegmentProviders = urlSegmentProviders; + _mediaUrlGenerators = mediaUrlGenerators; + _userService = userService; + _shortStringHelper = shortStringHelper; + _contentSettings = contentSettings.Value; + } - /// - public override IEnumerable GetValueSets(params IMedia[] media) + /// + public override IEnumerable GetValueSets(params IMedia[] media) + { + foreach (IMedia m in media) { - foreach (IMedia m in media) - { - - var urlValue = m.GetUrlSegment(_shortStringHelper, _urlSegmentProviders); + var urlValue = m.GetUrlSegment(_shortStringHelper, _urlSegmentProviders); - IEnumerable mediaFiles = m.GetUrls(_contentSettings, _mediaUrlGenerators) - .Select(x => Path.GetFileName(x)) - .Distinct(); + IEnumerable mediaFiles = m.GetUrls(_contentSettings, _mediaUrlGenerators) + .Select(x => Path.GetFileName(x)) + .Distinct(); - var values = new Dictionary> - { - {"icon", m.ContentType.Icon?.Yield() ?? Enumerable.Empty()}, - {"id", new object[] {m.Id}}, - {UmbracoExamineFieldNames.NodeKeyFieldName, new object[] {m.Key}}, - {"parentID", new object[] {m.Level > 1 ? m.ParentId : -1}}, - {"level", new object[] {m.Level}}, - {"creatorID", new object[] {m.CreatorId}}, - {"sortOrder", new object[] {m.SortOrder}}, - {"createDate", new object[] {m.CreateDate}}, - {"updateDate", new object[] {m.UpdateDate}}, - {UmbracoExamineFieldNames.NodeNameFieldName, m.Name?.Yield() ?? Enumerable.Empty()}, - {"urlName", urlValue?.Yield() ?? Enumerable.Empty()}, - {"path", m.Path?.Yield() ?? Enumerable.Empty()}, - {"nodeType", m.ContentType.Id.ToString().Yield() }, - {"creatorName", (m.GetCreatorProfile(_userService)?.Name ?? "??").Yield()}, - {UmbracoExamineFieldNames.UmbracoFileFieldName, mediaFiles} - }; + var values = new Dictionary> + { + {"icon", m.ContentType.Icon?.Yield() ?? Enumerable.Empty()}, + {"id", new object[] {m.Id}}, + {UmbracoExamineFieldNames.NodeKeyFieldName, new object[] {m.Key}}, + {"parentID", new object[] {m.Level > 1 ? m.ParentId : -1}}, + {"level", new object[] {m.Level}}, + {"creatorID", new object[] {m.CreatorId}}, + {"sortOrder", new object[] {m.SortOrder}}, + {"createDate", new object[] {m.CreateDate}}, + {"updateDate", new object[] {m.UpdateDate}}, + {UmbracoExamineFieldNames.NodeNameFieldName, m.Name?.Yield() ?? Enumerable.Empty()}, + {"urlName", urlValue?.Yield() ?? Enumerable.Empty()}, + {"path", m.Path?.Yield() ?? Enumerable.Empty()}, + {"nodeType", m.ContentType.Id.ToString().Yield()}, + {"creatorName", (m.GetCreatorProfile(_userService)?.Name ?? "??").Yield()}, + {UmbracoExamineFieldNames.UmbracoFileFieldName, mediaFiles} + }; - foreach (var property in m.Properties) - { - AddPropertyValue(property, null, null, values); - } + foreach (IProperty property in m.Properties) + { + AddPropertyValue(property, null, null, values); + } - var vs = new ValueSet(m.Id.ToInvariantString(), IndexTypes.Media, m.ContentType.Alias, values); + var vs = new ValueSet(m.Id.ToInvariantString(), IndexTypes.Media, m.ContentType.Alias, values); - yield return vs; - } + yield return vs; } } - } diff --git a/src/Umbraco.Infrastructure/Examine/MemberIndexPopulator.cs b/src/Umbraco.Infrastructure/Examine/MemberIndexPopulator.cs index 76dee234500b..b4f16e9ea601 100644 --- a/src/Umbraco.Infrastructure/Examine/MemberIndexPopulator.cs +++ b/src/Umbraco.Infrastructure/Examine/MemberIndexPopulator.cs @@ -1,44 +1,47 @@ -using System.Collections.Generic; -using System.Linq; -using Examine; +using Examine; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +public class MemberIndexPopulator : IndexPopulator { - public class MemberIndexPopulator : IndexPopulator + private readonly IMemberService _memberService; + private readonly IValueSetBuilder _valueSetBuilder; + + public MemberIndexPopulator(IMemberService memberService, IValueSetBuilder valueSetBuilder) { - private readonly IMemberService _memberService; - private readonly IValueSetBuilder _valueSetBuilder; + _memberService = memberService; + _valueSetBuilder = valueSetBuilder; + } - public MemberIndexPopulator(IMemberService memberService, IValueSetBuilder valueSetBuilder) + protected override void PopulateIndexes(IReadOnlyList indexes) + { + if (indexes.Count == 0) { - _memberService = memberService; - _valueSetBuilder = valueSetBuilder; + return; } - protected override void PopulateIndexes(IReadOnlyList indexes) - { - if (indexes.Count == 0) return; - const int pageSize = 1000; - var pageIndex = 0; + const int pageSize = 1000; + var pageIndex = 0; - IMember[] members; + IMember[] members; - //no node types specified, do all members - do - { - members = _memberService.GetAll(pageIndex, pageSize, out _).ToArray(); + //no node types specified, do all members + do + { + members = _memberService.GetAll(pageIndex, pageSize, out _).ToArray(); - if (members.Length > 0) + if (members.Length > 0) + { + // ReSharper disable once PossibleMultipleEnumeration + foreach (IIndex index in indexes) { - // ReSharper disable once PossibleMultipleEnumeration - foreach (var index in indexes) - index.IndexItems(_valueSetBuilder.GetValueSets(members)); + index.IndexItems(_valueSetBuilder.GetValueSets(members)); } + } - pageIndex++; - } while (members.Length == pageSize); - } + pageIndex++; + } while (members.Length == pageSize); } } diff --git a/src/Umbraco.Infrastructure/Examine/MemberValueSetBuilder.cs b/src/Umbraco.Infrastructure/Examine/MemberValueSetBuilder.cs index 33481c96a378..1b1885b29565 100644 --- a/src/Umbraco.Infrastructure/Examine/MemberValueSetBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/MemberValueSetBuilder.cs @@ -1,53 +1,48 @@ -using System.Collections.Generic; -using System.Linq; -using Examine; +using Examine; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Examine -{ +namespace Umbraco.Cms.Infrastructure.Examine; - public class MemberValueSetBuilder : BaseValueSetBuilder +public class MemberValueSetBuilder : BaseValueSetBuilder +{ + public MemberValueSetBuilder(PropertyEditorCollection propertyEditors) + : base(propertyEditors, false) { - public MemberValueSetBuilder(PropertyEditorCollection propertyEditors) - : base(propertyEditors, false) - { - } + } - /// - public override IEnumerable GetValueSets(params IMember[] members) + /// + public override IEnumerable GetValueSets(params IMember[] members) + { + foreach (IMember m in members) { - foreach (var m in members) + var values = new Dictionary> { - var values = new Dictionary> - { - {"icon", m.ContentType.Icon?.Yield() ?? Enumerable.Empty()}, - {"id", new object[] {m.Id}}, - {UmbracoExamineFieldNames.NodeKeyFieldName, new object[] {m.Key}}, - {"parentID", new object[] {m.Level > 1 ? m.ParentId : -1}}, - {"level", new object[] {m.Level}}, - {"creatorID", new object[] {m.CreatorId}}, - {"sortOrder", new object[] {m.SortOrder}}, - {"createDate", new object[] {m.CreateDate}}, - {"updateDate", new object[] {m.UpdateDate}}, - {UmbracoExamineFieldNames.NodeNameFieldName, m.Name?.Yield() ?? Enumerable.Empty()}, - {"path", m.Path?.Yield() ?? Enumerable.Empty()}, - {"nodeType", m.ContentType.Id.ToString().Yield() }, - {"loginName", m.Username?.Yield() ?? Enumerable.Empty()}, - {"email", m.Email?.Yield() ?? Enumerable.Empty()}, - }; + {"icon", m.ContentType.Icon?.Yield() ?? Enumerable.Empty()}, + {"id", new object[] {m.Id}}, + {UmbracoExamineFieldNames.NodeKeyFieldName, new object[] {m.Key}}, + {"parentID", new object[] {m.Level > 1 ? m.ParentId : -1}}, + {"level", new object[] {m.Level}}, + {"creatorID", new object[] {m.CreatorId}}, + {"sortOrder", new object[] {m.SortOrder}}, + {"createDate", new object[] {m.CreateDate}}, + {"updateDate", new object[] {m.UpdateDate}}, + {UmbracoExamineFieldNames.NodeNameFieldName, m.Name?.Yield() ?? Enumerable.Empty()}, + {"path", m.Path?.Yield() ?? Enumerable.Empty()}, + {"nodeType", m.ContentType.Id.ToString().Yield()}, + {"loginName", m.Username?.Yield() ?? Enumerable.Empty()}, + {"email", m.Email?.Yield() ?? Enumerable.Empty()} + }; - foreach (var property in m.Properties) - { - AddPropertyValue(property, null, null, values); - } + foreach (IProperty property in m.Properties) + { + AddPropertyValue(property, null, null, values); + } - var vs = new ValueSet(m.Id.ToInvariantString(), IndexTypes.Member, m.ContentType.Alias, values); + var vs = new ValueSet(m.Id.ToInvariantString(), IndexTypes.Member, m.ContentType.Alias, values); - yield return vs; - } + yield return vs; } } - } diff --git a/src/Umbraco.Infrastructure/Examine/MemberValueSetValidator.cs b/src/Umbraco.Infrastructure/Examine/MemberValueSetValidator.cs index f92a9dc62092..f375cdf31db5 100644 --- a/src/Umbraco.Infrastructure/Examine/MemberValueSetValidator.cs +++ b/src/Umbraco.Infrastructure/Examine/MemberValueSetValidator.cs @@ -1,30 +1,32 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Infrastructure.Examine; -namespace Umbraco.Cms.Infrastructure.Examine +public class MemberValueSetValidator : ValueSetValidator { - public class MemberValueSetValidator : ValueSetValidator + /// + /// By default these are the member fields we index + /// + public static readonly string[] DefaultMemberIndexFields = { - public MemberValueSetValidator() : base(null, null, DefaultMemberIndexFields, null) - { - } + "id", UmbracoExamineFieldNames.NodeNameFieldName, "updateDate", "loginName", "email", + UmbracoExamineFieldNames.NodeKeyFieldName + }; - public MemberValueSetValidator(IEnumerable includeItemTypes, IEnumerable excludeItemTypes) - : base(includeItemTypes, excludeItemTypes, DefaultMemberIndexFields, null) - { - } + private static readonly IEnumerable ValidCategories = new[] {IndexTypes.Member}; - public MemberValueSetValidator(IEnumerable includeItemTypes, IEnumerable excludeItemTypes, IEnumerable includeFields, IEnumerable excludeFields) - : base(includeItemTypes, excludeItemTypes, includeFields, excludeFields) - { - } - - /// - /// By default these are the member fields we index - /// - public static readonly string[] DefaultMemberIndexFields = { "id", UmbracoExamineFieldNames.NodeNameFieldName, "updateDate", "loginName", "email", UmbracoExamineFieldNames.NodeKeyFieldName }; + public MemberValueSetValidator() : base(null, null, DefaultMemberIndexFields, null) + { + } - private static readonly IEnumerable ValidCategories = new[] { IndexTypes.Member }; - protected override IEnumerable ValidIndexCategories => ValidCategories; + public MemberValueSetValidator(IEnumerable includeItemTypes, IEnumerable excludeItemTypes) + : base(includeItemTypes, excludeItemTypes, DefaultMemberIndexFields, null) + { + } + public MemberValueSetValidator(IEnumerable includeItemTypes, IEnumerable excludeItemTypes, + IEnumerable includeFields, IEnumerable excludeFields) + : base(includeItemTypes, excludeItemTypes, includeFields, excludeFields) + { } + + protected override IEnumerable ValidIndexCategories => ValidCategories; } diff --git a/src/Umbraco.Infrastructure/Examine/NoopBackOfficeExamineSearcher.cs b/src/Umbraco.Infrastructure/Examine/NoopBackOfficeExamineSearcher.cs index cb01bae57a52..4b60b33f89c2 100644 --- a/src/Umbraco.Infrastructure/Examine/NoopBackOfficeExamineSearcher.cs +++ b/src/Umbraco.Infrastructure/Examine/NoopBackOfficeExamineSearcher.cs @@ -1,17 +1,15 @@ -using System.Collections.Generic; -using System.Linq; using Examine; using Umbraco.Cms.Core.Models.ContentEditing; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +public class NoopBackOfficeExamineSearcher : IBackOfficeExamineSearcher { - public class NoopBackOfficeExamineSearcher : IBackOfficeExamineSearcher + public IEnumerable Search(string query, UmbracoEntityTypes entityType, int pageSize, long pageIndex, + out long totalFound, + string? searchFrom = null, bool ignoreUserStartNodes = false) { - public IEnumerable Search(string query, UmbracoEntityTypes entityType, int pageSize, long pageIndex, out long totalFound, - string? searchFrom = null, bool ignoreUserStartNodes = false) - { - totalFound = 0; - return Enumerable.Empty(); - } + totalFound = 0; + return Enumerable.Empty(); } } diff --git a/src/Umbraco.Infrastructure/Examine/PublishedContentIndexPopulator.cs b/src/Umbraco.Infrastructure/Examine/PublishedContentIndexPopulator.cs index f9ccaffdbca0..e473c689ffde 100644 --- a/src/Umbraco.Infrastructure/Examine/PublishedContentIndexPopulator.cs +++ b/src/Umbraco.Infrastructure/Examine/PublishedContentIndexPopulator.cs @@ -2,21 +2,24 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Performs the data lookups required to rebuild a content index containing only published content +/// +/// +/// The published (external) index will still rebuild just fine using the default +/// which is what +/// is used when rebuilding all indexes, but this will be used when the single index is rebuilt and will go a little +/// bit faster +/// since the data query is more specific. +/// +public class PublishedContentIndexPopulator : ContentIndexPopulator { - /// - /// Performs the data lookups required to rebuild a content index containing only published content - /// - /// - /// The published (external) index will still rebuild just fine using the default which is what - /// is used when rebuilding all indexes, but this will be used when the single index is rebuilt and will go a little bit faster - /// since the data query is more specific. - /// - public class PublishedContentIndexPopulator : ContentIndexPopulator + public PublishedContentIndexPopulator(ILogger logger, + IContentService contentService, IUmbracoDatabaseFactory umbracoDatabaseFactory, + IPublishedContentValueSetBuilder contentValueSetBuilder) : + base(logger, true, null, contentService, umbracoDatabaseFactory, contentValueSetBuilder) { - public PublishedContentIndexPopulator(ILogger logger, IContentService contentService, IUmbracoDatabaseFactory umbracoDatabaseFactory, IPublishedContentValueSetBuilder contentValueSetBuilder) : - base(logger, true, null, contentService, umbracoDatabaseFactory, contentValueSetBuilder) - { - } } } diff --git a/src/Umbraco.Infrastructure/Examine/RebuildOnStartupHandler.cs b/src/Umbraco.Infrastructure/Examine/RebuildOnStartupHandler.cs index 9ec145b030b1..3ad29b892e48 100644 --- a/src/Umbraco.Infrastructure/Examine/RebuildOnStartupHandler.cs +++ b/src/Umbraco.Infrastructure/Examine/RebuildOnStartupHandler.cs @@ -1,72 +1,68 @@ -using System; -using System.Threading; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Handles how the indexes are rebuilt on startup +/// +/// +/// On the first HTTP request this will rebuild the Examine indexes if they are empty. +/// If it is a cold boot, they are all rebuilt. +/// +public sealed class RebuildOnStartupHandler : INotificationHandler { + // These must be static because notification handlers are transient. + // this does unfortunatley mean that one RebuildOnStartupHandler instance + // will be created for each front-end request even though we only use the first one. + // TODO: Is there a better way to acheive this without allocating? We cannot remove + // a handler from the notification system. It's not a huge deal but would be better + // with less objects. + private static bool _isReady; + private static bool _isReadSet; + private static object? _isReadyLock; + private readonly ExamineIndexRebuilder _backgroundIndexRebuilder; + private readonly IRuntimeState _runtimeState; + private readonly ISyncBootStateAccessor _syncBootStateAccessor; + + public RebuildOnStartupHandler( + ISyncBootStateAccessor syncBootStateAccessor, + ExamineIndexRebuilder backgroundIndexRebuilder, + IRuntimeState runtimeState) + { + _syncBootStateAccessor = syncBootStateAccessor; + _backgroundIndexRebuilder = backgroundIndexRebuilder; + _runtimeState = runtimeState; + } + /// - /// Handles how the indexes are rebuilt on startup + /// On first http request schedule an index rebuild for any empty indexes (or all if it's a cold boot) /// - /// - /// On the first HTTP request this will rebuild the Examine indexes if they are empty. - /// If it is a cold boot, they are all rebuilt. - /// - public sealed class RebuildOnStartupHandler : INotificationHandler + /// + public void Handle(UmbracoRequestBeginNotification notification) { - private readonly ISyncBootStateAccessor _syncBootStateAccessor; - private readonly ExamineIndexRebuilder _backgroundIndexRebuilder; - private readonly IRuntimeState _runtimeState; - - // These must be static because notification handlers are transient. - // this does unfortunatley mean that one RebuildOnStartupHandler instance - // will be created for each front-end request even though we only use the first one. - // TODO: Is there a better way to acheive this without allocating? We cannot remove - // a handler from the notification system. It's not a huge deal but would be better - // with less objects. - private static bool _isReady; - private static bool _isReadSet; - private static object? _isReadyLock; - - public RebuildOnStartupHandler( - ISyncBootStateAccessor syncBootStateAccessor, - ExamineIndexRebuilder backgroundIndexRebuilder, - IRuntimeState runtimeState) + if (_runtimeState.Level != RuntimeLevel.Run) { - _syncBootStateAccessor = syncBootStateAccessor; - _backgroundIndexRebuilder = backgroundIndexRebuilder; - _runtimeState = runtimeState; + return; } - /// - /// On first http request schedule an index rebuild for any empty indexes (or all if it's a cold boot) - /// - /// - public void Handle(UmbracoRequestBeginNotification notification) - { - if (_runtimeState.Level != RuntimeLevel.Run) + LazyInitializer.EnsureInitialized( + ref _isReady, + ref _isReadSet, + ref _isReadyLock, + () => { - return; - } + SyncBootState bootState = _syncBootStateAccessor.GetSyncBootState(); - LazyInitializer.EnsureInitialized( - ref _isReady, - ref _isReadSet, - ref _isReadyLock, - () => - { - SyncBootState bootState = _syncBootStateAccessor.GetSyncBootState(); + _backgroundIndexRebuilder.RebuildIndexes( + // if it's not a cold boot, only rebuild empty ones + bootState != SyncBootState.ColdBoot, + TimeSpan.FromMinutes(1)); - _backgroundIndexRebuilder.RebuildIndexes( - // if it's not a cold boot, only rebuild empty ones - bootState != SyncBootState.ColdBoot, - TimeSpan.FromMinutes(1)); - - return true; - }); - } + return true; + }); } } diff --git a/src/Umbraco.Infrastructure/Examine/UmbracoExamineExtensions.cs b/src/Umbraco.Infrastructure/Examine/UmbracoExamineExtensions.cs index 7ae567739e5a..e2065b7c09aa 100644 --- a/src/Umbraco.Infrastructure/Examine/UmbracoExamineExtensions.cs +++ b/src/Umbraco.Infrastructure/Examine/UmbracoExamineExtensions.cs @@ -1,136 +1,136 @@ -using System.Collections.Generic; using System.Text.RegularExpressions; -using System.Threading.Tasks; using Examine; using Examine.Search; using Umbraco.Cms.Infrastructure.Examine; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class UmbracoExamineExtensions { - public static class UmbracoExamineExtensions - { - /// - /// Matches a culture iso name suffix - /// - /// - /// myFieldName_en-us will match the "en-us" - /// - internal static readonly Regex CultureIsoCodeFieldNameMatchExpression = new Regex("^(?[_\\w]+)_(?[a-z]{2,3}(-[a-z0-9]{2,4})?)$", RegexOptions.Compiled | RegexOptions.ExplicitCapture); + /// + /// Matches a culture iso name suffix + /// + /// + /// myFieldName_en-us will match the "en-us" + /// + internal static readonly Regex CultureIsoCodeFieldNameMatchExpression = new( + "^(?[_\\w]+)_(?[a-z]{2,3}(-[a-z0-9]{2,4})?)$", + RegexOptions.Compiled | RegexOptions.ExplicitCapture); - //TODO: We need a public method here to just match a field name against CultureIsoCodeFieldNameMatchExpression + //TODO: We need a public method here to just match a field name against CultureIsoCodeFieldNameMatchExpression - /// - /// Returns all index fields that are culture specific (suffixed) - /// - /// - /// - /// - public static IEnumerable GetCultureFields(this IUmbracoIndex index, string culture) - { - IEnumerable allFields = index.GetFieldNames(); + /// + /// Returns all index fields that are culture specific (suffixed) + /// + /// + /// + /// + public static IEnumerable GetCultureFields(this IUmbracoIndex index, string culture) + { + IEnumerable allFields = index.GetFieldNames(); - var results = new List(); - foreach (var field in allFields) + var results = new List(); + foreach (var field in allFields) + { + Match match = CultureIsoCodeFieldNameMatchExpression.Match(field); + if (match.Success && culture.InvariantEquals(match.Groups["CultureName"].Value)) { - var match = CultureIsoCodeFieldNameMatchExpression.Match(field); - if (match.Success && culture.InvariantEquals(match.Groups["CultureName"].Value)) - { - results.Add(field); - } + results.Add(field); } - - return results; } - /// - /// Returns all index fields that are culture specific (suffixed) or invariant - /// - /// - /// - /// - public static IEnumerable GetCultureAndInvariantFields(this IUmbracoIndex index, string culture) - { - IEnumerable allFields = index.GetFieldNames(); + return results; + } - foreach (var field in allFields) - { - var match = CultureIsoCodeFieldNameMatchExpression.Match(field); - if (match.Success && culture.InvariantEquals(match.Groups["CultureName"].Value)) - { - yield return field; //matches this culture field - } - else if (!match.Success) - { - yield return field; //matches no culture field (invariant) - } - } - } + /// + /// Returns all index fields that are culture specific (suffixed) or invariant + /// + /// + /// + /// + public static IEnumerable GetCultureAndInvariantFields(this IUmbracoIndex index, string culture) + { + IEnumerable allFields = index.GetFieldNames(); - public static IBooleanOperation Id(this IQuery query, int id) + foreach (var field in allFields) { - var fieldQuery = query.Id(id.ToInvariantString()); - return fieldQuery; + Match match = CultureIsoCodeFieldNameMatchExpression.Match(field); + if (match.Success && culture.InvariantEquals(match.Groups["CultureName"].Value)) + { + yield return field; //matches this culture field + } + else if (!match.Success) + { + yield return field; //matches no culture field (invariant) + } } + } - /// - /// Query method to search on parent id - /// - /// - /// - /// - public static IBooleanOperation ParentId(this IQuery query, int id) - { - var fieldQuery = query.Field("parentID", id); - return fieldQuery; - } + public static IBooleanOperation Id(this IQuery query, int id) + { + IBooleanOperation? fieldQuery = query.Id(id.ToInvariantString()); + return fieldQuery; + } - /// - /// Query method to search on node name - /// - /// - /// - /// - public static IBooleanOperation NodeName(this IQuery query, string nodeName) - { - var fieldQuery = query.Field(UmbracoExamineFieldNames.NodeNameFieldName, (IExamineValue)new ExamineValue(Examineness.Explicit, nodeName)); - return fieldQuery; - } + /// + /// Query method to search on parent id + /// + /// + /// + /// + public static IBooleanOperation ParentId(this IQuery query, int id) + { + IBooleanOperation? fieldQuery = query.Field("parentID", id); + return fieldQuery; + } - /// - /// Query method to search on node name - /// - /// - /// - /// - public static IBooleanOperation NodeName(this IQuery query, IExamineValue nodeName) - { - var fieldQuery = query.Field(UmbracoExamineFieldNames.NodeNameFieldName, nodeName); - return fieldQuery; - } + /// + /// Query method to search on node name + /// + /// + /// + /// + public static IBooleanOperation NodeName(this IQuery query, string nodeName) + { + IBooleanOperation? fieldQuery = query.Field(UmbracoExamineFieldNames.NodeNameFieldName, + (IExamineValue)new ExamineValue(Examineness.Explicit, nodeName)); + return fieldQuery; + } - /// - /// Query method to search on node type alias - /// - /// - /// - /// - public static IBooleanOperation NodeTypeAlias(this IQuery query, string nodeTypeAlias) - { - var fieldQuery = query.Field(ExamineFieldNames.ItemTypeFieldName, (IExamineValue)new ExamineValue(Examineness.Explicit, nodeTypeAlias)); - return fieldQuery; - } + /// + /// Query method to search on node name + /// + /// + /// + /// + public static IBooleanOperation NodeName(this IQuery query, IExamineValue nodeName) + { + IBooleanOperation? fieldQuery = query.Field(UmbracoExamineFieldNames.NodeNameFieldName, nodeName); + return fieldQuery; + } - /// - /// Query method to search on node type alias - /// - /// - /// - /// - public static IBooleanOperation NodeTypeAlias(this IQuery query, IExamineValue nodeTypeAlias) - { - var fieldQuery = query.Field(ExamineFieldNames.ItemTypeFieldName, nodeTypeAlias); - return fieldQuery; - } + /// + /// Query method to search on node type alias + /// + /// + /// + /// + public static IBooleanOperation NodeTypeAlias(this IQuery query, string nodeTypeAlias) + { + IBooleanOperation? fieldQuery = query.Field(ExamineFieldNames.ItemTypeFieldName, + (IExamineValue)new ExamineValue(Examineness.Explicit, nodeTypeAlias)); + return fieldQuery; + } + /// + /// Query method to search on node type alias + /// + /// + /// + /// + public static IBooleanOperation NodeTypeAlias(this IQuery query, IExamineValue nodeTypeAlias) + { + IBooleanOperation? fieldQuery = query.Field(ExamineFieldNames.ItemTypeFieldName, nodeTypeAlias); + return fieldQuery; } } diff --git a/src/Umbraco.Infrastructure/Examine/UmbracoExamineFieldNames.cs b/src/Umbraco.Infrastructure/Examine/UmbracoExamineFieldNames.cs index 72e914c5840c..b46ae49fa277 100644 --- a/src/Umbraco.Infrastructure/Examine/UmbracoExamineFieldNames.cs +++ b/src/Umbraco.Infrastructure/Examine/UmbracoExamineFieldNames.cs @@ -1,28 +1,28 @@ using Examine; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +public static class UmbracoExamineFieldNames { - public static class UmbracoExamineFieldNames - { - /// - /// Used to store the path of a content object - /// - public const string IndexPathFieldName = ExamineFieldNames.SpecialFieldPrefix + "Path"; - public const string NodeKeyFieldName = ExamineFieldNames.SpecialFieldPrefix + "Key"; - public const string UmbracoFileFieldName = "umbracoFileSrc"; - public const string IconFieldName = ExamineFieldNames.SpecialFieldPrefix + "Icon"; - public const string PublishedFieldName = ExamineFieldNames.SpecialFieldPrefix + "Published"; + /// + /// Used to store the path of a content object + /// + public const string IndexPathFieldName = ExamineFieldNames.SpecialFieldPrefix + "Path"; + + public const string NodeKeyFieldName = ExamineFieldNames.SpecialFieldPrefix + "Key"; + public const string UmbracoFileFieldName = "umbracoFileSrc"; + public const string IconFieldName = ExamineFieldNames.SpecialFieldPrefix + "Icon"; + public const string PublishedFieldName = ExamineFieldNames.SpecialFieldPrefix + "Published"; - /// - /// The prefix added to a field when it is duplicated in order to store the original raw value. - /// - public const string RawFieldPrefix = ExamineFieldNames.SpecialFieldPrefix + "Raw_"; + /// + /// The prefix added to a field when it is duplicated in order to store the original raw value. + /// + public const string RawFieldPrefix = ExamineFieldNames.SpecialFieldPrefix + "Raw_"; - public const string VariesByCultureFieldName = ExamineFieldNames.SpecialFieldPrefix + "VariesByCulture"; + public const string VariesByCultureFieldName = ExamineFieldNames.SpecialFieldPrefix + "VariesByCulture"; - public const string NodeNameFieldName = "nodeName"; - public const string ItemIdFieldName ="__NodeId"; - public const string CategoryFieldName = "__IndexType"; - public const string ItemTypeFieldName = "__NodeTypeAlias"; - } + public const string NodeNameFieldName = "nodeName"; + public const string ItemIdFieldName = "__NodeId"; + public const string CategoryFieldName = "__IndexType"; + public const string ItemTypeFieldName = "__NodeTypeAlias"; } diff --git a/src/Umbraco.Infrastructure/Examine/UmbracoFieldDefinitionCollection.cs b/src/Umbraco.Infrastructure/Examine/UmbracoFieldDefinitionCollection.cs index 912ae7546081..a6d47a1d6a70 100644 --- a/src/Umbraco.Infrastructure/Examine/UmbracoFieldDefinitionCollection.cs +++ b/src/Umbraco.Infrastructure/Examine/UmbracoFieldDefinitionCollection.cs @@ -1,94 +1,89 @@ -using Examine; +using System.Text.RegularExpressions; +using Examine; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Custom allowing dynamic creation of +/// +public class UmbracoFieldDefinitionCollection : FieldDefinitionCollection { /// - /// Custom allowing dynamic creation of + /// A type that defines the type of index for each Umbraco field (non user defined fields) + /// Alot of standard umbraco fields shouldn't be tokenized or even indexed, just stored into lucene + /// for retreival after searching. /// - public class UmbracoFieldDefinitionCollection : FieldDefinitionCollection + public static readonly FieldDefinition[] UmbracoIndexFieldDefinitions = + { + new("parentID", FieldDefinitionTypes.Integer), new("level", FieldDefinitionTypes.Integer), + new("writerID", FieldDefinitionTypes.Integer), new("creatorID", FieldDefinitionTypes.Integer), + new("sortOrder", FieldDefinitionTypes.Integer), new("template", FieldDefinitionTypes.Integer), + new("createDate", FieldDefinitionTypes.DateTime), new("updateDate", FieldDefinitionTypes.DateTime), + new(UmbracoExamineFieldNames.NodeKeyFieldName, FieldDefinitionTypes.InvariantCultureIgnoreCase), + new("version", FieldDefinitionTypes.Raw), new("nodeType", FieldDefinitionTypes.InvariantCultureIgnoreCase), + new("template", FieldDefinitionTypes.Raw), new("urlName", FieldDefinitionTypes.InvariantCultureIgnoreCase), + new("path", FieldDefinitionTypes.Raw), new("email", FieldDefinitionTypes.EmailAddress), + new(UmbracoExamineFieldNames.PublishedFieldName, FieldDefinitionTypes.Raw), + new(UmbracoExamineFieldNames.IndexPathFieldName, FieldDefinitionTypes.Raw), + new(UmbracoExamineFieldNames.IconFieldName, FieldDefinitionTypes.Raw), + new(UmbracoExamineFieldNames.VariesByCultureFieldName, FieldDefinitionTypes.Raw) + }; + + public UmbracoFieldDefinitionCollection() + : base(UmbracoIndexFieldDefinitions) { + } + - public UmbracoFieldDefinitionCollection() - : base(UmbracoIndexFieldDefinitions) + /// + /// Overridden to dynamically add field definitions for culture variations + /// + /// + /// + /// + /// + /// We need to do this so that we don't have to maintain a huge static list of all field names and their definitions + /// otherwise we'd have to dynamically add/remove definitions anytime languages are added/removed, etc... + /// For example, we have things like `nodeName` and `__Published` which are also used for culture fields like + /// `nodeName_en-us` + /// and we don't want to have a full static list of all of these definitions when we can just define the one definition + /// and then + /// dynamically apply that to culture specific fields. + /// There is a caveat to this however, when a field definition is found for a non-culture field we will create and + /// store a new field + /// definition for that culture so that the next time it needs to be looked up and used we are not allocating more + /// objects. This does mean + /// however that if a language is deleted, the field definitions for that language will still exist in memory. This + /// isn't going to cause any + /// problems and the mem will be cleared on next site restart but it's worth pointing out. + /// + public override bool TryGetValue(string fieldName, out FieldDefinition fieldDefinition) + { + if (base.TryGetValue(fieldName, out fieldDefinition)) { + return true; } - /// - /// A type that defines the type of index for each Umbraco field (non user defined fields) - /// Alot of standard umbraco fields shouldn't be tokenized or even indexed, just stored into lucene - /// for retreival after searching. - /// - public static readonly FieldDefinition[] UmbracoIndexFieldDefinitions = + //before we use regex to match do some faster simple matching since this is going to execute quite a lot + if (!fieldName.Contains("_")) { - new FieldDefinition("parentID", FieldDefinitionTypes.Integer), - new FieldDefinition("level", FieldDefinitionTypes.Integer), - new FieldDefinition("writerID", FieldDefinitionTypes.Integer), - new FieldDefinition("creatorID", FieldDefinitionTypes.Integer), - new FieldDefinition("sortOrder", FieldDefinitionTypes.Integer), - new FieldDefinition("template", FieldDefinitionTypes.Integer), - - new FieldDefinition("createDate", FieldDefinitionTypes.DateTime), - new FieldDefinition("updateDate", FieldDefinitionTypes.DateTime), - - new FieldDefinition(UmbracoExamineFieldNames.NodeKeyFieldName, FieldDefinitionTypes.InvariantCultureIgnoreCase), - new FieldDefinition("version", FieldDefinitionTypes.Raw), - new FieldDefinition("nodeType", FieldDefinitionTypes.InvariantCultureIgnoreCase), - new FieldDefinition("template", FieldDefinitionTypes.Raw), - new FieldDefinition("urlName", FieldDefinitionTypes.InvariantCultureIgnoreCase), - new FieldDefinition("path", FieldDefinitionTypes.Raw), - - new FieldDefinition("email", FieldDefinitionTypes.EmailAddress), - - new FieldDefinition(UmbracoExamineFieldNames.PublishedFieldName, FieldDefinitionTypes.Raw), - new FieldDefinition(UmbracoExamineFieldNames.IndexPathFieldName, FieldDefinitionTypes.Raw), - new FieldDefinition(UmbracoExamineFieldNames.IconFieldName, FieldDefinitionTypes.Raw), - new FieldDefinition(UmbracoExamineFieldNames.VariesByCultureFieldName, FieldDefinitionTypes.Raw), - }; - + return false; + } - /// - /// Overridden to dynamically add field definitions for culture variations - /// - /// - /// - /// - /// - /// We need to do this so that we don't have to maintain a huge static list of all field names and their definitions - /// otherwise we'd have to dynamically add/remove definitions anytime languages are added/removed, etc... - /// For example, we have things like `nodeName` and `__Published` which are also used for culture fields like `nodeName_en-us` - /// and we don't want to have a full static list of all of these definitions when we can just define the one definition and then - /// dynamically apply that to culture specific fields. - /// - /// There is a caveat to this however, when a field definition is found for a non-culture field we will create and store a new field - /// definition for that culture so that the next time it needs to be looked up and used we are not allocating more objects. This does mean - /// however that if a language is deleted, the field definitions for that language will still exist in memory. This isn't going to cause any - /// problems and the mem will be cleared on next site restart but it's worth pointing out. - /// - public override bool TryGetValue(string fieldName, out FieldDefinition fieldDefinition) + Match match = UmbracoExamineExtensions.CultureIsoCodeFieldNameMatchExpression.Match(fieldName); + if (match.Success) { - if (base.TryGetValue(fieldName, out fieldDefinition)) - return true; - - //before we use regex to match do some faster simple matching since this is going to execute quite a lot - if (!fieldName.Contains("_")) - return false; - - var match = UmbracoExamineExtensions.CultureIsoCodeFieldNameMatchExpression.Match(fieldName); - if (match.Success) + var nonCultureFieldName = match.Groups["FieldName"].Value; + //check if there's a definition for this and if so return the field definition for the culture field based on the non-culture field + if (base.TryGetValue(nonCultureFieldName, out FieldDefinition existingFieldDefinition)) { - var nonCultureFieldName = match.Groups["FieldName"].Value; - //check if there's a definition for this and if so return the field definition for the culture field based on the non-culture field - if (base.TryGetValue(nonCultureFieldName, out var existingFieldDefinition)) - { - //now add a new field def - fieldDefinition = GetOrAdd(fieldName, s => new FieldDefinition(s, existingFieldDefinition.Type)); - return true; - } + //now add a new field def + fieldDefinition = GetOrAdd(fieldName, s => new FieldDefinition(s, existingFieldDefinition.Type)); + return true; } - return false; } - + return false; } } diff --git a/src/Umbraco.Infrastructure/Examine/UmbracoIndexConfig.cs b/src/Umbraco.Infrastructure/Examine/UmbracoIndexConfig.cs index 49607b585153..db143e501d11 100644 --- a/src/Umbraco.Infrastructure/Examine/UmbracoIndexConfig.cs +++ b/src/Umbraco.Infrastructure/Examine/UmbracoIndexConfig.cs @@ -2,36 +2,28 @@ using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +public class UmbracoIndexConfig : IUmbracoIndexConfig { - public class UmbracoIndexConfig : IUmbracoIndexConfig + public UmbracoIndexConfig(IPublicAccessService publicAccessService, IScopeProvider scopeProvider) { + ScopeProvider = scopeProvider; + PublicAccessService = publicAccessService; + } - public UmbracoIndexConfig(IPublicAccessService publicAccessService, IScopeProvider scopeProvider) - { - ScopeProvider = scopeProvider; - PublicAccessService = publicAccessService; - } + protected IPublicAccessService PublicAccessService { get; } + protected IScopeProvider ScopeProvider { get; } - protected IPublicAccessService PublicAccessService { get; } - protected IScopeProvider ScopeProvider { get; } - public IContentValueSetValidator GetContentValueSetValidator() - { - return new ContentValueSetValidator(false, true, PublicAccessService, ScopeProvider); - } + public IContentValueSetValidator GetContentValueSetValidator() => + new ContentValueSetValidator(false, true, PublicAccessService, ScopeProvider); - public IContentValueSetValidator GetPublishedContentValueSetValidator() - { - return new ContentValueSetValidator(true, false, PublicAccessService, ScopeProvider); - } + public IContentValueSetValidator GetPublishedContentValueSetValidator() => + new ContentValueSetValidator(true, false, PublicAccessService, ScopeProvider); - /// - /// Returns the for the member indexer - /// - /// - public IValueSetValidator GetMemberValueSetValidator() - { - return new MemberValueSetValidator(); - } - } + /// + /// Returns the for the member indexer + /// + /// + public IValueSetValidator GetMemberValueSetValidator() => new MemberValueSetValidator(); } diff --git a/src/Umbraco.Infrastructure/Examine/ValueSetValidator.cs b/src/Umbraco.Infrastructure/Examine/ValueSetValidator.cs index 3bf4b97bf1f0..f28bc59295bc 100644 --- a/src/Umbraco.Infrastructure/Examine/ValueSetValidator.cs +++ b/src/Umbraco.Infrastructure/Examine/ValueSetValidator.cs @@ -1,100 +1,104 @@ -using System.Collections.Generic; -using System.Linq; -using Examine; +using Examine; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Performing basic validation of a value set +/// +public class ValueSetValidator : IValueSetValidator { - /// - /// Performing basic validation of a value set - /// - public class ValueSetValidator : IValueSetValidator + public ValueSetValidator( + IEnumerable? includeItemTypes, + IEnumerable? excludeItemTypes, + IEnumerable? includeFields, + IEnumerable? excludeFields) { - public ValueSetValidator( - IEnumerable? includeItemTypes, - IEnumerable? excludeItemTypes, - IEnumerable? includeFields, - IEnumerable? excludeFields) - { - IncludeItemTypes = includeItemTypes; - ExcludeItemTypes = excludeItemTypes; - IncludeFields = includeFields; - ExcludeFields = excludeFields; - ValidIndexCategories = null; - } + IncludeItemTypes = includeItemTypes; + ExcludeItemTypes = excludeItemTypes; + IncludeFields = includeFields; + ExcludeFields = excludeFields; + ValidIndexCategories = null; + } - protected virtual IEnumerable? ValidIndexCategories { get; } + protected virtual IEnumerable? ValidIndexCategories { get; } - /// - /// Optional inclusion list of content types to index - /// - /// - /// All other types will be ignored if they do not match this list - /// - public IEnumerable? IncludeItemTypes { get; } + /// + /// Optional inclusion list of content types to index + /// + /// + /// All other types will be ignored if they do not match this list + /// + public IEnumerable? IncludeItemTypes { get; } - /// - /// Optional exclusion list of content types to ignore - /// - /// - /// Any content type alias matched in this will not be included in the index - /// - public IEnumerable? ExcludeItemTypes { get; } + /// + /// Optional exclusion list of content types to ignore + /// + /// + /// Any content type alias matched in this will not be included in the index + /// + public IEnumerable? ExcludeItemTypes { get; } - /// - /// Optional inclusion list of index fields to index - /// - /// - /// If specified, all other fields in a will be filtered - /// - public IEnumerable? IncludeFields { get; } + /// + /// Optional inclusion list of index fields to index + /// + /// + /// If specified, all other fields in a will be filtered + /// + public IEnumerable? IncludeFields { get; } - /// - /// Optional exclusion list of index fields - /// - /// - /// If specified, all fields matching these field names will be filtered from the - /// - public IEnumerable? ExcludeFields { get; } + /// + /// Optional exclusion list of index fields + /// + /// + /// If specified, all fields matching these field names will be filtered from the + /// + public IEnumerable? ExcludeFields { get; } - public virtual ValueSetValidationResult Validate(ValueSet valueSet) + public virtual ValueSetValidationResult Validate(ValueSet valueSet) + { + if (ValidIndexCategories != null && !ValidIndexCategories.InvariantContains(valueSet.Category)) { - if (ValidIndexCategories != null && !ValidIndexCategories.InvariantContains(valueSet.Category)) - return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); + return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); + } - //check if this document is of a correct type of node type alias - if (IncludeItemTypes != null && !IncludeItemTypes.InvariantContains(valueSet.ItemType)) - return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); + //check if this document is of a correct type of node type alias + if (IncludeItemTypes != null && !IncludeItemTypes.InvariantContains(valueSet.ItemType)) + { + return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); + } - //if this node type is part of our exclusion list - if (ExcludeItemTypes != null && ExcludeItemTypes.InvariantContains(valueSet.ItemType)) - return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); + //if this node type is part of our exclusion list + if (ExcludeItemTypes != null && ExcludeItemTypes.InvariantContains(valueSet.ItemType)) + { + return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); + } - var isFiltered = false; + var isFiltered = false; - var filteredValues = valueSet.Values.ToDictionary(x => x.Key, x => x.Value.ToList()); - //filter based on the fields provided (if any) - if (IncludeFields != null || ExcludeFields != null) + var filteredValues = valueSet.Values.ToDictionary(x => x.Key, x => x.Value.ToList()); + //filter based on the fields provided (if any) + if (IncludeFields != null || ExcludeFields != null) + { + foreach (var key in valueSet.Values.Keys.ToList()) { - foreach (var key in valueSet.Values.Keys.ToList()) + if (IncludeFields != null && !IncludeFields.InvariantContains(key)) { - if (IncludeFields != null && !IncludeFields.InvariantContains(key)) - { - filteredValues.Remove(key); //remove any value with a key that doesn't match the inclusion list - isFiltered = true; - } - - if (ExcludeFields != null && ExcludeFields.InvariantContains(key)) - { - filteredValues.Remove(key); //remove any value with a key that matches the exclusion list - isFiltered = true; - } + filteredValues.Remove(key); //remove any value with a key that doesn't match the inclusion list + isFiltered = true; + } + if (ExcludeFields != null && ExcludeFields.InvariantContains(key)) + { + filteredValues.Remove(key); //remove any value with a key that matches the exclusion list + isFiltered = true; } } - - var filteredValueSet = new ValueSet(valueSet.Id, valueSet.Category, valueSet.ItemType, filteredValues.ToDictionary(x => x.Key, x => (IEnumerable)x.Value)); - return new ValueSetValidationResult(isFiltered ? ValueSetValidationStatus.Filtered : ValueSetValidationStatus.Valid, filteredValueSet); } + + var filteredValueSet = new ValueSet(valueSet.Id, valueSet.Category, valueSet.ItemType, + filteredValues.ToDictionary(x => x.Key, x => (IEnumerable)x.Value)); + return new ValueSetValidationResult( + isFiltered ? ValueSetValidationStatus.Filtered : ValueSetValidationStatus.Valid, filteredValueSet); } } diff --git a/src/Umbraco.Infrastructure/Extensions/EmailMessageExtensions.cs b/src/Umbraco.Infrastructure/Extensions/EmailMessageExtensions.cs index 8918b5f9511d..c657ec185bce 100644 --- a/src/Umbraco.Infrastructure/Extensions/EmailMessageExtensions.cs +++ b/src/Umbraco.Infrastructure/Extensions/EmailMessageExtensions.cs @@ -1,132 +1,128 @@ -using System; -using System.Collections.Generic; using MimeKit; using MimeKit.Text; using Umbraco.Cms.Core.Models.Email; -namespace Umbraco.Cms.Infrastructure.Extensions +namespace Umbraco.Cms.Infrastructure.Extensions; + +internal static class EmailMessageExtensions { - internal static class EmailMessageExtensions + public static MimeMessage ToMimeMessage(this EmailMessage mailMessage, string configuredFromAddress) { - public static MimeMessage ToMimeMessage(this EmailMessage mailMessage, string configuredFromAddress) - { - var fromEmail = string.IsNullOrEmpty(mailMessage.From) ? configuredFromAddress : mailMessage.From; + var fromEmail = string.IsNullOrEmpty(mailMessage.From) ? configuredFromAddress : mailMessage.From; - if (!InternetAddress.TryParse(fromEmail, out InternetAddress fromAddress)) - { - throw new ArgumentException($"Email could not be sent. Could not parse from address {fromEmail} as a valid email address."); - } + if (!InternetAddress.TryParse(fromEmail, out InternetAddress fromAddress)) + { + throw new ArgumentException( + $"Email could not be sent. Could not parse from address {fromEmail} as a valid email address."); + } - var messageToSend = new MimeMessage - { - From = { fromAddress }, - Subject = mailMessage.Subject, - }; + var messageToSend = new MimeMessage {From = {fromAddress}, Subject = mailMessage.Subject}; - AddAddresses(messageToSend, mailMessage.To, x => x.To, throwIfNoneValid: true); - AddAddresses(messageToSend, mailMessage.Cc, x => x.Cc); - AddAddresses(messageToSend, mailMessage.Bcc, x => x.Bcc); - AddAddresses(messageToSend, mailMessage.ReplyTo, x => x.ReplyTo); + AddAddresses(messageToSend, mailMessage.To, x => x.To, true); + AddAddresses(messageToSend, mailMessage.Cc, x => x.Cc); + AddAddresses(messageToSend, mailMessage.Bcc, x => x.Bcc); + AddAddresses(messageToSend, mailMessage.ReplyTo, x => x.ReplyTo); - if (mailMessage.HasAttachments) + if (mailMessage.HasAttachments) + { + var builder = new BodyBuilder(); + if (mailMessage.IsBodyHtml) { - var builder = new BodyBuilder(); - if (mailMessage.IsBodyHtml) - { - builder.HtmlBody = mailMessage.Body; - } - else - { - builder.TextBody = mailMessage.Body; - } - - foreach (EmailMessageAttachment attachment in mailMessage.Attachments!) - { - builder.Attachments.Add(attachment.FileName, attachment.Stream); - } - - messageToSend.Body = builder.ToMessageBody(); + builder.HtmlBody = mailMessage.Body; } else { - messageToSend.Body = new TextPart(mailMessage.IsBodyHtml ? TextFormat.Html : TextFormat.Plain) { Text = mailMessage.Body }; + builder.TextBody = mailMessage.Body; } - return messageToSend; - } - - private static void AddAddresses(MimeMessage message, string?[]? addresses, Func addressListGetter, bool throwIfNoneValid = false) - { - var foundValid = false; - if (addresses != null) + foreach (EmailMessageAttachment attachment in mailMessage.Attachments!) { - foreach (var address in addresses) - { - if (InternetAddress.TryParse(address, out InternetAddress internetAddress)) - { - addressListGetter(message).Add(internetAddress); - foundValid = true; - } - } + builder.Attachments.Add(attachment.FileName, attachment.Stream); } - if (throwIfNoneValid && foundValid == false) - { - throw new InvalidOperationException($"Email could not be sent. Could not parse a valid recipient address."); - } + messageToSend.Body = builder.ToMessageBody(); } - - public static NotificationEmailModel ToNotificationEmail(this EmailMessage emailMessage, - string? configuredFromAddress) + else { - var fromEmail = string.IsNullOrEmpty(emailMessage.From) ? configuredFromAddress : emailMessage.From; - - NotificationEmailAddress? from = ToNotificationAddress(fromEmail); - - return new NotificationEmailModel( - from, - GetNotificationAddresses(emailMessage.To), - GetNotificationAddresses(emailMessage.Cc), - GetNotificationAddresses(emailMessage.Bcc), - GetNotificationAddresses(emailMessage.ReplyTo), - emailMessage.Subject, - emailMessage.Body, - emailMessage.Attachments, - emailMessage.IsBodyHtml); + messageToSend.Body = + new TextPart(mailMessage.IsBodyHtml ? TextFormat.Html : TextFormat.Plain) {Text = mailMessage.Body}; } - private static NotificationEmailAddress? ToNotificationAddress(string? address) + return messageToSend; + } + + private static void AddAddresses(MimeMessage message, string?[]? addresses, + Func addressListGetter, bool throwIfNoneValid = false) + { + var foundValid = false; + if (addresses != null) { - if (InternetAddress.TryParse(address, out InternetAddress internetAddress)) + foreach (var address in addresses) { - if (internetAddress is MailboxAddress mailboxAddress) + if (InternetAddress.TryParse(address, out InternetAddress internetAddress)) { - return new NotificationEmailAddress(mailboxAddress.Address, internetAddress.Name); + addressListGetter(message).Add(internetAddress); + foundValid = true; } } + } - return null; + if (throwIfNoneValid && foundValid == false) + { + throw new InvalidOperationException("Email could not be sent. Could not parse a valid recipient address."); } + } - private static IEnumerable? GetNotificationAddresses(IEnumerable? addresses) + public static NotificationEmailModel ToNotificationEmail(this EmailMessage emailMessage, + string? configuredFromAddress) + { + var fromEmail = string.IsNullOrEmpty(emailMessage.From) ? configuredFromAddress : emailMessage.From; + + NotificationEmailAddress? from = ToNotificationAddress(fromEmail); + + return new NotificationEmailModel( + from, + GetNotificationAddresses(emailMessage.To), + GetNotificationAddresses(emailMessage.Cc), + GetNotificationAddresses(emailMessage.Bcc), + GetNotificationAddresses(emailMessage.ReplyTo), + emailMessage.Subject, + emailMessage.Body, + emailMessage.Attachments, + emailMessage.IsBodyHtml); + } + + private static NotificationEmailAddress? ToNotificationAddress(string? address) + { + if (InternetAddress.TryParse(address, out InternetAddress internetAddress)) { - if (addresses is null) + if (internetAddress is MailboxAddress mailboxAddress) { - return null; + return new NotificationEmailAddress(mailboxAddress.Address, internetAddress.Name); } + } - var notificationAddresses = new List(); + return null; + } - foreach (var address in addresses) + private static IEnumerable? GetNotificationAddresses(IEnumerable? addresses) + { + if (addresses is null) + { + return null; + } + + var notificationAddresses = new List(); + + foreach (var address in addresses) + { + NotificationEmailAddress? notificationAddress = ToNotificationAddress(address); + if (notificationAddress is not null) { - NotificationEmailAddress? notificationAddress = ToNotificationAddress(address); - if (notificationAddress is not null) - { - notificationAddresses.Add(notificationAddress); - } + notificationAddresses.Add(notificationAddress); } - - return notificationAddresses; } + + return notificationAddresses; } } diff --git a/src/Umbraco.Infrastructure/Extensions/InfrastuctureTypeLoaderExtensions.cs b/src/Umbraco.Infrastructure/Extensions/InfrastuctureTypeLoaderExtensions.cs index b420f77253ff..4f5eb30c9562 100644 --- a/src/Umbraco.Infrastructure/Extensions/InfrastuctureTypeLoaderExtensions.cs +++ b/src/Umbraco.Infrastructure/Extensions/InfrastuctureTypeLoaderExtensions.cs @@ -1,18 +1,15 @@ -using System; -using System.Collections.Generic; -using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Packaging; -namespace Umbraco.Extensions -{ - public static class InfrastuctureTypeLoaderExtensions - { - /// - /// Gets all types implementing - /// - /// - /// - public static IEnumerable GetPackageMigrationPlans(this TypeLoader mgr) => mgr.GetTypes(); +namespace Umbraco.Extensions; - } +public static class InfrastuctureTypeLoaderExtensions +{ + /// + /// Gets all types implementing + /// + /// + /// + public static IEnumerable GetPackageMigrationPlans(this TypeLoader mgr) => + mgr.GetTypes(); } diff --git a/src/Umbraco.Infrastructure/Extensions/InstanceIdentifiableExtensions.cs b/src/Umbraco.Infrastructure/Extensions/InstanceIdentifiableExtensions.cs index 10f919589a43..20007bd11da8 100644 --- a/src/Umbraco.Infrastructure/Extensions/InstanceIdentifiableExtensions.cs +++ b/src/Umbraco.Infrastructure/Extensions/InstanceIdentifiableExtensions.cs @@ -1,20 +1,16 @@ -using System; -using System.Collections.Generic; -using System.Text; using Umbraco.Cms.Core.Scoping; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +internal static class InstanceIdentifiableExtensions { - internal static class InstanceIdentifiableExtensions + public static string GetDebugInfo(this IInstanceIdentifiable instance) { - public static string GetDebugInfo(this IInstanceIdentifiable instance) + if (instance == null) { - if (instance == null) - { - return "(NULL)"; - } - - return $"(id: {instance.InstanceId.ToString("N").Substring(0, 8)} from thread: {instance.CreatedThreadId})"; + return "(NULL)"; } + + return $"(id: {instance.InstanceId.ToString("N").Substring(0, 8)} from thread: {instance.CreatedThreadId})"; } } diff --git a/src/Umbraco.Infrastructure/Extensions/MediaPicker3ConfigurationExtensions.cs b/src/Umbraco.Infrastructure/Extensions/MediaPicker3ConfigurationExtensions.cs index 62a3f96b221d..42fa901fd56f 100644 --- a/src/Umbraco.Infrastructure/Extensions/MediaPicker3ConfigurationExtensions.cs +++ b/src/Umbraco.Infrastructure/Extensions/MediaPicker3ConfigurationExtensions.cs @@ -1,43 +1,42 @@ -using System.Collections.Generic; -using System.Linq; -using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.PropertyEditors.ValueConverters; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class MediaPicker3ConfigurationExtensions { - public static class MediaPicker3ConfigurationExtensions + /// + /// Applies the configuration to ensure only valid crops are kept and have the correct width/height. + /// + /// The configuration. + public static void ApplyConfiguration(this ImageCropperValue imageCropperValue, + MediaPicker3Configuration? configuration) { - /// - /// Applies the configuration to ensure only valid crops are kept and have the correct width/height. - /// - /// The configuration. - public static void ApplyConfiguration(this ImageCropperValue imageCropperValue, MediaPicker3Configuration? configuration) - { - var crops = new List(); + var crops = new List(); - var configuredCrops = configuration?.Crops; - if (configuredCrops != null) + MediaPicker3Configuration.CropConfiguration[]? configuredCrops = configuration?.Crops; + if (configuredCrops != null) + { + foreach (MediaPicker3Configuration.CropConfiguration configuredCrop in configuredCrops) { - foreach (var configuredCrop in configuredCrops) - { - var crop = imageCropperValue.Crops?.FirstOrDefault(x => x.Alias == configuredCrop.Alias); + ImageCropperValue.ImageCropperCrop? crop = + imageCropperValue.Crops?.FirstOrDefault(x => x.Alias == configuredCrop.Alias); - crops.Add(new ImageCropperValue.ImageCropperCrop - { - Alias = configuredCrop.Alias, - Width = configuredCrop.Width, - Height = configuredCrop.Height, - Coordinates = crop?.Coordinates - }); - } + crops.Add(new ImageCropperValue.ImageCropperCrop + { + Alias = configuredCrop.Alias, + Width = configuredCrop.Width, + Height = configuredCrop.Height, + Coordinates = crop?.Coordinates + }); } + } - imageCropperValue.Crops = crops; + imageCropperValue.Crops = crops; - if (configuration?.EnableLocalFocalPoint == false) - { - imageCropperValue.FocalPoint = null; - } + if (configuration?.EnableLocalFocalPoint == false) + { + imageCropperValue.FocalPoint = null; } } } diff --git a/src/Umbraco.Infrastructure/Extensions/ObjectJsonExtensions.cs b/src/Umbraco.Infrastructure/Extensions/ObjectJsonExtensions.cs index 609d8165a3da..5afcb87bb284 100644 --- a/src/Umbraco.Infrastructure/Extensions/ObjectJsonExtensions.cs +++ b/src/Umbraco.Infrastructure/Extensions/ObjectJsonExtensions.cs @@ -1,52 +1,56 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; +using System.Collections.Concurrent; using System.Reflection; using Newtonsoft.Json; using Umbraco.Cms.Core; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides object extension methods. +/// +public static class ObjectJsonExtensions { + private static readonly ConcurrentDictionary> ToObjectTypes = new(); + /// - /// Provides object extension methods. + /// Converts an object's properties into a dictionary. /// - public static class ObjectJsonExtensions + /// The object to convert. + /// A property namer function. + /// A dictionary containing each properties. + public static Dictionary ToObjectDictionary(T obj, Func? namer = null) { - private static readonly ConcurrentDictionary> ToObjectTypes = new ConcurrentDictionary>(); - - /// - /// Converts an object's properties into a dictionary. - /// - /// The object to convert. - /// A property namer function. - /// A dictionary containing each properties. - public static Dictionary ToObjectDictionary(T obj, Func? namer = null) + if (obj == null) { - if (obj == null) return new Dictionary(); - - string DefaultNamer(PropertyInfo property) - { - var jsonProperty = property.GetCustomAttribute(); - return jsonProperty?.PropertyName ?? property.Name; - } + return new Dictionary(); + } - var t = obj.GetType(); + string DefaultNamer(PropertyInfo property) + { + JsonPropertyAttribute? jsonProperty = property.GetCustomAttribute(); + return jsonProperty?.PropertyName ?? property.Name; + } - if (namer == null) namer = DefaultNamer; + Type t = obj.GetType(); - if (!ToObjectTypes.TryGetValue(t, out var properties)) - { - properties = new Dictionary(); + if (namer == null) + { + namer = DefaultNamer; + } - foreach (var p in t.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy)) - properties[namer(p)] = ReflectionUtilities.EmitPropertyGetter(p); + if (!ToObjectTypes.TryGetValue(t, out Dictionary? properties)) + { + properties = new Dictionary(); - ToObjectTypes[t] = properties; + foreach (PropertyInfo p in t.GetProperties(BindingFlags.Public | BindingFlags.Instance | + BindingFlags.FlattenHierarchy)) + { + properties[namer(p)] = ReflectionUtilities.EmitPropertyGetter(p); } - return properties.ToDictionary(x => x.Key, x => ((Func) x.Value)(obj)); + ToObjectTypes[t] = properties; } + return properties.ToDictionary(x => x.Key, x => ((Func)x.Value)(obj)); } } diff --git a/src/Umbraco.Infrastructure/Extensions/ScopeExtensions.cs b/src/Umbraco.Infrastructure/Extensions/ScopeExtensions.cs index ed2c520070bf..d49c57692432 100644 --- a/src/Umbraco.Infrastructure/Extensions/ScopeExtensions.cs +++ b/src/Umbraco.Infrastructure/Extensions/ScopeExtensions.cs @@ -1,24 +1,22 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Scoping; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class ScopeExtensions { - public static class ScopeExtensions + public static void ReadLock(this IScope scope, ICollection lockIds) { - public static void ReadLock(this IScope scope, ICollection lockIds) + foreach (var lockId in lockIds) { - foreach(var lockId in lockIds) - { - scope.ReadLock(lockId); - } + scope.ReadLock(lockId); } + } - public static void WriteLock(this IScope scope, ICollection lockIds) + public static void WriteLock(this IScope scope, ICollection lockIds) + { + foreach (var lockId in lockIds) { - foreach (var lockId in lockIds) - { - scope.WriteLock(lockId); - } + scope.WriteLock(lockId); } } } diff --git a/src/Umbraco.Infrastructure/HealthChecks/MarkdownToHtmlConverter.cs b/src/Umbraco.Infrastructure/HealthChecks/MarkdownToHtmlConverter.cs index 64e329be97c7..b36d5923be5f 100644 --- a/src/Umbraco.Infrastructure/HealthChecks/MarkdownToHtmlConverter.cs +++ b/src/Umbraco.Infrastructure/HealthChecks/MarkdownToHtmlConverter.cs @@ -2,33 +2,30 @@ using Umbraco.Cms.Core.HealthChecks; using Umbraco.Cms.Core.HealthChecks.NotificationMethods; -namespace Umbraco.Cms.Infrastructure.HealthChecks +namespace Umbraco.Cms.Infrastructure.HealthChecks; + +public class MarkdownToHtmlConverter : IMarkdownToHtmlConverter { - public class MarkdownToHtmlConverter : IMarkdownToHtmlConverter + public string ToHtml(HealthCheckResults results, HealthCheckNotificationVerbosity verbosity) { - public string ToHtml(HealthCheckResults results, HealthCheckNotificationVerbosity verbosity) - { - var mark = new Markdown(); - var html = mark.Transform(results.ResultsAsMarkDown(verbosity)); - html = ApplyHtmlHighlighting(html); - return html; - } - - private string ApplyHtmlHighlighting(string html) - { - const string SuccessHexColor = "5cb85c"; - const string WarningHexColor = "f0ad4e"; - const string ErrorHexColor = "d9534f"; + var mark = new Markdown(); + var html = mark.Transform(results.ResultsAsMarkDown(verbosity)); + html = ApplyHtmlHighlighting(html); + return html; + } - html = ApplyHtmlHighlightingForStatus(html, StatusResultType.Success, SuccessHexColor); - html = ApplyHtmlHighlightingForStatus(html, StatusResultType.Warning, WarningHexColor); - return ApplyHtmlHighlightingForStatus(html, StatusResultType.Error, ErrorHexColor); - } + private string ApplyHtmlHighlighting(string html) + { + const string SuccessHexColor = "5cb85c"; + const string WarningHexColor = "f0ad4e"; + const string ErrorHexColor = "d9534f"; - private string ApplyHtmlHighlightingForStatus(string html, StatusResultType status, string color) - { - return html - .Replace("Result: '" + status + "'", "Result: " + status + ""); - } + html = ApplyHtmlHighlightingForStatus(html, StatusResultType.Success, SuccessHexColor); + html = ApplyHtmlHighlightingForStatus(html, StatusResultType.Warning, WarningHexColor); + return ApplyHtmlHighlightingForStatus(html, StatusResultType.Error, ErrorHexColor); } + + private string ApplyHtmlHighlightingForStatus(string html, StatusResultType status, string color) => + html + .Replace("Result: '" + status + "'", "Result: " + status + ""); } diff --git a/src/Umbraco.Infrastructure/HostedServices/BackgroundTaskQueue.cs b/src/Umbraco.Infrastructure/HostedServices/BackgroundTaskQueue.cs index ece1827ed00c..ad31a642472c 100644 --- a/src/Umbraco.Infrastructure/HostedServices/BackgroundTaskQueue.cs +++ b/src/Umbraco.Infrastructure/HostedServices/BackgroundTaskQueue.cs @@ -1,42 +1,37 @@ -using System; -using System.Collections.Concurrent; -using System.Threading; -using System.Threading.Tasks; +using System.Collections.Concurrent; -namespace Umbraco.Cms.Infrastructure.HostedServices +namespace Umbraco.Cms.Infrastructure.HostedServices; + +/// +/// A Background Task Queue, to enqueue tasks for executing in the background. +/// +/// +/// Borrowed from https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0 +/// +public class BackgroundTaskQueue : IBackgroundTaskQueue { - /// - /// A Background Task Queue, to enqueue tasks for executing in the background. - /// - /// - /// Borrowed from https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0 - /// - public class BackgroundTaskQueue : IBackgroundTaskQueue - { - private readonly ConcurrentQueue> _workItems = - new ConcurrentQueue>(); + private readonly SemaphoreSlim _signal = new(0); - private readonly SemaphoreSlim _signal = new SemaphoreSlim(0); + private readonly ConcurrentQueue> _workItems = new(); - /// - public void QueueBackgroundWorkItem(Func workItem) + /// + public void QueueBackgroundWorkItem(Func workItem) + { + if (workItem == null) { - if (workItem == null) - { - throw new ArgumentNullException(nameof(workItem)); - } - - _workItems.Enqueue(workItem); - _signal.Release(); + throw new ArgumentNullException(nameof(workItem)); } - /// - public async Task?> DequeueAsync(CancellationToken cancellationToken) - { - await _signal.WaitAsync(cancellationToken); - _workItems.TryDequeue(out Func? workItem); + _workItems.Enqueue(workItem); + _signal.Release(); + } - return workItem; - } + /// + public async Task?> DequeueAsync(CancellationToken cancellationToken) + { + await _signal.WaitAsync(cancellationToken); + _workItems.TryDequeue(out Func? workItem); + + return workItem; } } diff --git a/src/Umbraco.Infrastructure/HostedServices/ContentVersionCleanup.cs b/src/Umbraco.Infrastructure/HostedServices/ContentVersionCleanup.cs index 5fdd69035d1e..1b62e8e31dbe 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ContentVersionCleanup.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ContentVersionCleanup.cs @@ -1,5 +1,3 @@ -using System; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -8,89 +6,89 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Infrastructure.HostedServices +namespace Umbraco.Cms.Infrastructure.HostedServices; + +/// +/// Recurring hosted service that executes the content history cleanup. +/// +public class ContentVersionCleanup : RecurringHostedServiceBase { + private readonly ILogger _logger; + private readonly IMainDom _mainDom; + private readonly IRuntimeState _runtimeState; + private readonly IServerRoleAccessor _serverRoleAccessor; + private readonly IContentVersionService _service; + private readonly IOptionsMonitor _settingsMonitor; + /// - /// Recurring hosted service that executes the content history cleanup. + /// Initializes a new instance of the class. /// - public class ContentVersionCleanup : RecurringHostedServiceBase + public ContentVersionCleanup( + IRuntimeState runtimeState, + ILogger logger, + IOptionsMonitor settingsMonitor, + IContentVersionService service, + IMainDom mainDom, + IServerRoleAccessor serverRoleAccessor) + : base(logger, TimeSpan.FromHours(1), TimeSpan.FromMinutes(3)) { - private readonly IRuntimeState _runtimeState; - private readonly ILogger _logger; - private readonly IOptionsMonitor _settingsMonitor; - private readonly IContentVersionService _service; - private readonly IMainDom _mainDom; - private readonly IServerRoleAccessor _serverRoleAccessor; + _runtimeState = runtimeState; + _logger = logger; + _settingsMonitor = settingsMonitor; + _service = service; + _mainDom = mainDom; + _serverRoleAccessor = serverRoleAccessor; + } - /// - /// Initializes a new instance of the class. - /// - public ContentVersionCleanup( - IRuntimeState runtimeState, - ILogger logger, - IOptionsMonitor settingsMonitor, - IContentVersionService service, - IMainDom mainDom, - IServerRoleAccessor serverRoleAccessor) - : base(logger, TimeSpan.FromHours(1), TimeSpan.FromMinutes(3)) + /// + public override Task PerformExecuteAsync(object? state) + { + // Globally disabled by feature flag + if (!_settingsMonitor.CurrentValue.ContentVersionCleanupPolicy.EnableCleanup) { - _runtimeState = runtimeState; - _logger = logger; - _settingsMonitor = settingsMonitor; - _service = service; - _mainDom = mainDom; - _serverRoleAccessor = serverRoleAccessor; + _logger.LogInformation( + "ContentVersionCleanup task will not run as it has been globally disabled via configuration"); + return Task.CompletedTask; } - /// - public override Task PerformExecuteAsync(object? state) + if (_runtimeState.Level != RuntimeLevel.Run) { - // Globally disabled by feature flag - if (!_settingsMonitor.CurrentValue.ContentVersionCleanupPolicy.EnableCleanup) - { - _logger.LogInformation("ContentVersionCleanup task will not run as it has been globally disabled via configuration"); - return Task.CompletedTask; - } - - if (_runtimeState.Level != RuntimeLevel.Run) - { - return Task.FromResult(true); // repeat... - } - - // Don't run on replicas nor unknown role servers - switch (_serverRoleAccessor.CurrentServerRole) - { - case ServerRole.Subscriber: - _logger.LogDebug("Does not run on subscriber servers"); - return Task.CompletedTask; - case ServerRole.Unknown: - _logger.LogDebug("Does not run on servers with unknown role"); - return Task.CompletedTask; - case ServerRole.Single: - case ServerRole.SchedulingPublisher: - default: - break; - } + return Task.FromResult(true); // repeat... + } - // Ensure we do not run if not main domain, but do NOT lock it - if (!_mainDom.IsMainDom) - { - _logger.LogDebug("Does not run if not MainDom"); - return Task.FromResult(false); // do NOT repeat, going down - } + // Don't run on replicas nor unknown role servers + switch (_serverRoleAccessor.CurrentServerRole) + { + case ServerRole.Subscriber: + _logger.LogDebug("Does not run on subscriber servers"); + return Task.CompletedTask; + case ServerRole.Unknown: + _logger.LogDebug("Does not run on servers with unknown role"); + return Task.CompletedTask; + case ServerRole.Single: + case ServerRole.SchedulingPublisher: + default: + break; + } - var count = _service.PerformContentVersionCleanup(DateTime.Now).Count; + // Ensure we do not run if not main domain, but do NOT lock it + if (!_mainDom.IsMainDom) + { + _logger.LogDebug("Does not run if not MainDom"); + return Task.FromResult(false); // do NOT repeat, going down + } - if (count > 0) - { - _logger.LogInformation("Deleted {count} ContentVersion(s)", count); - } - else - { - _logger.LogDebug("Task complete, no items were Deleted"); - } + var count = _service.PerformContentVersionCleanup(DateTime.Now).Count; - return Task.FromResult(true); + if (count > 0) + { + _logger.LogInformation("Deleted {count} ContentVersion(s)", count); + } + else + { + _logger.LogDebug("Task complete, no items were Deleted"); } + + return Task.FromResult(true); } } diff --git a/src/Umbraco.Infrastructure/HostedServices/HealthCheckNotifier.cs b/src/Umbraco.Infrastructure/HostedServices/HealthCheckNotifier.cs index efbb30291e6d..5a9cf8f84901 100644 --- a/src/Umbraco.Infrastructure/HostedServices/HealthCheckNotifier.cs +++ b/src/Umbraco.Infrastructure/HostedServices/HealthCheckNotifier.cs @@ -1,10 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -19,123 +15,122 @@ using Umbraco.Cms.Core.Sync; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.HostedServices +namespace Umbraco.Cms.Infrastructure.HostedServices; + +/// +/// Hosted service implementation for recurring health check notifications. +/// +public class HealthCheckNotifier : RecurringHostedServiceBase { + private readonly HealthCheckCollection _healthChecks; + private readonly ILogger _logger; + private readonly IMainDom _mainDom; + private readonly HealthCheckNotificationMethodCollection _notifications; + private readonly IProfilingLogger _profilingLogger; + private readonly IRuntimeState _runtimeState; + private readonly ICoreScopeProvider _scopeProvider; + private readonly IServerRoleAccessor _serverRegistrar; + private HealthChecksSettings _healthChecksSettings; + /// - /// Hosted service implementation for recurring health check notifications. + /// Initializes a new instance of the class. /// - public class HealthCheckNotifier : RecurringHostedServiceBase + /// The configuration for health check settings. + /// The collection of healthchecks. + /// The collection of healthcheck notification methods. + /// Representation of the state of the Umbraco runtime. + /// Provider of server registrations to the distributed cache. + /// Representation of the main application domain. + /// Provides scopes for database operations. + /// The typed logger. + /// The profiling logger. + /// Parser of crontab expressions. + public HealthCheckNotifier( + IOptionsMonitor healthChecksSettings, + HealthCheckCollection healthChecks, + HealthCheckNotificationMethodCollection notifications, + IRuntimeState runtimeState, + IServerRoleAccessor serverRegistrar, + IMainDom mainDom, + ICoreScopeProvider scopeProvider, + ILogger logger, + IProfilingLogger profilingLogger, + ICronTabParser cronTabParser) + : base( + logger, + healthChecksSettings.CurrentValue.Notification.Period, + healthChecksSettings.CurrentValue.GetNotificationDelay(cronTabParser, DateTime.Now, DefaultDelay)) { - private HealthChecksSettings _healthChecksSettings; - private readonly HealthCheckCollection _healthChecks; - private readonly HealthCheckNotificationMethodCollection _notifications; - private readonly IRuntimeState _runtimeState; - private readonly IServerRoleAccessor _serverRegistrar; - private readonly IMainDom _mainDom; - private readonly ICoreScopeProvider _scopeProvider; - private readonly ILogger _logger; - private readonly IProfilingLogger _profilingLogger; + _healthChecksSettings = healthChecksSettings.CurrentValue; + _healthChecks = healthChecks; + _notifications = notifications; + _runtimeState = runtimeState; + _serverRegistrar = serverRegistrar; + _mainDom = mainDom; + _scopeProvider = scopeProvider; + _logger = logger; + _profilingLogger = profilingLogger; - /// - /// Initializes a new instance of the class. - /// - /// The configuration for health check settings. - /// The collection of healthchecks. - /// The collection of healthcheck notification methods. - /// Representation of the state of the Umbraco runtime. - /// Provider of server registrations to the distributed cache. - /// Representation of the main application domain. - /// Provides scopes for database operations. - /// The typed logger. - /// The profiling logger. - /// Parser of crontab expressions. - public HealthCheckNotifier( - IOptionsMonitor healthChecksSettings, - HealthCheckCollection healthChecks, - HealthCheckNotificationMethodCollection notifications, - IRuntimeState runtimeState, - IServerRoleAccessor serverRegistrar, - IMainDom mainDom, - ICoreScopeProvider scopeProvider, - ILogger logger, - IProfilingLogger profilingLogger, - ICronTabParser cronTabParser) - : base( - logger, - healthChecksSettings.CurrentValue.Notification.Period, - healthChecksSettings.CurrentValue.GetNotificationDelay(cronTabParser, DateTime.Now, DefaultDelay)) + healthChecksSettings.OnChange(x => { - _healthChecksSettings = healthChecksSettings.CurrentValue; - _healthChecks = healthChecks; - _notifications = notifications; - _runtimeState = runtimeState; - _serverRegistrar = serverRegistrar; - _mainDom = mainDom; - _scopeProvider = scopeProvider; - _logger = logger; - _profilingLogger = profilingLogger; + _healthChecksSettings = x; + ChangePeriod(x.Notification.Period); + }); + } - healthChecksSettings.OnChange(x => - { - _healthChecksSettings = x; - ChangePeriod(x.Notification.Period); - }); + public override async Task PerformExecuteAsync(object? state) + { + if (_healthChecksSettings.Notification.Enabled == false) + { + return; } - public override async Task PerformExecuteAsync(object? state) + if (_runtimeState.Level != RuntimeLevel.Run) { - if (_healthChecksSettings.Notification.Enabled == false) - { - return; - } + return; + } - if (_runtimeState.Level != RuntimeLevel.Run) - { + switch (_serverRegistrar.CurrentServerRole) + { + case ServerRole.Subscriber: + _logger.LogDebug("Does not run on subscriber servers."); return; - } - - switch (_serverRegistrar.CurrentServerRole) - { - case ServerRole.Subscriber: - _logger.LogDebug("Does not run on subscriber servers."); - return; - case ServerRole.Unknown: - _logger.LogDebug("Does not run on servers with unknown role."); - return; - } - - // Ensure we do not run if not main domain, but do NOT lock it - if (_mainDom.IsMainDom == false) - { - _logger.LogDebug("Does not run if not MainDom."); + case ServerRole.Unknown: + _logger.LogDebug("Does not run on servers with unknown role."); return; - } + } - // Ensure we use an explicit scope since we are running on a background thread and plugin health - // checks can be making service/database calls so we want to ensure the CallContext/Ambient scope - // isn't used since that can be problematic. - using (ICoreScope scope = _scopeProvider.CreateCoreScope()) - using (_profilingLogger.DebugDuration("Health checks executing", "Health checks complete")) - { - // Don't notify for any checks that are disabled, nor for any disabled just for notifications. - Guid[] disabledCheckIds = _healthChecksSettings.Notification.DisabledChecks - .Select(x => x.Id) - .Union(_healthChecksSettings.DisabledChecks - .Select(x => x.Id)) - .Distinct() - .ToArray(); + // Ensure we do not run if not main domain, but do NOT lock it + if (_mainDom.IsMainDom == false) + { + _logger.LogDebug("Does not run if not MainDom."); + return; + } - IEnumerable checks = _healthChecks - .Where(x => disabledCheckIds.Contains(x.Id) == false); + // Ensure we use an explicit scope since we are running on a background thread and plugin health + // checks can be making service/database calls so we want to ensure the CallContext/Ambient scope + // isn't used since that can be problematic. + using (ICoreScope scope = _scopeProvider.CreateCoreScope()) + using (_profilingLogger.DebugDuration("Health checks executing", "Health checks complete")) + { + // Don't notify for any checks that are disabled, nor for any disabled just for notifications. + Guid[] disabledCheckIds = _healthChecksSettings.Notification.DisabledChecks + .Select(x => x.Id) + .Union(_healthChecksSettings.DisabledChecks + .Select(x => x.Id)) + .Distinct() + .ToArray(); + + IEnumerable checks = _healthChecks + .Where(x => disabledCheckIds.Contains(x.Id) == false); - var results = await HealthCheckResults.Create(checks); - results.LogResults(); + HealthCheckResults results = await HealthCheckResults.Create(checks); + results.LogResults(); - // Send using registered notification methods that are enabled. - foreach (IHealthCheckNotificationMethod notificationMethod in _notifications.Where(x => x.Enabled)) - { - await notificationMethod.SendAsync(results); - } + // Send using registered notification methods that are enabled. + foreach (IHealthCheckNotificationMethod notificationMethod in _notifications.Where(x => x.Enabled)) + { + await notificationMethod.SendAsync(results); } } } diff --git a/src/Umbraco.Infrastructure/HostedServices/IBackgroundTaskQueue.cs b/src/Umbraco.Infrastructure/HostedServices/IBackgroundTaskQueue.cs index 4e3052dd9eb1..346372fdcbc5 100644 --- a/src/Umbraco.Infrastructure/HostedServices/IBackgroundTaskQueue.cs +++ b/src/Umbraco.Infrastructure/HostedServices/IBackgroundTaskQueue.cs @@ -1,25 +1,20 @@ -using System; -using System.Threading; -using System.Threading.Tasks; +namespace Umbraco.Cms.Infrastructure.HostedServices; -namespace Umbraco.Cms.Infrastructure.HostedServices +/// +/// A Background Task Queue, to enqueue tasks for executing in the background. +/// +/// +/// Borrowed from https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0 +/// +public interface IBackgroundTaskQueue { /// - /// A Background Task Queue, to enqueue tasks for executing in the background. + /// Enqueue a work item to be executed on in the background. /// - /// - /// Borrowed from https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0 - /// - public interface IBackgroundTaskQueue - { - /// - /// Enqueue a work item to be executed on in the background. - /// - void QueueBackgroundWorkItem(Func workItem); + void QueueBackgroundWorkItem(Func workItem); - /// - /// Dequeue the first item on the queue. - /// - Task?> DequeueAsync(CancellationToken cancellationToken); - } + /// + /// Dequeue the first item on the queue. + /// + Task?> DequeueAsync(CancellationToken cancellationToken); } diff --git a/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs b/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs index e6b8ce47eb4f..7d78be62ab22 100644 --- a/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs +++ b/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs @@ -1,10 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.IO; -using System.Net.Http; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -16,99 +12,99 @@ using Umbraco.Cms.Core.Sync; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.HostedServices +namespace Umbraco.Cms.Infrastructure.HostedServices; + +/// +/// Hosted service implementation for keep alive feature. +/// +public class KeepAlive : RecurringHostedServiceBase { + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IHttpClientFactory _httpClientFactory; + private readonly ILogger _logger; + private readonly IMainDom _mainDom; + private readonly IProfilingLogger _profilingLogger; + private readonly IServerRoleAccessor _serverRegistrar; + private KeepAliveSettings _keepAliveSettings; + /// - /// Hosted service implementation for keep alive feature. + /// Initializes a new instance of the class. /// - public class KeepAlive : RecurringHostedServiceBase + /// The current hosting environment + /// Representation of the main application domain. + /// The configuration for keep alive settings. + /// The typed logger. + /// The profiling logger. + /// Provider of server registrations to the distributed cache. + /// Factory for instances. + public KeepAlive( + IHostingEnvironment hostingEnvironment, + IMainDom mainDom, + IOptionsMonitor keepAliveSettings, + ILogger logger, + IProfilingLogger profilingLogger, + IServerRoleAccessor serverRegistrar, + IHttpClientFactory httpClientFactory) + : base(logger, TimeSpan.FromMinutes(5), DefaultDelay) { - private readonly IHostingEnvironment _hostingEnvironment; - private readonly IMainDom _mainDom; - private KeepAliveSettings _keepAliveSettings; - private readonly ILogger _logger; - private readonly IProfilingLogger _profilingLogger; - private readonly IServerRoleAccessor _serverRegistrar; - private readonly IHttpClientFactory _httpClientFactory; + _hostingEnvironment = hostingEnvironment; + _mainDom = mainDom; + _keepAliveSettings = keepAliveSettings.CurrentValue; + _logger = logger; + _profilingLogger = profilingLogger; + _serverRegistrar = serverRegistrar; + _httpClientFactory = httpClientFactory; - /// - /// Initializes a new instance of the class. - /// - /// The current hosting environment - /// Representation of the main application domain. - /// The configuration for keep alive settings. - /// The typed logger. - /// The profiling logger. - /// Provider of server registrations to the distributed cache. - /// Factory for instances. - public KeepAlive( - IHostingEnvironment hostingEnvironment, - IMainDom mainDom, - IOptionsMonitor keepAliveSettings, - ILogger logger, - IProfilingLogger profilingLogger, - IServerRoleAccessor serverRegistrar, - IHttpClientFactory httpClientFactory) - : base(logger, TimeSpan.FromMinutes(5), DefaultDelay) + keepAliveSettings.OnChange(x => _keepAliveSettings = x); + } + + public override async Task PerformExecuteAsync(object? state) + { + if (_keepAliveSettings.DisableKeepAliveTask) { - _hostingEnvironment = hostingEnvironment; - _mainDom = mainDom; - _keepAliveSettings = keepAliveSettings.CurrentValue; - _logger = logger; - _profilingLogger = profilingLogger; - _serverRegistrar = serverRegistrar; - _httpClientFactory = httpClientFactory; + return; + } + + // Don't run on replicas nor unknown role servers + switch (_serverRegistrar.CurrentServerRole) + { + case ServerRole.Subscriber: + _logger.LogDebug("Does not run on subscriber servers."); + return; + case ServerRole.Unknown: + _logger.LogDebug("Does not run on servers with unknown role."); + return; + } - keepAliveSettings.OnChange(x => _keepAliveSettings = x); + // Ensure we do not run if not main domain, but do NOT lock it + if (_mainDom.IsMainDom == false) + { + _logger.LogDebug("Does not run if not MainDom."); + return; } - public override async Task PerformExecuteAsync(object? state) + using (_profilingLogger.DebugDuration("Keep alive executing", "Keep alive complete")) { - if (_keepAliveSettings.DisableKeepAliveTask) + var umbracoAppUrl = _hostingEnvironment.ApplicationMainUrl?.ToString(); + if (umbracoAppUrl.IsNullOrWhiteSpace()) { + _logger.LogWarning("No umbracoApplicationUrl for service (yet), skip."); return; } - // Don't run on replicas nor unknown role servers - switch (_serverRegistrar.CurrentServerRole) - { - case ServerRole.Subscriber: - _logger.LogDebug("Does not run on subscriber servers."); - return; - case ServerRole.Unknown: - _logger.LogDebug("Does not run on servers with unknown role."); - return; - } + // If the config is an absolute path, just use it + var keepAlivePingUrl = WebPath.Combine(umbracoAppUrl!, + _hostingEnvironment.ToAbsolute(_keepAliveSettings.KeepAlivePingUrl)); - // Ensure we do not run if not main domain, but do NOT lock it - if (_mainDom.IsMainDom == false) + try { - _logger.LogDebug("Does not run if not MainDom."); - return; + var request = new HttpRequestMessage(HttpMethod.Get, keepAlivePingUrl); + HttpClient httpClient = _httpClientFactory.CreateClient(Constants.HttpClients.IgnoreCertificateErrors); + _ = await httpClient.SendAsync(request); } - - using (_profilingLogger.DebugDuration("Keep alive executing", "Keep alive complete")) + catch (Exception ex) { - var umbracoAppUrl = _hostingEnvironment.ApplicationMainUrl?.ToString(); - if (umbracoAppUrl.IsNullOrWhiteSpace()) - { - _logger.LogWarning("No umbracoApplicationUrl for service (yet), skip."); - return; - } - - // If the config is an absolute path, just use it - string keepAlivePingUrl = WebPath.Combine(umbracoAppUrl!, _hostingEnvironment.ToAbsolute(_keepAliveSettings.KeepAlivePingUrl)); - - try - { - var request = new HttpRequestMessage(HttpMethod.Get, keepAlivePingUrl); - HttpClient httpClient = _httpClientFactory.CreateClient(Constants.HttpClients.IgnoreCertificateErrors); - _ = await httpClient.SendAsync(request); - } - catch (Exception ex) - { - _logger.LogError(ex, "Keep alive failed (at '{keepAlivePingUrl}').", keepAlivePingUrl); - } + _logger.LogError(ex, "Keep alive failed (at '{keepAlivePingUrl}').", keepAlivePingUrl); } } } diff --git a/src/Umbraco.Infrastructure/HostedServices/LogScrubber.cs b/src/Umbraco.Infrastructure/HostedServices/LogScrubber.cs index 4877c4cb2567..b69342d25b4e 100644 --- a/src/Umbraco.Infrastructure/HostedServices/LogScrubber.cs +++ b/src/Umbraco.Infrastructure/HostedServices/LogScrubber.cs @@ -1,8 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; @@ -12,82 +10,81 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Infrastructure.HostedServices +namespace Umbraco.Cms.Infrastructure.HostedServices; + +/// +/// Log scrubbing hosted service. +/// +/// +/// Will only run on non-replica servers. +/// +public class LogScrubber : RecurringHostedServiceBase { + private readonly IAuditService _auditService; + private readonly ILogger _logger; + private readonly IMainDom _mainDom; + private readonly IProfilingLogger _profilingLogger; + private readonly ICoreScopeProvider _scopeProvider; + private readonly IServerRoleAccessor _serverRegistrar; + private LoggingSettings _settings; + /// - /// Log scrubbing hosted service. + /// Initializes a new instance of the class. /// - /// - /// Will only run on non-replica servers. - /// - public class LogScrubber : RecurringHostedServiceBase + /// Representation of the main application domain. + /// Provider of server registrations to the distributed cache. + /// Service for handling audit operations. + /// The configuration for logging settings. + /// Provides scopes for database operations. + /// The typed logger. + /// The profiling logger. + public LogScrubber( + IMainDom mainDom, + IServerRoleAccessor serverRegistrar, + IAuditService auditService, + IOptionsMonitor settings, + ICoreScopeProvider scopeProvider, + ILogger logger, + IProfilingLogger profilingLogger) + : base(logger, TimeSpan.FromHours(4), DefaultDelay) { - private readonly IMainDom _mainDom; - private readonly IServerRoleAccessor _serverRegistrar; - private readonly IAuditService _auditService; - private LoggingSettings _settings; - private readonly IProfilingLogger _profilingLogger; - private readonly ILogger _logger; - private readonly ICoreScopeProvider _scopeProvider; + _mainDom = mainDom; + _serverRegistrar = serverRegistrar; + _auditService = auditService; + _settings = settings.CurrentValue; + _scopeProvider = scopeProvider; + _logger = logger; + _profilingLogger = profilingLogger; + settings.OnChange(x => _settings = x); + } - /// - /// Initializes a new instance of the class. - /// - /// Representation of the main application domain. - /// Provider of server registrations to the distributed cache. - /// Service for handling audit operations. - /// The configuration for logging settings. - /// Provides scopes for database operations. - /// The typed logger. - /// The profiling logger. - public LogScrubber( - IMainDom mainDom, - IServerRoleAccessor serverRegistrar, - IAuditService auditService, - IOptionsMonitor settings, - ICoreScopeProvider scopeProvider, - ILogger logger, - IProfilingLogger profilingLogger) - : base(logger, TimeSpan.FromHours(4), DefaultDelay) + public override Task PerformExecuteAsync(object? state) + { + switch (_serverRegistrar.CurrentServerRole) { - _mainDom = mainDom; - _serverRegistrar = serverRegistrar; - _auditService = auditService; - _settings = settings.CurrentValue; - _scopeProvider = scopeProvider; - _logger = logger; - _profilingLogger = profilingLogger; - settings.OnChange(x => _settings = x); + case ServerRole.Subscriber: + _logger.LogDebug("Does not run on subscriber servers."); + return Task.CompletedTask; + case ServerRole.Unknown: + _logger.LogDebug("Does not run on servers with unknown role."); + return Task.CompletedTask; } - public override Task PerformExecuteAsync(object? state) + // Ensure we do not run if not main domain, but do NOT lock it + if (_mainDom.IsMainDom == false) { - switch (_serverRegistrar.CurrentServerRole) - { - case ServerRole.Subscriber: - _logger.LogDebug("Does not run on subscriber servers."); - return Task.CompletedTask; - case ServerRole.Unknown: - _logger.LogDebug("Does not run on servers with unknown role."); - return Task.CompletedTask; - } - - // Ensure we do not run if not main domain, but do NOT lock it - if (_mainDom.IsMainDom == false) - { - _logger.LogDebug("Does not run if not MainDom."); - return Task.CompletedTask; - } - - // Ensure we use an explicit scope since we are running on a background thread. - using (ICoreScope scope = _scopeProvider.CreateCoreScope()) - using (_profilingLogger.DebugDuration("Log scrubbing executing", "Log scrubbing complete")) - { - _auditService.CleanLogs((int)_settings.MaxLogAge.TotalMinutes); - _ = scope.Complete(); - } - + _logger.LogDebug("Does not run if not MainDom."); return Task.CompletedTask; } + + // Ensure we use an explicit scope since we are running on a background thread. + using (ICoreScope scope = _scopeProvider.CreateCoreScope()) + using (_profilingLogger.DebugDuration("Log scrubbing executing", "Log scrubbing complete")) + { + _auditService.CleanLogs((int)_settings.MaxLogAge.TotalMinutes); + _ = scope.Complete(); + } + + return Task.CompletedTask; } } diff --git a/src/Umbraco.Infrastructure/HostedServices/QueuedHostedService.cs b/src/Umbraco.Infrastructure/HostedServices/QueuedHostedService.cs index e271c98324bf..afc08c91ebc0 100644 --- a/src/Umbraco.Infrastructure/HostedServices/QueuedHostedService.cs +++ b/src/Umbraco.Infrastructure/HostedServices/QueuedHostedService.cs @@ -1,62 +1,55 @@ -using System; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace Umbraco.Cms.Infrastructure.HostedServices +namespace Umbraco.Cms.Infrastructure.HostedServices; + +/// +/// A queue based hosted service used to executing tasks on a background thread. +/// +/// +/// Borrowed from https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0 +/// +public class QueuedHostedService : BackgroundService { + private readonly ILogger _logger; - /// - /// A queue based hosted service used to executing tasks on a background thread. - /// - /// - /// Borrowed from https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0 - /// - public class QueuedHostedService : BackgroundService + public QueuedHostedService(IBackgroundTaskQueue taskQueue, + ILogger logger) { - private readonly ILogger _logger; + TaskQueue = taskQueue; + _logger = logger; + } - public QueuedHostedService(IBackgroundTaskQueue taskQueue, - ILogger logger) - { - TaskQueue = taskQueue; - _logger = logger; - } + public IBackgroundTaskQueue TaskQueue { get; } - public IBackgroundTaskQueue TaskQueue { get; } + protected override async Task ExecuteAsync(CancellationToken stoppingToken) => + await BackgroundProcessing(stoppingToken); - protected override async Task ExecuteAsync(CancellationToken stoppingToken) + private async Task BackgroundProcessing(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) { - await BackgroundProcessing(stoppingToken); - } + Func? workItem = await TaskQueue.DequeueAsync(stoppingToken); - private async Task BackgroundProcessing(CancellationToken stoppingToken) - { - while (!stoppingToken.IsCancellationRequested) + try { - Func? workItem = await TaskQueue.DequeueAsync(stoppingToken); - - try + if (workItem is not null) { - if (workItem is not null) - { - await workItem(stoppingToken); - } - } - catch (Exception ex) - { - _logger.LogError(ex, - "Error occurred executing {WorkItem}.", nameof(workItem)); + await workItem(stoppingToken); } } + catch (Exception ex) + { + _logger.LogError(ex, + "Error occurred executing {WorkItem}.", nameof(workItem)); + } } + } - public override async Task StopAsync(CancellationToken stoppingToken) - { - _logger.LogInformation("Queued Hosted Service is stopping."); + public override async Task StopAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("Queued Hosted Service is stopping."); - await base.StopAsync(stoppingToken); - } + await base.StopAsync(stoppingToken); } } diff --git a/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs b/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs index 4888d173d794..544a395fe10c 100644 --- a/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs +++ b/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs @@ -1,128 +1,131 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; -namespace Umbraco.Cms.Infrastructure.HostedServices +namespace Umbraco.Cms.Infrastructure.HostedServices; + +/// +/// Provides a base class for recurring background tasks implemented as hosted services. +/// +/// +/// See: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-3.1&tabs +/// =visual-studio#timed-background-tasks +/// +public abstract class RecurringHostedServiceBase : IHostedService, IDisposable { /// - /// Provides a base class for recurring background tasks implemented as hosted services. + /// The default delay to use for recurring tasks for the first run after application start-up if no alternative is + /// configured. + /// + protected static readonly TimeSpan DefaultDelay = TimeSpan.FromMinutes(3); + + private readonly TimeSpan _delay; + + private readonly ILogger? _logger; + private bool _disposedValue; + private TimeSpan _period; + private Timer? _timer; + + /// + /// Initializes a new instance of the class. /// - /// - /// See: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-3.1&tabs=visual-studio#timed-background-tasks - /// - public abstract class RecurringHostedServiceBase : IHostedService, IDisposable + /// Logger. + /// Timespan representing how often the task should recur. + /// + /// Timespan representing the initial delay after application start-up before the first run of the task + /// occurs. + /// + protected RecurringHostedServiceBase(ILogger? logger, TimeSpan period, TimeSpan delay) + { + _logger = logger; + _period = period; + _delay = delay; + } + + // Scheduled for removal in V11 + [Obsolete("Please use constructor that takes an ILogger instead")] + protected RecurringHostedServiceBase(TimeSpan period, TimeSpan delay) + : this(null, period, delay) + { + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + public Task StartAsync(CancellationToken cancellationToken) { - /// - /// The default delay to use for recurring tasks for the first run after application start-up if no alternative is configured. - /// - protected static readonly TimeSpan DefaultDelay = TimeSpan.FromMinutes(3); - - private readonly ILogger? _logger; - private TimeSpan _period; - private readonly TimeSpan _delay; - private Timer? _timer; - private bool _disposedValue; - - /// - /// Initializes a new instance of the class. - /// - /// Logger. - /// Timespan representing how often the task should recur. - /// Timespan representing the initial delay after application start-up before the first run of the task occurs. - protected RecurringHostedServiceBase(ILogger? logger, TimeSpan period, TimeSpan delay) + using (!ExecutionContext.IsFlowSuppressed() ? (IDisposable)ExecutionContext.SuppressFlow() : null) { - _logger = logger; - _period = period; - _delay = delay; + _timer = new Timer(ExecuteAsync, null, (int)_delay.TotalMilliseconds, (int)_period.TotalMilliseconds); } - // Scheduled for removal in V11 - [Obsolete("Please use constructor that takes an ILogger instead")] - protected RecurringHostedServiceBase(TimeSpan period, TimeSpan delay) - : this(null, period, delay) - { } + return Task.CompletedTask; + } - /// - /// Change the period between operations. - /// - /// The new period between tasks - protected void ChangePeriod(TimeSpan newPeriod) => _period = newPeriod; + /// + public Task StopAsync(CancellationToken cancellationToken) + { + _period = Timeout.InfiniteTimeSpan; + _timer?.Change(Timeout.Infinite, 0); + return Task.CompletedTask; + } - /// - public Task StartAsync(CancellationToken cancellationToken) + /// + /// Change the period between operations. + /// + /// The new period between tasks + protected void ChangePeriod(TimeSpan newPeriod) => _period = newPeriod; + + /// + /// Executes the task. + /// + /// The task state. + public async void ExecuteAsync(object? state) + { + try { - using (!ExecutionContext.IsFlowSuppressed() ? (IDisposable)ExecutionContext.SuppressFlow() : null) - { - _timer = new Timer(ExecuteAsync, null, (int)_delay.TotalMilliseconds, (int)_period.TotalMilliseconds); - } + // First, stop the timer, we do not want tasks to execute in parallel + _timer?.Change(Timeout.Infinite, 0); - return Task.CompletedTask; + // Delegate work to method returning a task, that can be called and asserted in a unit test. + // Without this there can be behaviour where tests pass, but an error within them causes the test + // running process to crash. + // Hat-tip: https://stackoverflow.com/a/14207615/489433 + await PerformExecuteAsync(state); } - - /// - /// Executes the task. - /// - /// The task state. - public async void ExecuteAsync(object? state) + catch (Exception ex) { - try - { - // First, stop the timer, we do not want tasks to execute in parallel - _timer?.Change(Timeout.Infinite, 0); - - // Delegate work to method returning a task, that can be called and asserted in a unit test. - // Without this there can be behaviour where tests pass, but an error within them causes the test - // running process to crash. - // Hat-tip: https://stackoverflow.com/a/14207615/489433 - await PerformExecuteAsync(state); - } - catch (Exception ex) - { - ILogger logger = _logger ?? StaticApplicationLogging.CreateLogger(GetType()); - logger.LogError(ex, "Unhandled exception in recurring hosted service."); - } - finally - { - // Resume now that the task is complete - Note we use period in both because we don't want to execute again after the delay. - // So first execution is after _delay, and the we wait _period between each - _timer?.Change((int)_period.TotalMilliseconds, (int)_period.TotalMilliseconds); - } + ILogger logger = _logger ?? StaticApplicationLogging.CreateLogger(GetType()); + logger.LogError(ex, "Unhandled exception in recurring hosted service."); } - - public abstract Task PerformExecuteAsync(object? state); - - /// - public Task StopAsync(CancellationToken cancellationToken) + finally { - _period = Timeout.InfiniteTimeSpan; - _timer?.Change(Timeout.Infinite, 0); - return Task.CompletedTask; + // Resume now that the task is complete - Note we use period in both because we don't want to execute again after the delay. + // So first execution is after _delay, and the we wait _period between each + _timer?.Change((int)_period.TotalMilliseconds, (int)_period.TotalMilliseconds); } + } - protected virtual void Dispose(bool disposing) + public abstract Task PerformExecuteAsync(object? state); + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) { - if (!_disposedValue) + if (disposing) { - if (disposing) - { - _timer?.Dispose(); - } - - _disposedValue = true; + _timer?.Dispose(); } - } - /// - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); + _disposedValue = true; } } } diff --git a/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs b/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs index 54137fad9906..74871bf33ec6 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs @@ -1,7 +1,4 @@ -using System; -using System.Net.Http; using System.Text; -using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -12,81 +9,81 @@ using Umbraco.Cms.Core.Telemetry.Models; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Infrastructure.HostedServices +namespace Umbraco.Cms.Infrastructure.HostedServices; + +public class ReportSiteTask : RecurringHostedServiceBase { - public class ReportSiteTask : RecurringHostedServiceBase + private static HttpClient s_httpClient = new(); + private readonly ILogger _logger; + private readonly ITelemetryService _telemetryService; + + public ReportSiteTask( + ILogger logger, + ITelemetryService telemetryService) + : base(logger, TimeSpan.FromDays(1), TimeSpan.FromMinutes(1)) { - private readonly ILogger _logger; - private readonly ITelemetryService _telemetryService; - private static HttpClient s_httpClient = new(); + _logger = logger; + _telemetryService = telemetryService; + s_httpClient = new HttpClient(); + } - public ReportSiteTask( - ILogger logger, - ITelemetryService telemetryService) - : base(logger, TimeSpan.FromDays(1), TimeSpan.FromMinutes(1)) - { - _logger = logger; - _telemetryService = telemetryService; - s_httpClient = new HttpClient(); - } + [Obsolete("Use the constructor that takes ITelemetryService instead, scheduled for removal in V11")] + public ReportSiteTask( + ILogger logger, + IUmbracoVersion umbracoVersion, + IOptions globalSettings) + : this(logger, StaticServiceProvider.Instance.GetRequiredService()) + { + } - [Obsolete("Use the constructor that takes ITelemetryService instead, scheduled for removal in V11")] - public ReportSiteTask( - ILogger logger, - IUmbracoVersion umbracoVersion, - IOptions globalSettings) - : this(logger, StaticServiceProvider.Instance.GetRequiredService()) + /// + /// Runs the background task to send the anonymous ID + /// to telemetry service + /// + public override async Task PerformExecuteAsync(object? state) + { + if (_telemetryService.TryGetTelemetryReportData(out TelemetryReportData? telemetryReportData) is false) { + _logger.LogWarning("No telemetry marker found"); + + return; } - /// - /// Runs the background task to send the anonymous ID - /// to telemetry service - /// - public override async Task PerformExecuteAsync(object? state) + try { - if (_telemetryService.TryGetTelemetryReportData(out TelemetryReportData? telemetryReportData) is false) + if (s_httpClient.BaseAddress is null) { - _logger.LogWarning("No telemetry marker found"); - - return; - } - - try - { - if (s_httpClient.BaseAddress is null) - { - // Send data to LIVE telemetry - s_httpClient.BaseAddress = new Uri("https://telemetry.umbraco.com/"); + // Send data to LIVE telemetry + s_httpClient.BaseAddress = new Uri("https://telemetry.umbraco.com/"); #if DEBUG - // Send data to DEBUG telemetry service - s_httpClient.BaseAddress = new Uri("https://telemetry.rainbowsrock.net/"); + // Send data to DEBUG telemetry service + s_httpClient.BaseAddress = new Uri("https://telemetry.rainbowsrock.net/"); #endif - } + } - s_httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json"); + s_httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json"); - using (var request = new HttpRequestMessage(HttpMethod.Post, "installs/")) - { - request.Content = new StringContent(JsonConvert.SerializeObject(telemetryReportData), Encoding.UTF8, "application/json"); //CONTENT-TYPE header + using (var request = new HttpRequestMessage(HttpMethod.Post, "installs/")) + { + request.Content = new StringContent(JsonConvert.SerializeObject(telemetryReportData), Encoding.UTF8, + "application/json"); //CONTENT-TYPE header - // Make a HTTP Post to telemetry service - // https://telemetry.umbraco.com/installs/ - // Fire & Forget, do not need to know if its a 200, 500 etc - using (HttpResponseMessage response = await s_httpClient.SendAsync(request)) - { - } + // Make a HTTP Post to telemetry service + // https://telemetry.umbraco.com/installs/ + // Fire & Forget, do not need to know if its a 200, 500 etc + using (HttpResponseMessage response = await s_httpClient.SendAsync(request)) + { } } - catch - { - // Silently swallow - // The user does not need the logs being polluted if our service has fallen over or is down etc - // Hence only logging this at a more verbose level (which users should not be using in production) - _logger.LogDebug("There was a problem sending a request to the Umbraco telemetry service"); - } + } + catch + { + // Silently swallow + // The user does not need the logs being polluted if our service has fallen over or is down etc + // Hence only logging this at a more verbose level (which users should not be using in production) + _logger.LogDebug("There was a problem sending a request to the Umbraco telemetry service"); } } } diff --git a/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs b/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs index 6d659425e01b..1bf98bd0f272 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs @@ -1,11 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Runtime; @@ -13,129 +8,128 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Core.Web; -using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Infrastructure.HostedServices +namespace Umbraco.Cms.Infrastructure.HostedServices; + +/// +/// Hosted service implementation for scheduled publishing feature. +/// +/// +/// Runs only on non-replica servers. +/// +public class ScheduledPublishing : RecurringHostedServiceBase { + private readonly IContentService _contentService; + private readonly ILogger _logger; + private readonly IMainDom _mainDom; + private readonly IRuntimeState _runtimeState; + private readonly ICoreScopeProvider _scopeProvider; + private readonly IServerMessenger _serverMessenger; + private readonly IServerRoleAccessor _serverRegistrar; + private readonly IUmbracoContextFactory _umbracoContextFactory; + /// - /// Hosted service implementation for scheduled publishing feature. + /// Initializes a new instance of the class. /// - /// - /// Runs only on non-replica servers. - public class ScheduledPublishing : RecurringHostedServiceBase + public ScheduledPublishing( + IRuntimeState runtimeState, + IMainDom mainDom, + IServerRoleAccessor serverRegistrar, + IContentService contentService, + IUmbracoContextFactory umbracoContextFactory, + ILogger logger, + IServerMessenger serverMessenger, + ICoreScopeProvider scopeProvider) + : base(logger, TimeSpan.FromMinutes(1), DefaultDelay) { - private readonly IContentService _contentService; - private readonly ILogger _logger; - private readonly IMainDom _mainDom; - private readonly IRuntimeState _runtimeState; - private readonly IServerMessenger _serverMessenger; - private readonly ICoreScopeProvider _scopeProvider; - private readonly IServerRoleAccessor _serverRegistrar; - private readonly IUmbracoContextFactory _umbracoContextFactory; + _runtimeState = runtimeState; + _mainDom = mainDom; + _serverRegistrar = serverRegistrar; + _contentService = contentService; + _umbracoContextFactory = umbracoContextFactory; + _logger = logger; + _serverMessenger = serverMessenger; + _scopeProvider = scopeProvider; + } - /// - /// Initializes a new instance of the class. - /// - public ScheduledPublishing( - IRuntimeState runtimeState, - IMainDom mainDom, - IServerRoleAccessor serverRegistrar, - IContentService contentService, - IUmbracoContextFactory umbracoContextFactory, - ILogger logger, - IServerMessenger serverMessenger, - ICoreScopeProvider scopeProvider) - : base(logger, TimeSpan.FromMinutes(1), DefaultDelay) + public override Task PerformExecuteAsync(object? state) + { + if (Suspendable.ScheduledPublishing.CanRun == false) { - _runtimeState = runtimeState; - _mainDom = mainDom; - _serverRegistrar = serverRegistrar; - _contentService = contentService; - _umbracoContextFactory = umbracoContextFactory; - _logger = logger; - _serverMessenger = serverMessenger; - _scopeProvider = scopeProvider; + return Task.CompletedTask; } - public override Task PerformExecuteAsync(object? state) + switch (_serverRegistrar.CurrentServerRole) { - if (Suspendable.ScheduledPublishing.CanRun == false) - { + case ServerRole.Subscriber: + _logger.LogDebug("Does not run on subscriber servers."); return Task.CompletedTask; - } - - switch (_serverRegistrar.CurrentServerRole) - { - case ServerRole.Subscriber: - _logger.LogDebug("Does not run on subscriber servers."); - return Task.CompletedTask; - case ServerRole.Unknown: - _logger.LogDebug("Does not run on servers with unknown role."); - return Task.CompletedTask; - } - - // Ensure we do not run if not main domain, but do NOT lock it - if (_mainDom.IsMainDom == false) - { - _logger.LogDebug("Does not run if not MainDom."); + case ServerRole.Unknown: + _logger.LogDebug("Does not run on servers with unknown role."); return Task.CompletedTask; - } + } - // Do NOT run publishing if not properly running - if (_runtimeState.Level != RuntimeLevel.Run) - { - _logger.LogDebug("Does not run if run level is not Run."); - return Task.CompletedTask; - } + // Ensure we do not run if not main domain, but do NOT lock it + if (_mainDom.IsMainDom == false) + { + _logger.LogDebug("Does not run if not MainDom."); + return Task.CompletedTask; + } - try - { - // Ensure we run with an UmbracoContext, because this will run in a background task, - // and developers may be using the UmbracoContext in the event handlers. + // Do NOT run publishing if not properly running + if (_runtimeState.Level != RuntimeLevel.Run) + { + _logger.LogDebug("Does not run if run level is not Run."); + return Task.CompletedTask; + } - // TODO: or maybe not, CacheRefresherComponent already ensures a context when handling events - // - UmbracoContext 'current' needs to be refactored and cleaned up - // - batched messenger should not depend on a current HttpContext - // but then what should be its "scope"? could we attach it to scopes? - // - and we should definitively *not* have to flush it here (should be auto) + try + { + // Ensure we run with an UmbracoContext, because this will run in a background task, + // and developers may be using the UmbracoContext in the event handlers. - using UmbracoContextReference contextReference = _umbracoContextFactory.EnsureUmbracoContext(); - using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + // TODO: or maybe not, CacheRefresherComponent already ensures a context when handling events + // - UmbracoContext 'current' needs to be refactored and cleaned up + // - batched messenger should not depend on a current HttpContext + // but then what should be its "scope"? could we attach it to scopes? + // - and we should definitively *not* have to flush it here (should be auto) - /* We used to assume that there will never be two instances running concurrently where (IsMainDom && ServerRole == SchedulingPublisher) - * However this is possible during an azure deployment slot swap for the SchedulingPublisher instance when trying to achieve zero downtime deployments. - * If we take a distributed write lock, we are certain that the multiple instances of the job will not run in parallel. - * It's possible that during the swapping process we may run this job more frequently than intended but this is not of great concern and it's - * only until the old SchedulingPublisher shuts down. */ - scope.EagerWriteLock(Constants.Locks.ScheduledPublishing); - try - { - // Run - IEnumerable result = _contentService.PerformScheduledPublish(DateTime.Now); - foreach (IGrouping grouped in result.GroupBy(x => x.Result)) - { - _logger.LogInformation( - "Scheduled publishing result: '{StatusCount}' items with status {Status}", - grouped.Count(), - grouped.Key); - } - } - finally + using UmbracoContextReference contextReference = _umbracoContextFactory.EnsureUmbracoContext(); + using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + + /* We used to assume that there will never be two instances running concurrently where (IsMainDom && ServerRole == SchedulingPublisher) + * However this is possible during an azure deployment slot swap for the SchedulingPublisher instance when trying to achieve zero downtime deployments. + * If we take a distributed write lock, we are certain that the multiple instances of the job will not run in parallel. + * It's possible that during the swapping process we may run this job more frequently than intended but this is not of great concern and it's + * only until the old SchedulingPublisher shuts down. */ + scope.EagerWriteLock(Constants.Locks.ScheduledPublishing); + try + { + // Run + IEnumerable result = _contentService.PerformScheduledPublish(DateTime.Now); + foreach (IGrouping grouped in result.GroupBy(x => x.Result)) { - // If running on a temp context, we have to flush the messenger - if (contextReference.IsRoot) - { - _serverMessenger.SendMessages(); - } + _logger.LogInformation( + "Scheduled publishing result: '{StatusCount}' items with status {Status}", + grouped.Count(), + grouped.Key); } } - catch (Exception ex) + finally { - // important to catch *everything* to ensure the task repeats - _logger.LogError(ex, "Failed."); + // If running on a temp context, we have to flush the messenger + if (contextReference.IsRoot) + { + _serverMessenger.SendMessages(); + } } - - return Task.CompletedTask; } + catch (Exception ex) + { + // important to catch *everything* to ensure the task repeats + _logger.LogError(ex, "Failed."); + } + + return Task.CompletedTask; } } diff --git a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs index d15336694972..0de91b7a6c9a 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs @@ -1,8 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -10,65 +8,65 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration +namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration; + +/// +/// Implements periodic database instruction processing as a hosted service. +/// +public class InstructionProcessTask : RecurringHostedServiceBase { + private readonly ILogger _logger; + private readonly IServerMessenger _messenger; + private readonly IRuntimeState _runtimeState; + private bool _disposedValue; + /// - /// Implements periodic database instruction processing as a hosted service. + /// Initializes a new instance of the class. /// - public class InstructionProcessTask : RecurringHostedServiceBase + /// Representation of the state of the Umbraco runtime. + /// Service broadcasting cache notifications to registered servers. + /// The typed logger. + /// The configuration for global settings. + public InstructionProcessTask(IRuntimeState runtimeState, IServerMessenger messenger, + ILogger logger, IOptions globalSettings) + : base(logger, globalSettings.Value.DatabaseServerMessenger.TimeBetweenSyncOperations, TimeSpan.FromMinutes(1)) { - private readonly IRuntimeState _runtimeState; - private readonly IServerMessenger _messenger; - private readonly ILogger _logger; - private bool _disposedValue; + _runtimeState = runtimeState; + _messenger = messenger; + _logger = logger; + } - /// - /// Initializes a new instance of the class. - /// - /// Representation of the state of the Umbraco runtime. - /// Service broadcasting cache notifications to registered servers. - /// The typed logger. - /// The configuration for global settings. - public InstructionProcessTask(IRuntimeState runtimeState, IServerMessenger messenger, ILogger logger, IOptions globalSettings) - : base(logger, globalSettings.Value.DatabaseServerMessenger.TimeBetweenSyncOperations, TimeSpan.FromMinutes(1)) + public override Task PerformExecuteAsync(object? state) + { + if (_runtimeState.Level != RuntimeLevel.Run) { - _runtimeState = runtimeState; - _messenger = messenger; - _logger = logger; + return Task.CompletedTask; } - public override Task PerformExecuteAsync(object? state) + try { - if (_runtimeState.Level != RuntimeLevel.Run) - { - return Task.CompletedTask; - } - - try - { - _messenger.Sync(); - } - catch (Exception e) - { - _logger.LogError(e, "Failed (will repeat)."); - } - - return Task.CompletedTask; + _messenger.Sync(); + } + catch (Exception e) + { + _logger.LogError(e, "Failed (will repeat)."); } - protected override void Dispose(bool disposing) + return Task.CompletedTask; + } + + protected override void Dispose(bool disposing) + { + if (!_disposedValue) { - if (!_disposedValue) + if (disposing && _messenger is IDisposable disposable) { - if (disposing && _messenger is IDisposable disposable) - { - disposable.Dispose(); - } - - _disposedValue = true; + disposable.Dispose(); } - base.Dispose(disposing); + _disposedValue = true; } + + base.Dispose(disposing); } } diff --git a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs index d75532487837..a0f8411d8eac 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs @@ -1,10 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -12,85 +8,86 @@ using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; -using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration +namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration; + +/// +/// Implements periodic server "touching" (to mark as active/deactive) as a hosted service. +/// +public class TouchServerTask : RecurringHostedServiceBase { + private readonly IHostingEnvironment _hostingEnvironment; + private readonly ILogger _logger; + private readonly IRuntimeState _runtimeState; + private readonly IServerRegistrationService _serverRegistrationService; + private readonly IServerRoleAccessor _serverRoleAccessor; + private GlobalSettings _globalSettings; + /// - /// Implements periodic server "touching" (to mark as active/deactive) as a hosted service. + /// Initializes a new instance of the class. /// - public class TouchServerTask : RecurringHostedServiceBase + /// Representation of the state of the Umbraco runtime. + /// Services for server registrations. + /// Accessor for the current request. + /// The typed logger. + /// The configuration for global settings. + public TouchServerTask( + IRuntimeState runtimeState, + IServerRegistrationService serverRegistrationService, + IHostingEnvironment hostingEnvironment, + ILogger logger, + IOptionsMonitor globalSettings, + IServerRoleAccessor serverRoleAccessor) + : base(logger, globalSettings.CurrentValue.DatabaseServerRegistrar.WaitTimeBetweenCalls, + TimeSpan.FromSeconds(15)) { - private readonly IRuntimeState _runtimeState; - private readonly IServerRegistrationService _serverRegistrationService; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly ILogger _logger; - private readonly IServerRoleAccessor _serverRoleAccessor; - private GlobalSettings _globalSettings; + _runtimeState = runtimeState; + _serverRegistrationService = serverRegistrationService ?? + throw new ArgumentNullException(nameof(serverRegistrationService)); + _hostingEnvironment = hostingEnvironment; + _logger = logger; + _globalSettings = globalSettings.CurrentValue; + globalSettings.OnChange(x => + { + _globalSettings = x; + ChangePeriod(x.DatabaseServerRegistrar.WaitTimeBetweenCalls); + }); + _serverRoleAccessor = serverRoleAccessor; + } - /// - /// Initializes a new instance of the class. - /// - /// Representation of the state of the Umbraco runtime. - /// Services for server registrations. - /// Accessor for the current request. - /// The typed logger. - /// The configuration for global settings. - public TouchServerTask( - IRuntimeState runtimeState, - IServerRegistrationService serverRegistrationService, - IHostingEnvironment hostingEnvironment, - ILogger logger, - IOptionsMonitor globalSettings, - IServerRoleAccessor serverRoleAccessor) - : base(logger, globalSettings.CurrentValue.DatabaseServerRegistrar.WaitTimeBetweenCalls, TimeSpan.FromSeconds(15)) + public override Task PerformExecuteAsync(object? state) + { + if (_runtimeState.Level != RuntimeLevel.Run) { - _runtimeState = runtimeState; - _serverRegistrationService = serverRegistrationService ?? throw new ArgumentNullException(nameof(serverRegistrationService)); - _hostingEnvironment = hostingEnvironment; - _logger = logger; - _globalSettings = globalSettings.CurrentValue; - globalSettings.OnChange(x => - { - _globalSettings = x; - ChangePeriod(x.DatabaseServerRegistrar.WaitTimeBetweenCalls); - }); - _serverRoleAccessor = serverRoleAccessor; + return Task.CompletedTask; } - public override Task PerformExecuteAsync(object? state) + // If the IServerRoleAccessor has been changed away from ElectedServerRoleAccessor this task no longer makes sense, + // since all it's used for is to allow the ElectedServerRoleAccessor + // to figure out what role a given server has, so we just stop this task. + if (_serverRoleAccessor is not ElectedServerRoleAccessor) { - if (_runtimeState.Level != RuntimeLevel.Run) - { - return Task.CompletedTask; - } - - // If the IServerRoleAccessor has been changed away from ElectedServerRoleAccessor this task no longer makes sense, - // since all it's used for is to allow the ElectedServerRoleAccessor - // to figure out what role a given server has, so we just stop this task. - if (_serverRoleAccessor is not ElectedServerRoleAccessor) - { - return StopAsync(CancellationToken.None); - } - - var serverAddress = _hostingEnvironment.ApplicationMainUrl?.ToString(); - if (serverAddress.IsNullOrWhiteSpace()) - { - _logger.LogWarning("No umbracoApplicationUrl for service (yet), skip."); - return Task.CompletedTask; - } - - try - { - _serverRegistrationService.TouchServer(serverAddress!, _globalSettings.DatabaseServerRegistrar.StaleServerTimeout); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to update server record in database."); - } + return StopAsync(CancellationToken.None); + } + var serverAddress = _hostingEnvironment.ApplicationMainUrl?.ToString(); + if (serverAddress.IsNullOrWhiteSpace()) + { + _logger.LogWarning("No umbracoApplicationUrl for service (yet), skip."); return Task.CompletedTask; } + + try + { + _serverRegistrationService.TouchServer(serverAddress!, + _globalSettings.DatabaseServerRegistrar.StaleServerTimeout); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to update server record in database."); + } + + return Task.CompletedTask; } } diff --git a/src/Umbraco.Infrastructure/HostedServices/TempFileCleanup.cs b/src/Umbraco.Infrastructure/HostedServices/TempFileCleanup.cs index ec8d48bca3c6..663a89b05a19 100644 --- a/src/Umbraco.Infrastructure/HostedServices/TempFileCleanup.cs +++ b/src/Umbraco.Infrastructure/HostedServices/TempFileCleanup.cs @@ -1,102 +1,99 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.IO; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Runtime; -namespace Umbraco.Cms.Infrastructure.HostedServices +namespace Umbraco.Cms.Infrastructure.HostedServices; + +/// +/// Used to cleanup temporary file locations. +/// +/// +/// Will run on all servers - even though file upload should only be handled on the scheduling publisher, this will +/// ensure that in the case it happens on subscribers that they are cleaned up too. +/// +public class TempFileCleanup : RecurringHostedServiceBase { + private readonly TimeSpan _age = TimeSpan.FromDays(1); + private readonly IIOHelper _ioHelper; + private readonly ILogger _logger; + private readonly IMainDom _mainDom; + + private readonly DirectoryInfo[] _tempFolders; + /// - /// Used to cleanup temporary file locations. + /// Initializes a new instance of the class. /// - /// - /// Will run on all servers - even though file upload should only be handled on the scheduling publisher, this will - /// ensure that in the case it happens on subscribers that they are cleaned up too. - /// - public class TempFileCleanup : RecurringHostedServiceBase + /// Helper service for IO operations. + /// Representation of the main application domain. + /// The typed logger. + public TempFileCleanup(IIOHelper ioHelper, IMainDom mainDom, ILogger logger) + : base(logger, TimeSpan.FromMinutes(60), DefaultDelay) { - private readonly IIOHelper _ioHelper; - private readonly IMainDom _mainDom; - private readonly ILogger _logger; + _ioHelper = ioHelper; + _mainDom = mainDom; + _logger = logger; - private readonly DirectoryInfo[] _tempFolders; - private readonly TimeSpan _age = TimeSpan.FromDays(1); + _tempFolders = _ioHelper.GetTempFolders(); + } - /// - /// Initializes a new instance of the class. - /// - /// Helper service for IO operations. - /// Representation of the main application domain. - /// The typed logger. - public TempFileCleanup(IIOHelper ioHelper, IMainDom mainDom, ILogger logger) - : base(logger, TimeSpan.FromMinutes(60), DefaultDelay) + public override Task PerformExecuteAsync(object? state) + { + // Ensure we do not run if not main domain + if (_mainDom.IsMainDom == false) { - _ioHelper = ioHelper; - _mainDom = mainDom; - _logger = logger; - - _tempFolders = _ioHelper.GetTempFolders(); + _logger.LogDebug("Does not run if not MainDom."); + return Task.CompletedTask; } - public override Task PerformExecuteAsync(object? state) + foreach (DirectoryInfo folder in _tempFolders) { - // Ensure we do not run if not main domain - if (_mainDom.IsMainDom == false) - { - _logger.LogDebug("Does not run if not MainDom."); - return Task.CompletedTask; - } - - foreach (DirectoryInfo folder in _tempFolders) - { - CleanupFolder(folder); - } - - return Task.CompletedTask; + CleanupFolder(folder); } - private void CleanupFolder(DirectoryInfo folder) + return Task.CompletedTask; + } + + private void CleanupFolder(DirectoryInfo folder) + { + CleanFolderResult result = _ioHelper.CleanFolder(folder, _age); + switch (result.Status) { - CleanFolderResult result = _ioHelper.CleanFolder(folder, _age); - switch (result.Status) - { - case CleanFolderResultStatus.FailedAsDoesNotExist: - _logger.LogDebug("The cleanup folder doesn't exist {Folder}", folder.FullName); - break; - case CleanFolderResultStatus.FailedWithException: - foreach (CleanFolderResult.Error error in result.Errors!) - { - _logger.LogError(error.Exception, "Could not delete temp file {FileName}", error.ErroringFile.FullName); - } + case CleanFolderResultStatus.FailedAsDoesNotExist: + _logger.LogDebug("The cleanup folder doesn't exist {Folder}", folder.FullName); + break; + case CleanFolderResultStatus.FailedWithException: + foreach (CleanFolderResult.Error error in result.Errors!) + { + _logger.LogError(error.Exception, "Could not delete temp file {FileName}", + error.ErroringFile.FullName); + } - break; - } + break; + } - folder.Refresh(); // In case it's changed during runtime - if (!folder.Exists) - { - _logger.LogDebug("The cleanup folder doesn't exist {Folder}", folder.FullName); - return; - } + folder.Refresh(); // In case it's changed during runtime + if (!folder.Exists) + { + _logger.LogDebug("The cleanup folder doesn't exist {Folder}", folder.FullName); + return; + } - FileInfo[] files = folder.GetFiles("*.*", SearchOption.AllDirectories); - foreach (FileInfo file in files) + FileInfo[] files = folder.GetFiles("*.*", SearchOption.AllDirectories); + foreach (FileInfo file in files) + { + if (DateTime.UtcNow - file.LastWriteTimeUtc > _age) { - if (DateTime.UtcNow - file.LastWriteTimeUtc > _age) + try + { + file.IsReadOnly = false; + file.Delete(); + } + catch (Exception ex) { - try - { - file.IsReadOnly = false; - file.Delete(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Could not delete temp file {FileName}", file.FullName); - } + _logger.LogError(ex, "Could not delete temp file {FileName}", file.FullName); } } } diff --git a/src/Umbraco.Infrastructure/IPublishedContentQuery.cs b/src/Umbraco.Infrastructure/IPublishedContentQuery.cs index ab71edf650ea..56333c8e7403 100644 --- a/src/Umbraco.Infrastructure/IPublishedContentQuery.cs +++ b/src/Umbraco.Infrastructure/IPublishedContentQuery.cs @@ -1,116 +1,127 @@ -using System; -using System.Collections.Generic; using System.Xml.XPath; using Examine.Search; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Xml; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Query methods used for accessing strongly typed content in templates. +/// +public interface IPublishedContentQuery { + IPublishedContent? Content(int id); + + IPublishedContent? Content(Guid id); + + IPublishedContent? Content(Udi id); + + IPublishedContent? Content(object id); + + IPublishedContent? ContentSingleAtXPath(string xpath, params XPathVariable[] vars); + + IEnumerable Content(IEnumerable ids); + + IEnumerable Content(IEnumerable ids); + + IEnumerable Content(IEnumerable ids); + + IEnumerable ContentAtXPath(string xpath, params XPathVariable[] vars); + + IEnumerable ContentAtXPath(XPathExpression xpath, params XPathVariable[] vars); + + IEnumerable ContentAtRoot(); + + IPublishedContent? Media(int id); + + IPublishedContent? Media(Guid id); + + IPublishedContent? Media(Udi id); + + IPublishedContent? Media(object id); + + IEnumerable Media(IEnumerable ids); + + IEnumerable Media(IEnumerable ids); + + IEnumerable Media(IEnumerable ids); + + IEnumerable MediaAtRoot(); + + /// + /// Searches content. + /// + /// The term to search. + /// The amount of results to skip. + /// The amount of results to take/return. + /// The total amount of records. + /// The culture (defaults to a culture insensitive search). + /// + /// The name of the index to search (defaults to + /// ). + /// + /// + /// This parameter is no longer used, because the results are loaded from the published snapshot + /// using the single item ID field. + /// + /// + /// The search results. + /// + /// + /// + /// When the is not specified or is *, all cultures are searched. + /// To search for only invariant documents and fields use null. + /// When searching on a specific culture, all culture specific fields are searched for the provided culture and all + /// invariant fields for all documents. + /// + /// While enumerating results, the ambient culture is changed to be the searched culture. + /// + IEnumerable Search(string term, int skip, int take, out long totalRecords, + string culture = "*", string indexName = Constants.UmbracoIndexes.ExternalIndexName, + ISet? loadedFields = null); + + /// + /// Searches content. + /// + /// The term to search. + /// The culture (defaults to a culture insensitive search). + /// + /// The name of the index to search (defaults to + /// ). + /// + /// + /// The search results. + /// + /// + /// + /// When the is not specified or is *, all cultures are searched. + /// To search for only invariant documents and fields use null. + /// When searching on a specific culture, all culture specific fields are searched for the provided culture and all + /// invariant fields for all documents. + /// + /// While enumerating results, the ambient culture is changed to be the searched culture. + /// + IEnumerable Search(string term, string culture = "*", + string indexName = Constants.UmbracoIndexes.ExternalIndexName); + + /// + /// Executes the query and converts the results to . + /// + /// The query. + /// + /// The search results. + /// + IEnumerable Search(IQueryExecutor query); + /// - /// Query methods used for accessing strongly typed content in templates. + /// Executes the query and converts the results to . /// - public interface IPublishedContentQuery - { - IPublishedContent? Content(int id); - - IPublishedContent? Content(Guid id); - - IPublishedContent? Content(Udi id); - - IPublishedContent? Content(object id); - - IPublishedContent? ContentSingleAtXPath(string xpath, params XPathVariable[] vars); - - IEnumerable Content(IEnumerable ids); - - IEnumerable Content(IEnumerable ids); - - IEnumerable Content(IEnumerable ids); - - IEnumerable ContentAtXPath(string xpath, params XPathVariable[] vars); - - IEnumerable ContentAtXPath(XPathExpression xpath, params XPathVariable[] vars); - - IEnumerable ContentAtRoot(); - - IPublishedContent? Media(int id); - - IPublishedContent? Media(Guid id); - - IPublishedContent? Media(Udi id); - - IPublishedContent? Media(object id); - - IEnumerable Media(IEnumerable ids); - - IEnumerable Media(IEnumerable ids); - - IEnumerable Media(IEnumerable ids); - - IEnumerable MediaAtRoot(); - - /// - /// Searches content. - /// - /// The term to search. - /// The amount of results to skip. - /// The amount of results to take/return. - /// The total amount of records. - /// The culture (defaults to a culture insensitive search). - /// The name of the index to search (defaults to ). - /// This parameter is no longer used, because the results are loaded from the published snapshot using the single item ID field. - /// - /// The search results. - /// - /// - /// - /// When the is not specified or is *, all cultures are searched. - /// To search for only invariant documents and fields use null. - /// When searching on a specific culture, all culture specific fields are searched for the provided culture and all invariant fields for all documents. - /// - /// While enumerating results, the ambient culture is changed to be the searched culture. - /// - IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = Constants.UmbracoIndexes.ExternalIndexName, ISet? loadedFields = null); - - /// - /// Searches content. - /// - /// The term to search. - /// The culture (defaults to a culture insensitive search). - /// The name of the index to search (defaults to ). - /// - /// The search results. - /// - /// - /// - /// When the is not specified or is *, all cultures are searched. - /// To search for only invariant documents and fields use null. - /// When searching on a specific culture, all culture specific fields are searched for the provided culture and all invariant fields for all documents. - /// - /// While enumerating results, the ambient culture is changed to be the searched culture. - /// - IEnumerable Search(string term, string culture = "*", string indexName = Constants.UmbracoIndexes.ExternalIndexName); - - /// - /// Executes the query and converts the results to . - /// - /// The query. - /// - /// The search results. - /// - IEnumerable Search(IQueryExecutor query); - - /// - /// Executes the query and converts the results to . - /// - /// The query. - /// The amount of results to skip. - /// The amount of results to take/return. - /// The total amount of records. - /// - /// The search results. - /// - IEnumerable Search(IQueryExecutor query, int skip, int take, out long totalRecords); - } + /// The query. + /// The amount of results to skip. + /// The amount of results to take/return. + /// The total amount of records. + /// + /// The search results. + /// + IEnumerable Search(IQueryExecutor query, int skip, int take, out long totalRecords); } diff --git a/src/Umbraco.Infrastructure/IPublishedContentQueryAccessor.cs b/src/Umbraco.Infrastructure/IPublishedContentQueryAccessor.cs index 01aea4c48f08..9e964663779e 100644 --- a/src/Umbraco.Infrastructure/IPublishedContentQueryAccessor.cs +++ b/src/Umbraco.Infrastructure/IPublishedContentQueryAccessor.cs @@ -1,22 +1,23 @@ using System.Diagnostics.CodeAnalysis; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Not intended for use in background threads where you should make use of +/// +/// and instead resolve IPublishedContentQuery from a +/// +/// e.g. using +/// +/// +/// // Background thread example +/// using UmbracoContextReference _ = _umbracoContextFactory.EnsureUmbracoContext(); +/// using IServiceScope serviceScope = _serviceProvider.CreateScope(); +/// IPublishedContentQuery query = serviceScope.ServiceProvider.GetRequiredService<IPublishedContentQuery>(); +/// +/// +/// +public interface IPublishedContentQueryAccessor { - /// - /// Not intended for use in background threads where you should make use of - /// and instead resolve IPublishedContentQuery from a - /// e.g. using - /// - /// - /// // Background thread example - /// using UmbracoContextReference _ = _umbracoContextFactory.EnsureUmbracoContext(); - /// using IServiceScope serviceScope = _serviceProvider.CreateScope(); - /// IPublishedContentQuery query = serviceScope.ServiceProvider.GetRequiredService<IPublishedContentQuery>(); - /// - /// - /// - public interface IPublishedContentQueryAccessor - { - bool TryGetValue([MaybeNullWhen(false)]out IPublishedContentQuery publishedContentQuery); - } + bool TryGetValue([MaybeNullWhen(false)] out IPublishedContentQuery publishedContentQuery); } diff --git a/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs b/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs index 032459513303..4416d1a4570b 100644 --- a/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs +++ b/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs @@ -1,11 +1,8 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Security.AccessControl; +using System.Security.Principal; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; @@ -14,252 +11,256 @@ using Umbraco.Cms.Core.IO; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Install +namespace Umbraco.Cms.Infrastructure.Install; + +/// +public class FilePermissionHelper : IFilePermissionHelper { - /// - public class FilePermissionHelper : IFilePermissionHelper + private readonly GlobalSettings _globalSettings; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IIOHelper _ioHelper; + + private readonly string[] _packagesPermissionsDirs; + + // ensure that these directories exist and Umbraco can write to them + private readonly string[] _permissionDirs; + + // ensure Umbraco can write to these files (the directories must exist) + private readonly string[] _permissionFiles = Array.Empty(); + private readonly string _basePath; + + /// + /// Initializes a new instance of the class. + /// + public FilePermissionHelper(IOptions globalSettings, IIOHelper ioHelper, + IHostingEnvironment hostingEnvironment) { - // ensure that these directories exist and Umbraco can write to them - private readonly string[] _permissionDirs; - private readonly string[] _packagesPermissionsDirs; - - // ensure Umbraco can write to these files (the directories must exist) - private readonly string[] _permissionFiles = Array.Empty(); - private readonly GlobalSettings _globalSettings; - private readonly IIOHelper _ioHelper; - private readonly IHostingEnvironment _hostingEnvironment; - private string _basePath; - - /// - /// Initializes a new instance of the class. - /// - public FilePermissionHelper(IOptions globalSettings, IIOHelper ioHelper, IHostingEnvironment hostingEnvironment) + _globalSettings = globalSettings.Value; + _ioHelper = ioHelper; + _hostingEnvironment = hostingEnvironment; + _basePath = hostingEnvironment.MapPathContentRoot("/"); + _permissionDirs = new[] { - _globalSettings = globalSettings.Value; - _ioHelper = ioHelper; - _hostingEnvironment = hostingEnvironment; - _basePath = hostingEnvironment.MapPathContentRoot("/"); - _permissionDirs = new[] - { - hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoCssPath), - hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Config), - hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data), - hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPhysicalRootPath), - hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Preview) - }; - _packagesPermissionsDirs = new[] - { - hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Bin), - hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Umbraco), - hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoPath), - hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Packages) - }; - } - - /// - public bool RunFilePermissionTestSuite(out Dictionary> report) + hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoCssPath), + hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Config), + hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data), + hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPhysicalRootPath), + hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Preview) + }; + _packagesPermissionsDirs = new[] { - report = new Dictionary>(); + hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Bin), + hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Umbraco), + hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoPath), + hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Packages) + }; + } + + /// + public bool RunFilePermissionTestSuite(out Dictionary> report) + { + report = new Dictionary>(); - EnsureDirectories(_permissionDirs, out IEnumerable errors); - report[FilePermissionTest.FolderCreation] = errors.ToList(); + EnsureDirectories(_permissionDirs, out IEnumerable errors); + report[FilePermissionTest.FolderCreation] = errors.ToList(); - EnsureDirectories(_packagesPermissionsDirs, out errors); - report[FilePermissionTest.FileWritingForPackages] = errors.ToList(); + EnsureDirectories(_packagesPermissionsDirs, out errors); + report[FilePermissionTest.FileWritingForPackages] = errors.ToList(); - EnsureFiles(_permissionFiles, out errors); - report[FilePermissionTest.FileWriting] = errors.ToList(); + EnsureFiles(_permissionFiles, out errors); + report[FilePermissionTest.FileWriting] = errors.ToList(); - EnsureCanCreateSubDirectory(_hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPhysicalRootPath), out errors); - report[FilePermissionTest.MediaFolderCreation] = errors.ToList(); + EnsureCanCreateSubDirectory(_hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPhysicalRootPath), + out errors); + report[FilePermissionTest.MediaFolderCreation] = errors.ToList(); - return report.Sum(x => x.Value.Count()) == 0; - } + return report.Sum(x => x.Value.Count()) == 0; + } - private bool EnsureDirectories(string[] dirs, out IEnumerable errors, bool writeCausesRestart = false) + private bool EnsureDirectories(string[] dirs, out IEnumerable errors, bool writeCausesRestart = false) + { + List? temp = null; + var success = true; + foreach (var dir in dirs) { - List? temp = null; - var success = true; - foreach (var dir in dirs) + // we don't want to create/ship unnecessary directories, so + // here we just ensure we can access the directory, not create it + var tryAccess = TryAccessDirectory(dir, !writeCausesRestart); + if (tryAccess) { - // we don't want to create/ship unnecessary directories, so - // here we just ensure we can access the directory, not create it - var tryAccess = TryAccessDirectory(dir, !writeCausesRestart); - if (tryAccess) - { - continue; - } - - if (temp == null) - { - temp = new List(); - } - - temp.Add(dir.TrimStart(_basePath)); - success = false; + continue; } - errors = success ? Enumerable.Empty() : temp ?? Enumerable.Empty(); - return success; - } - - private bool EnsureFiles(string[] files, out IEnumerable errors) - { - List? temp = null; - var success = true; - foreach (var file in files) + if (temp == null) { - var canWrite = TryWriteFile(file); - if (canWrite) - { - continue; - } - - if (temp == null) - { - temp = new List(); - } - - temp.Add(file.TrimStart(_basePath)); - success = false; + temp = new List(); } - errors = success ? Enumerable.Empty() : temp ?? Enumerable.Empty(); - return success; + temp.Add(dir.TrimStart(_basePath)); + success = false; } - private bool EnsureCanCreateSubDirectory(string dir, out IEnumerable errors) - => EnsureCanCreateSubDirectories(new[] { dir }, out errors); + errors = success ? Enumerable.Empty() : temp ?? Enumerable.Empty(); + return success; + } - private bool EnsureCanCreateSubDirectories(IEnumerable dirs, out IEnumerable errors) + private bool EnsureFiles(string[] files, out IEnumerable errors) + { + List? temp = null; + var success = true; + foreach (var file in files) { - List? temp = null; - var success = true; - foreach (var dir in dirs) + var canWrite = TryWriteFile(file); + if (canWrite) { - var canCreate = TryCreateSubDirectory(dir); - if (canCreate) - { - continue; - } - - if (temp == null) - { - temp = new List(); - } - - temp.Add(dir); - success = false; + continue; } - errors = success ? Enumerable.Empty() : temp ?? Enumerable.Empty(); - return success; + if (temp == null) + { + temp = new List(); + } + + temp.Add(file.TrimStart(_basePath)); + success = false; } - // tries to create a sub-directory - // if successful, the sub-directory is deleted - // creates the directory if needed - does not delete it - private bool TryCreateSubDirectory(string dir) + errors = success ? Enumerable.Empty() : temp ?? Enumerable.Empty(); + return success; + } + + private bool EnsureCanCreateSubDirectory(string dir, out IEnumerable errors) + => EnsureCanCreateSubDirectories(new[] {dir}, out errors); + + private bool EnsureCanCreateSubDirectories(IEnumerable dirs, out IEnumerable errors) + { + List? temp = null; + var success = true; + foreach (var dir in dirs) { - try + var canCreate = TryCreateSubDirectory(dir); + if (canCreate) { - var path = Path.Combine(dir, _ioHelper.CreateRandomFileName()); - Directory.CreateDirectory(path); - Directory.Delete(path); - return true; + continue; } - catch + + if (temp == null) { - return false; + temp = new List(); } + + temp.Add(dir); + success = false; + } + + errors = success ? Enumerable.Empty() : temp ?? Enumerable.Empty(); + return success; + } + + // tries to create a sub-directory + // if successful, the sub-directory is deleted + // creates the directory if needed - does not delete it + private bool TryCreateSubDirectory(string dir) + { + try + { + var path = Path.Combine(dir, _ioHelper.CreateRandomFileName()); + Directory.CreateDirectory(path); + Directory.Delete(path); + return true; } + catch + { + return false; + } + } - // tries to create a file - // if successful, the file is deleted - // - // or - // - // use the ACL APIs to avoid creating files - // - // if the directory does not exist, do nothing & success - private bool TryAccessDirectory(string dirPath, bool canWrite) + // tries to create a file + // if successful, the file is deleted + // + // or + // + // use the ACL APIs to avoid creating files + // + // if the directory does not exist, do nothing & success + private bool TryAccessDirectory(string dirPath, bool canWrite) + { + try { - try + if (Directory.Exists(dirPath) == false) { - if (Directory.Exists(dirPath) == false) - { - return true; - } - - if (canWrite) - { - var filePath = dirPath + "/" + _ioHelper.CreateRandomFileName() + ".tmp"; - File.WriteAllText(filePath, "This is an Umbraco internal test file. It is safe to delete it."); - File.Delete(filePath); - return true; - } - - return HasWritePermission(dirPath); + return true; } - catch + + if (canWrite) { - return false; + var filePath = dirPath + "/" + _ioHelper.CreateRandomFileName() + ".tmp"; + File.WriteAllText(filePath, "This is an Umbraco internal test file. It is safe to delete it."); + File.Delete(filePath); + return true; } + + return HasWritePermission(dirPath); + } + catch + { + return false; } + } + + private bool HasWritePermission(string path) + { + var writeAllow = false; + var writeDeny = false; + var accessControlList = new DirectorySecurity(path, + AccessControlSections.Access | AccessControlSections.Owner | AccessControlSections.Group); - private bool HasWritePermission(string path) + AuthorizationRuleCollection accessRules; + try { - var writeAllow = false; - var writeDeny = false; - var accessControlList = new DirectorySecurity(path, AccessControlSections.Access | AccessControlSections.Owner | AccessControlSections.Group); + accessRules = accessControlList.GetAccessRules(true, true, typeof(SecurityIdentifier)); + } + catch (Exception) + { + // This is not 100% accurate because it could turn out that the current user doesn't + // have access to read the current permissions but does have write access. + // I think this is an edge case however + return false; + } - AuthorizationRuleCollection accessRules; - try + foreach (FileSystemAccessRule rule in accessRules) + { + if ((FileSystemRights.Write & rule.FileSystemRights) != FileSystemRights.Write) { - accessRules = accessControlList.GetAccessRules(true, true, typeof(System.Security.Principal.SecurityIdentifier)); + continue; } - catch (Exception) + + if (rule.AccessControlType == AccessControlType.Allow) { - // This is not 100% accurate because it could turn out that the current user doesn't - // have access to read the current permissions but does have write access. - // I think this is an edge case however - return false; + writeAllow = true; } - - foreach (FileSystemAccessRule rule in accessRules) + else if (rule.AccessControlType == AccessControlType.Deny) { - if ((FileSystemRights.Write & rule.FileSystemRights) != FileSystemRights.Write) - { - continue; - } - - if (rule.AccessControlType == AccessControlType.Allow) - { - writeAllow = true; - } - else if (rule.AccessControlType == AccessControlType.Deny) - { - writeDeny = true; - } + writeDeny = true; } - - return writeAllow && writeDeny == false; } - // tries to write into a file - // fails if the directory does not exist - private bool TryWriteFile(string file) + return writeAllow && writeDeny == false; + } + + // tries to write into a file + // fails if the directory does not exist + private bool TryWriteFile(string file) + { + try { - try - { - var path = file; - File.AppendText(path).Close(); - return true; - } - catch - { - return false; - } + var path = file; + File.AppendText(path).Close(); + return true; + } + catch + { + return false; } } } diff --git a/src/Umbraco.Infrastructure/Install/InstallHelper.cs b/src/Umbraco.Infrastructure/Install/InstallHelper.cs index 671dc85c4fa6..f81922d22b89 100644 --- a/src/Umbraco.Infrastructure/Install/InstallHelper.cs +++ b/src/Umbraco.Infrastructure/Install/InstallHelper.cs @@ -1,5 +1,3 @@ -using System; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -12,93 +10,93 @@ using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Extensions; -using Constants = Umbraco.Cms.Core.Constants; -namespace Umbraco.Cms.Infrastructure.Install +namespace Umbraco.Cms.Infrastructure.Install; + +public sealed class InstallHelper { - public sealed class InstallHelper + private readonly IOptionsMonitor _connectionStrings; + private readonly ICookieManager _cookieManager; + private readonly DatabaseBuilder _databaseBuilder; + private readonly IInstallationService _installationService; + private readonly ILogger _logger; + private readonly IUmbracoDatabaseFactory _umbracoDatabaseFactory; + private readonly IUmbracoVersion _umbracoVersion; + private readonly IUserAgentProvider _userAgentProvider; + private InstallationType? _installationType; + + public InstallHelper(DatabaseBuilder databaseBuilder, + ILogger logger, + IUmbracoVersion umbracoVersion, + IOptionsMonitor connectionStrings, + IInstallationService installationService, + ICookieManager cookieManager, + IUserAgentProvider userAgentProvider, + IUmbracoDatabaseFactory umbracoDatabaseFactory) { - private readonly DatabaseBuilder _databaseBuilder; - private readonly ILogger _logger; - private readonly IUmbracoVersion _umbracoVersion; - private readonly IOptionsMonitor _connectionStrings; - private readonly IInstallationService _installationService; - private readonly ICookieManager _cookieManager; - private readonly IUserAgentProvider _userAgentProvider; - private readonly IUmbracoDatabaseFactory _umbracoDatabaseFactory; - private InstallationType? _installationType; + _logger = logger; + _umbracoVersion = umbracoVersion; + _databaseBuilder = databaseBuilder; + _connectionStrings = connectionStrings; + _installationService = installationService; + _cookieManager = cookieManager; + _userAgentProvider = userAgentProvider; + _umbracoDatabaseFactory = umbracoDatabaseFactory; - public InstallHelper(DatabaseBuilder databaseBuilder, - ILogger logger, - IUmbracoVersion umbracoVersion, - IOptionsMonitor connectionStrings, - IInstallationService installationService, - ICookieManager cookieManager, - IUserAgentProvider userAgentProvider, - IUmbracoDatabaseFactory umbracoDatabaseFactory) - { - _logger = logger; - _umbracoVersion = umbracoVersion; - _databaseBuilder = databaseBuilder; - _connectionStrings = connectionStrings; - _installationService = installationService; - _cookieManager = cookieManager; - _userAgentProvider = userAgentProvider; - _umbracoDatabaseFactory = umbracoDatabaseFactory; + // We need to initialize the type already, as we can't detect later, if the connection string is added on the fly. + GetInstallationType(); + } - // We need to initialize the type already, as we can't detect later, if the connection string is added on the fly. - GetInstallationType(); - } + /// + /// Checks if this is a brand new install, meaning that there is no configured database connection or the database is + /// empty. + /// + /// + /// true if this is a brand new install; otherwise, false. + /// + private bool IsBrandNewInstall => + _connectionStrings.Get(Constants.System.UmbracoConnectionName).IsConnectionStringConfigured() == false || + _databaseBuilder.IsDatabaseConfigured == false || + _databaseBuilder.CanConnectToDatabase == false || + _databaseBuilder.IsUmbracoInstalled() == false; - public InstallationType GetInstallationType() => _installationType ??= IsBrandNewInstall ? InstallationType.NewInstall : InstallationType.Upgrade; + public InstallationType GetInstallationType() => _installationType ??= + IsBrandNewInstall ? InstallationType.NewInstall : InstallationType.Upgrade; - public async Task SetInstallStatusAsync(bool isCompleted, string errorMsg) + public async Task SetInstallStatusAsync(bool isCompleted, string errorMsg) + { + try { - try - { - var userAgent = _userAgentProvider.GetUserAgent(); - - // Check for current install ID - var installCookie = _cookieManager.GetCookieValue(Constants.Web.InstallerCookieName); - if (!Guid.TryParse(installCookie, out var installId)) - { - installId = Guid.NewGuid(); - - _cookieManager.SetCookieValue(Constants.Web.InstallerCookieName, installId.ToString()); - } + var userAgent = _userAgentProvider.GetUserAgent(); - var dbProvider = string.Empty; - if (IsBrandNewInstall == false) - { - // we don't have DatabaseProvider anymore... doing it differently - //dbProvider = ApplicationContext.Current.DatabaseContext.DatabaseProvider.ToString(); - dbProvider = _umbracoDatabaseFactory.SqlContext.SqlSyntax.DbProvider; - } - - var installLog = new InstallLog(installId: installId, isUpgrade: IsBrandNewInstall == false, - installCompleted: isCompleted, timestamp: DateTime.Now, versionMajor: _umbracoVersion.Version.Major, - versionMinor: _umbracoVersion.Version.Minor, versionPatch: _umbracoVersion.Version.Build, - versionComment: _umbracoVersion.Comment, error: errorMsg, userAgent: userAgent, - dbProvider: dbProvider); + // Check for current install ID + var installCookie = _cookieManager.GetCookieValue(Constants.Web.InstallerCookieName); + if (!Guid.TryParse(installCookie, out Guid installId)) + { + installId = Guid.NewGuid(); - await _installationService.LogInstall(installLog); + _cookieManager.SetCookieValue(Constants.Web.InstallerCookieName, installId.ToString()); } - catch (Exception ex) + + var dbProvider = string.Empty; + if (IsBrandNewInstall == false) { - _logger.LogError(ex, "An error occurred in InstallStatus trying to check upgrades"); + // we don't have DatabaseProvider anymore... doing it differently + //dbProvider = ApplicationContext.Current.DatabaseContext.DatabaseProvider.ToString(); + dbProvider = _umbracoDatabaseFactory.SqlContext.SqlSyntax.DbProvider; } - } - /// - /// Checks if this is a brand new install, meaning that there is no configured database connection or the database is empty. - /// - /// - /// true if this is a brand new install; otherwise, false. - /// - private bool IsBrandNewInstall => - _connectionStrings.Get(Constants.System.UmbracoConnectionName).IsConnectionStringConfigured() == false || - _databaseBuilder.IsDatabaseConfigured == false || - _databaseBuilder.CanConnectToDatabase == false || - _databaseBuilder.IsUmbracoInstalled() == false; + var installLog = new InstallLog(installId, IsBrandNewInstall == false, + isCompleted, DateTime.Now, _umbracoVersion.Version.Major, + _umbracoVersion.Version.Minor, _umbracoVersion.Version.Build, + _umbracoVersion.Comment, errorMsg, userAgent, + dbProvider); + + await _installationService.LogInstall(installLog); + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred in InstallStatus trying to check upgrades"); + } } } diff --git a/src/Umbraco.Infrastructure/Install/InstallStepCollection.cs b/src/Umbraco.Infrastructure/Install/InstallStepCollection.cs index 2a9c30334908..08eadd5d0530 100644 --- a/src/Umbraco.Infrastructure/Install/InstallStepCollection.cs +++ b/src/Umbraco.Infrastructure/Install/InstallStepCollection.cs @@ -1,61 +1,50 @@ -using System.Collections.Generic; -using System.Linq; -using Umbraco.Cms.Core.Install.InstallSteps; +using Umbraco.Cms.Core.Install.InstallSteps; using Umbraco.Cms.Core.Install.Models; using Umbraco.Cms.Infrastructure.Install.InstallSteps; -namespace Umbraco.Cms.Infrastructure.Install +namespace Umbraco.Cms.Infrastructure.Install; + +public sealed class InstallStepCollection { - public sealed class InstallStepCollection + private readonly InstallHelper _installHelper; + private readonly IEnumerable _orderedInstallerSteps; + + public InstallStepCollection(InstallHelper installHelper, IEnumerable installerSteps) { - private readonly InstallHelper _installHelper; - private readonly IEnumerable _orderedInstallerSteps; + _installHelper = installHelper; - public InstallStepCollection(InstallHelper installHelper, IEnumerable installerSteps) - { - _installHelper = installHelper; - - // TODO: this is ugly but I have a branch where it's nicely refactored - for now we just want to manage ordering - var a = installerSteps.ToArray(); - _orderedInstallerSteps = new InstallSetupStep[] - { - a.OfType().First(), - a.OfType().First(), - a.OfType().First(), - a.OfType().First(), - a.OfType().First(), - a.OfType().First(), - a.OfType().First(), - - // TODO: Add these back once we have a compatible Starter kit - // a.OfType().First(), - // a.OfType().First(), - // a.OfType().First(), - - a.OfType().First(), - }; - } - - - /// - /// Get the installer steps - /// - /// - /// - /// The step order returned here is how they will appear on the front-end if they have views assigned - /// - public IEnumerable GetAllSteps() + // TODO: this is ugly but I have a branch where it's nicely refactored - for now we just want to manage ordering + InstallSetupStep[] a = installerSteps.ToArray(); + _orderedInstallerSteps = new InstallSetupStep[] { - return _orderedInstallerSteps; - } - - /// - /// Returns the steps that are used only for the current installation type - /// - /// - public IEnumerable GetStepsForCurrentInstallType() - { - return GetAllSteps().Where(x => x.InstallTypeTarget.HasFlag(_installHelper.GetInstallationType())); - } + a.OfType().First(), a.OfType().First(), + a.OfType().First(), a.OfType().First(), + a.OfType().First(), a.OfType().First(), + a.OfType().First(), + + // TODO: Add these back once we have a compatible Starter kit + // a.OfType().First(), + // a.OfType().First(), + // a.OfType().First(), + + a.OfType().First() + }; } + + + /// + /// Get the installer steps + /// + /// + /// + /// The step order returned here is how they will appear on the front-end if they have views assigned + /// + public IEnumerable GetAllSteps() => _orderedInstallerSteps; + + /// + /// Returns the steps that are used only for the current installation type + /// + /// + public IEnumerable GetStepsForCurrentInstallType() => GetAllSteps() + .Where(x => x.InstallTypeTarget.HasFlag(_installHelper.GetInstallationType())); } diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/CompleteInstallStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/CompleteInstallStep.cs index 0666a3eee559..00ec5b4e7dc2 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/CompleteInstallStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/CompleteInstallStep.cs @@ -1,31 +1,23 @@ -using System.Threading.Tasks; -using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Core.Install.Models; -namespace Umbraco.Cms.Infrastructure.Install.InstallSteps -{ - [InstallSetupStep(InstallationType.NewInstall | InstallationType.Upgrade, - "UmbracoVersion", 50, "Installation is complete! Get ready to be redirected to your new CMS.", - PerformsAppRestart = true)] - public class CompleteInstallStep : InstallSetupStep - { - private readonly InstallHelper _installHelper; +namespace Umbraco.Cms.Infrastructure.Install.InstallSteps; - public CompleteInstallStep(InstallHelper installHelper) - { - _installHelper = installHelper; - } +[InstallSetupStep(InstallationType.NewInstall | InstallationType.Upgrade, + "UmbracoVersion", 50, "Installation is complete! Get ready to be redirected to your new CMS.", + PerformsAppRestart = true)] +public class CompleteInstallStep : InstallSetupStep +{ + private readonly InstallHelper _installHelper; - public override async Task ExecuteAsync(object model) - { - //reports the ended install - await _installHelper.SetInstallStatusAsync(true, ""); + public CompleteInstallStep(InstallHelper installHelper) => _installHelper = installHelper; - return null; - } + public override async Task ExecuteAsync(object model) + { + //reports the ended install + await _installHelper.SetInstallStatusAsync(true, ""); - public override bool RequiresExecution(object model) - { - return true; - } + return null; } + + public override bool RequiresExecution(object model) => true; } diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs index 44879f31b85e..0f29118f2a1e 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs @@ -1,9 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Install; using Umbraco.Cms.Core.Install.Models; @@ -11,83 +8,79 @@ using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Install.InstallSteps +namespace Umbraco.Cms.Infrastructure.Install.InstallSteps; + +[InstallSetupStep(InstallationType.NewInstall, + "DatabaseConfigure", "database", 10, "Setting up a database, so Umbraco has a place to store your website", + PerformsAppRestart = true)] +public class DatabaseConfigureStep : InstallSetupStep { - [InstallSetupStep(InstallationType.NewInstall, - "DatabaseConfigure", "database", 10, "Setting up a database, so Umbraco has a place to store your website", - PerformsAppRestart = true)] - public class DatabaseConfigureStep : InstallSetupStep - { - private readonly DatabaseBuilder _databaseBuilder; - private readonly ILogger _logger; - private readonly IEnumerable _databaseProviderMetadata; - private readonly IOptionsMonitor _connectionStrings; + private readonly IOptionsMonitor _connectionStrings; + private readonly DatabaseBuilder _databaseBuilder; + private readonly IEnumerable _databaseProviderMetadata; + private readonly ILogger _logger; - public DatabaseConfigureStep( - DatabaseBuilder databaseBuilder, - IOptionsMonitor connectionStrings, - ILogger logger, - IEnumerable databaseProviderMetadata) - { - _databaseBuilder = databaseBuilder; - _connectionStrings = connectionStrings; - _logger = logger; - _databaseProviderMetadata = databaseProviderMetadata; - } + public DatabaseConfigureStep( + DatabaseBuilder databaseBuilder, + IOptionsMonitor connectionStrings, + ILogger logger, + IEnumerable databaseProviderMetadata) + { + _databaseBuilder = databaseBuilder; + _connectionStrings = connectionStrings; + _logger = logger; + _databaseProviderMetadata = databaseProviderMetadata; + } - public override Task ExecuteAsync(DatabaseModel databaseSettings) + public override object ViewModel + { + get { - if (!_databaseBuilder.ConfigureDatabaseConnection(databaseSettings, isTrialRun: false)) - { - throw new InstallException("Could not connect to the database"); - } + var options = _databaseProviderMetadata + .Where(x => x.IsAvailable) + .OrderBy(x => x.SortOrder) + .ToList(); - return Task.FromResult(null); + return new {databases = options}; } + } - public override object ViewModel - { - get - { - var options = _databaseProviderMetadata - .Where(x => x.IsAvailable) - .OrderBy(x => x.SortOrder) - .ToList(); + public override string View => ShouldDisplayView() ? base.View : ""; - return new - { - databases = options - }; - } + public override Task ExecuteAsync(DatabaseModel databaseSettings) + { + if (!_databaseBuilder.ConfigureDatabaseConnection(databaseSettings, false)) + { + throw new InstallException("Could not connect to the database"); } - public override string View => ShouldDisplayView() ? base.View : ""; + return Task.FromResult(null); + } - public override bool RequiresExecution(DatabaseModel model) => ShouldDisplayView(); + public override bool RequiresExecution(DatabaseModel model) => ShouldDisplayView(); - private bool ShouldDisplayView() - { - //If the connection string is already present in web.config we don't need to show the settings page and we jump to installing/upgrading. - var databaseSettings = _connectionStrings.Get(Core.Constants.System.UmbracoConnectionName); + private bool ShouldDisplayView() + { + //If the connection string is already present in web.config we don't need to show the settings page and we jump to installing/upgrading. + ConnectionStrings? databaseSettings = _connectionStrings.Get(Constants.System.UmbracoConnectionName); - if (databaseSettings.IsConnectionStringConfigured()) + if (databaseSettings.IsConnectionStringConfigured()) + { + try { - try - { - //Since a connection string was present we verify the db can connect and query - _ = _databaseBuilder.ValidateSchema(); + //Since a connection string was present we verify the db can connect and query + _ = _databaseBuilder.ValidateSchema(); - return false; - } - catch (Exception ex) - { - _logger.LogError(ex, "An error occurred, reconfiguring..."); - //something went wrong, could not connect so probably need to reconfigure - return true; - } + return false; + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred, reconfiguring..."); + //something went wrong, could not connect so probably need to reconfigure + return true; } - - return true; } + + return true; } } diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseInstallStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseInstallStep.cs index 21da2f797acb..d4ee0ba7d2a0 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseInstallStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseInstallStep.cs @@ -1,55 +1,50 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Install; using Umbraco.Cms.Core.Install.Models; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Migrations.Install; -namespace Umbraco.Cms.Infrastructure.Install.InstallSteps +namespace Umbraco.Cms.Infrastructure.Install.InstallSteps; + +[InstallSetupStep(InstallationType.NewInstall | InstallationType.Upgrade, "DatabaseInstall", 11, "")] +public class DatabaseInstallStep : InstallSetupStep { - [InstallSetupStep(InstallationType.NewInstall | InstallationType.Upgrade, "DatabaseInstall", 11, "")] - public class DatabaseInstallStep : InstallSetupStep + private readonly DatabaseBuilder _databaseBuilder; + private readonly IRuntimeState _runtime; + + public DatabaseInstallStep(IRuntimeState runtime, DatabaseBuilder databaseBuilder) + { + _runtime = runtime; + _databaseBuilder = databaseBuilder; + } + + public override Task ExecuteAsync(object model) { - private readonly IRuntimeState _runtime; - private readonly DatabaseBuilder _databaseBuilder; + if (_runtime.Level == RuntimeLevel.Run) + { + throw new Exception("Umbraco is already configured!"); + } + + if (_runtime.Reason == RuntimeLevelReason.InstallMissingDatabase) + { + _databaseBuilder.CreateDatabase(); + } + + DatabaseBuilder.Result? result = _databaseBuilder.CreateSchemaAndData(); - public DatabaseInstallStep(IRuntimeState runtime, DatabaseBuilder databaseBuilder) + if (result?.Success == false) { - _runtime = runtime; - _databaseBuilder = databaseBuilder; + throw new InstallException("The database failed to install. ERROR: " + result.Message); } - public override Task ExecuteAsync(object model) + if (result?.RequiresUpgrade == false) { - if (_runtime.Level == RuntimeLevel.Run) - throw new Exception("Umbraco is already configured!"); - - if (_runtime.Reason == RuntimeLevelReason.InstallMissingDatabase) - { - _databaseBuilder.CreateDatabase(); - } - - var result = _databaseBuilder.CreateSchemaAndData(); - - if (result?.Success == false) - { - throw new InstallException("The database failed to install. ERROR: " + result.Message); - } - - if (result?.RequiresUpgrade == false) - { - return Task.FromResult(null); - } - - // Upgrade is required, so set the flag for the next step - return Task.FromResult(new InstallSetupResult(new Dictionary - { - { "upgrade", true} - }))!; + return Task.FromResult(null); } - public override bool RequiresExecution(object model) => true; + // Upgrade is required, so set the flag for the next step + return Task.FromResult(new InstallSetupResult(new Dictionary {{"upgrade", true}}))!; } + + public override bool RequiresExecution(object model) => true; } diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseUpgradeStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseUpgradeStep.cs index 25494ff92536..d753c014ac0a 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseUpgradeStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseUpgradeStep.cs @@ -1,6 +1,3 @@ -using System; -using System.Linq; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -14,81 +11,82 @@ using Umbraco.Cms.Infrastructure.Migrations.Upgrade; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Install.InstallSteps +namespace Umbraco.Cms.Infrastructure.Install.InstallSteps; + +[InstallSetupStep(InstallationType.Upgrade | InstallationType.NewInstall, + "DatabaseUpgrade", 12, "")] +public class DatabaseUpgradeStep : InstallSetupStep { - [InstallSetupStep(InstallationType.Upgrade | InstallationType.NewInstall, - "DatabaseUpgrade", 12, "")] - public class DatabaseUpgradeStep : InstallSetupStep + private readonly IOptionsMonitor _connectionStrings; + private readonly DatabaseBuilder _databaseBuilder; + private readonly ILogger _logger; + private readonly IRuntimeState _runtime; + private readonly IUmbracoVersion _umbracoVersion; + + public DatabaseUpgradeStep( + DatabaseBuilder databaseBuilder, + IRuntimeState runtime, + ILogger logger, + IUmbracoVersion umbracoVersion, + IOptionsMonitor connectionStrings) { - private readonly DatabaseBuilder _databaseBuilder; - private readonly IRuntimeState _runtime; - private readonly ILogger _logger; - private readonly IUmbracoVersion _umbracoVersion; - private readonly IOptionsMonitor _connectionStrings; + _databaseBuilder = databaseBuilder; + _runtime = runtime; + _logger = logger; + _umbracoVersion = umbracoVersion; + _connectionStrings = connectionStrings; + } - public DatabaseUpgradeStep( - DatabaseBuilder databaseBuilder, - IRuntimeState runtime, - ILogger logger, - IUmbracoVersion umbracoVersion, - IOptionsMonitor connectionStrings) - { - _databaseBuilder = databaseBuilder; - _runtime = runtime; - _logger = logger; - _umbracoVersion = umbracoVersion; - _connectionStrings = connectionStrings; - } + public override Task ExecuteAsync(object model) + { + InstallTrackingItem[] installSteps = InstallStatusTracker.GetStatus().ToArray(); + InstallTrackingItem previousStep = installSteps.Single(x => x.Name == "DatabaseInstall"); + var upgrade = previousStep.AdditionalData.ContainsKey("upgrade"); - public override Task ExecuteAsync(object model) + if (upgrade) { - var installSteps = InstallStatusTracker.GetStatus().ToArray(); - var previousStep = installSteps.Single(x => x.Name == "DatabaseInstall"); - var upgrade = previousStep.AdditionalData.ContainsKey("upgrade"); + _logger.LogInformation("Running 'Upgrade' service"); - if (upgrade) - { - _logger.LogInformation("Running 'Upgrade' service"); + var plan = new UmbracoPlan(_umbracoVersion); + plan.AddPostMigration(); // needed when running installer (back-office) - var plan = new UmbracoPlan(_umbracoVersion); - plan.AddPostMigration(); // needed when running installer (back-office) + DatabaseBuilder.Result? result = _databaseBuilder.UpgradeSchemaAndData(plan); - var result = _databaseBuilder.UpgradeSchemaAndData(plan); - - if (result?.Success == false) - { - throw new InstallException("The database failed to upgrade. ERROR: " + result.Message); - } + if (result?.Success == false) + { + throw new InstallException("The database failed to upgrade. ERROR: " + result.Message); } - - return Task.FromResult((InstallSetupResult?)null); } - public override bool RequiresExecution(object model) - { - //if it's properly configured (i.e. the versions match) then no upgrade necessary - if (_runtime.Level == RuntimeLevel.Run) - return false; + return Task.FromResult((InstallSetupResult?)null); + } - var installSteps = InstallStatusTracker.GetStatus().ToArray(); - //this step relies on the previous one completed - because it has stored some information we need - if (installSteps.Any(x => x.Name == "DatabaseInstall" && x.AdditionalData.ContainsKey("upgrade")) == false) - { - return false; - } + public override bool RequiresExecution(object model) + { + //if it's properly configured (i.e. the versions match) then no upgrade necessary + if (_runtime.Level == RuntimeLevel.Run) + { + return false; + } - var databaseSettings = _connectionStrings.Get(Core.Constants.System.UmbracoConnectionName); + InstallTrackingItem[] installSteps = InstallStatusTracker.GetStatus().ToArray(); + //this step relies on the previous one completed - because it has stored some information we need + if (installSteps.Any(x => x.Name == "DatabaseInstall" && x.AdditionalData.ContainsKey("upgrade")) == false) + { + return false; + } - if (databaseSettings.IsConnectionStringConfigured()) - { - // a connection string was present, determine whether this is an install/upgrade - // return true (upgrade) if there is an installed version, else false (install) - var result = _databaseBuilder.ValidateSchema(); - return result?.DetermineHasInstalledVersion() ?? false; - } + ConnectionStrings? databaseSettings = _connectionStrings.Get(Constants.System.UmbracoConnectionName); - //no connection string configured, probably a fresh install - return false; + if (databaseSettings.IsConnectionStringConfigured()) + { + // a connection string was present, determine whether this is an install/upgrade + // return true (upgrade) if there is an installed version, else false (install) + DatabaseSchemaResult? result = _databaseBuilder.ValidateSchema(); + return result?.DetermineHasInstalledVersion() ?? false; } + + //no connection string configured, probably a fresh install + return false; } } diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs index 38ac00045212..9a371ebb4642 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs @@ -1,236 +1,241 @@ -using System; -using System.Collections.Generic; using System.Collections.Specialized; -using System.Linq; -using System.Net.Http; +using System.Data.Common; using System.Text; -using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; using Newtonsoft.Json; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Extensions; -using Constants = Umbraco.Cms.Core.Constants; -namespace Umbraco.Cms.Infrastructure.Install.InstallSteps +namespace Umbraco.Cms.Infrastructure.Install.InstallSteps; + +/// +/// This is the first UI step for a brand new install +/// +/// +/// By default this will show the user view which is the most basic information to configure a new install, but if an +/// install get's interrupted because of an +/// error, etc... and the end-user refreshes the installer then we cannot show the user screen because they've already +/// entered that information so instead we'll +/// display a simple continue installation view. +/// +[InstallSetupStep(InstallationType.NewInstall, "User", 20, "")] +public class NewInstallStep : InstallSetupStep { + private readonly IOptionsMonitor _connectionStrings; + private readonly ICookieManager _cookieManager; + private readonly DatabaseBuilder _databaseBuilder; + private readonly IEnumerable _databaseProviderMetadata; + private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator; + private readonly IHttpClientFactory _httpClientFactory; + private readonly UserPasswordConfigurationSettings _passwordConfiguration; + private readonly SecuritySettings _securitySettings; + private readonly IBackOfficeUserManager _userManager; + private readonly IUserService _userService; + + public NewInstallStep( + IUserService userService, + DatabaseBuilder databaseBuilder, + IHttpClientFactory httpClientFactory, + IOptions passwordConfiguration, + IOptions securitySettings, + IOptionsMonitor connectionStrings, + ICookieManager cookieManager, + IBackOfficeUserManager userManager, + IDbProviderFactoryCreator dbProviderFactoryCreator, + IEnumerable databaseProviderMetadata) + { + _userService = userService ?? throw new ArgumentNullException(nameof(userService)); + _databaseBuilder = databaseBuilder ?? throw new ArgumentNullException(nameof(databaseBuilder)); + _httpClientFactory = httpClientFactory; + _passwordConfiguration = + passwordConfiguration.Value ?? throw new ArgumentNullException(nameof(passwordConfiguration)); + _securitySettings = securitySettings.Value ?? throw new ArgumentNullException(nameof(securitySettings)); + _connectionStrings = connectionStrings; + _cookieManager = cookieManager; + _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager)); + _dbProviderFactoryCreator = dbProviderFactoryCreator ?? + throw new ArgumentNullException(nameof(dbProviderFactoryCreator)); + _databaseProviderMetadata = databaseProviderMetadata; + } + /// - /// This is the first UI step for a brand new install + /// Return a custom view model for this step /// - /// - /// By default this will show the user view which is the most basic information to configure a new install, but if an install get's interrupted because of an - /// error, etc... and the end-user refreshes the installer then we cannot show the user screen because they've already entered that information so instead we'll - /// display a simple continue installation view. - /// - [InstallSetupStep(InstallationType.NewInstall, "User", 20, "")] - public class NewInstallStep : InstallSetupStep + public override object ViewModel { - private readonly IUserService _userService; - private readonly DatabaseBuilder _databaseBuilder; - private readonly IHttpClientFactory _httpClientFactory; - private readonly UserPasswordConfigurationSettings _passwordConfiguration; - private readonly SecuritySettings _securitySettings; - private readonly IOptionsMonitor _connectionStrings; - private readonly ICookieManager _cookieManager; - private readonly IBackOfficeUserManager _userManager; - private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator; - private readonly IEnumerable _databaseProviderMetadata; - - public NewInstallStep( - IUserService userService, - DatabaseBuilder databaseBuilder, - IHttpClientFactory httpClientFactory, - IOptions passwordConfiguration, - IOptions securitySettings, - IOptionsMonitor connectionStrings, - ICookieManager cookieManager, - IBackOfficeUserManager userManager, - IDbProviderFactoryCreator dbProviderFactoryCreator, - IEnumerable databaseProviderMetadata) - { - _userService = userService ?? throw new ArgumentNullException(nameof(userService)); - _databaseBuilder = databaseBuilder ?? throw new ArgumentNullException(nameof(databaseBuilder)); - _httpClientFactory = httpClientFactory; - _passwordConfiguration = passwordConfiguration.Value ?? throw new ArgumentNullException(nameof(passwordConfiguration)); - _securitySettings = securitySettings.Value ?? throw new ArgumentNullException(nameof(securitySettings)); - _connectionStrings = connectionStrings; - _cookieManager = cookieManager; - _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager)); - _dbProviderFactoryCreator = dbProviderFactoryCreator ?? throw new ArgumentNullException(nameof(dbProviderFactoryCreator)); - _databaseProviderMetadata = databaseProviderMetadata; - } - - public override async Task ExecuteAsync(UserModel user) + get { - var admin = _userService.GetUserById(Constants.Security.SuperUserId); - if (admin == null) + var quickInstallSettings = _databaseProviderMetadata + .Where(x => x.SupportsQuickInstall) + .Where(x => x.IsAvailable) + .OrderBy(x => x.SortOrder) + .Select(x => new {displayName = x.DisplayName, defaultDatabaseName = x.DefaultDatabaseName}) + .FirstOrDefault(); + + return new { - throw new InvalidOperationException("Could not find the super user!"); - } - admin.Email = user.Email.Trim(); - admin.Name = user.Name!.Trim(); - admin.Username = user.Email.Trim(); - - _userService.Save(admin); - - var membershipUser = await _userManager.FindByIdAsync(Constants.Security.SuperUserIdAsString); - if (membershipUser == null) - { - throw new InvalidOperationException( - $"No user found in membership provider with id of {Constants.Security.SuperUserIdAsString}."); - } - - //To change the password here we actually need to reset it since we don't have an old one to use to change - var resetToken = await _userManager.GeneratePasswordResetTokenAsync(membershipUser); - if (string.IsNullOrWhiteSpace(resetToken)) - throw new InvalidOperationException("Could not reset password: unable to generate internal reset token"); + minCharLength = _passwordConfiguration.RequiredLength, + minNonAlphaNumericLength = _passwordConfiguration.GetMinNonAlphaNumericChars(), + quickInstallSettings, + customInstallAvailable = !GetInstallState().HasFlag(InstallState.ConnectionStringConfigured) + }; + } + } - var resetResult = await _userManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, user.Password.Trim()); - if (!resetResult.Succeeded) - throw new InvalidOperationException("Could not reset password: " + string.Join(", ", resetResult.Errors.ToErrorMessage())); + public override string View => + ShowView() + // the user UI + ? "user" + // continue install UI + : "continueinstall"; - if (user.SubscribeToNewsLetter) - { - var values = new NameValueCollection { { "name", admin.Name }, { "email", admin.Email } }; - var content = new StringContent(JsonConvert.SerializeObject(values), Encoding.UTF8, "application/json"); + public override async Task ExecuteAsync(UserModel user) + { + IUser? admin = _userService.GetUserById(Constants.Security.SuperUserId); + if (admin == null) + { + throw new InvalidOperationException("Could not find the super user!"); + } - HttpClient httpClient = _httpClientFactory.CreateClient(); + admin.Email = user.Email.Trim(); + admin.Name = user.Name!.Trim(); + admin.Username = user.Email.Trim(); - try - { - var response = httpClient.PostAsync("https://shop.umbraco.com/base/Ecom/SubmitEmail/installer.aspx", content).Result; - } - catch { /* fail in silence */ } - } + _userService.Save(admin); - return null; + BackOfficeIdentityUser? membershipUser = + await _userManager.FindByIdAsync(Constants.Security.SuperUserIdAsString); + if (membershipUser == null) + { + throw new InvalidOperationException( + $"No user found in membership provider with id of {Constants.Security.SuperUserIdAsString}."); } - /// - /// Return a custom view model for this step - /// - public override object ViewModel + //To change the password here we actually need to reset it since we don't have an old one to use to change + var resetToken = await _userManager.GeneratePasswordResetTokenAsync(membershipUser); + if (string.IsNullOrWhiteSpace(resetToken)) { - get - { - var quickInstallSettings = _databaseProviderMetadata - .Where(x => x.SupportsQuickInstall) - .Where(x => x.IsAvailable) - .OrderBy(x => x.SortOrder) - .Select(x => new - { - displayName = x.DisplayName, - defaultDatabaseName = x.DefaultDatabaseName, - }) - .FirstOrDefault(); - - return new - { - minCharLength = _passwordConfiguration.RequiredLength, - minNonAlphaNumericLength = _passwordConfiguration.GetMinNonAlphaNumericChars(), - quickInstallSettings, - customInstallAvailable = !GetInstallState().HasFlag(InstallState.ConnectionStringConfigured) - }; - } + throw new InvalidOperationException("Could not reset password: unable to generate internal reset token"); } - public override string View + IdentityResult resetResult = + await _userManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, user.Password.Trim()); + if (!resetResult.Succeeded) { - get - { - return ShowView() - // the user UI - ? "user" - // continue install UI - : "continueinstall"; - } + throw new InvalidOperationException("Could not reset password: " + + string.Join(", ", resetResult.Errors.ToErrorMessage())); } - private InstallState GetInstallState() + if (user.SubscribeToNewsLetter) { - var installState = InstallState.Unknown; - - - // TODO: we need to do a null check here since this could be entirely missing and we end up with a null ref - // exception in the installer. + var values = new NameValueCollection {{"name", admin.Name}, {"email", admin.Email}}; + var content = new StringContent(JsonConvert.SerializeObject(values), Encoding.UTF8, "application/json"); - var databaseSettings = _connectionStrings.Get(Constants.System.UmbracoConnectionName); + HttpClient httpClient = _httpClientFactory.CreateClient(); - var hasConnString = databaseSettings != null && _databaseBuilder.IsDatabaseConfigured; - if (hasConnString) + try { - installState = (installState | InstallState.HasConnectionString) & ~InstallState.Unknown; + HttpResponseMessage response = httpClient + .PostAsync("https://shop.umbraco.com/base/Ecom/SubmitEmail/installer.aspx", content).Result; } - - var connStringConfigured = databaseSettings?.IsConnectionStringConfigured() ?? false; - if (connStringConfigured) + catch { - installState = (installState | InstallState.ConnectionStringConfigured) & ~InstallState.Unknown; + /* fail in silence */ } + } + + return null; + } + private InstallState GetInstallState() + { + InstallState installState = InstallState.Unknown; - var factory = _dbProviderFactoryCreator.CreateFactory(databaseSettings?.ProviderName); - var canConnect = connStringConfigured && DbConnectionExtensions.IsConnectionAvailable(databaseSettings?.ConnectionString, factory); - if (canConnect) - { - installState = (installState | InstallState.CanConnect) & ~InstallState.Unknown; - } - var umbracoInstalled = canConnect ? _databaseBuilder.IsUmbracoInstalled() : false; - if (umbracoInstalled) - { - installState = (installState | InstallState.UmbracoInstalled) & ~InstallState.Unknown; - } + // TODO: we need to do a null check here since this could be entirely missing and we end up with a null ref + // exception in the installer. - var hasNonDefaultUser = umbracoInstalled ? _databaseBuilder.HasSomeNonDefaultUser() : false; - if (hasNonDefaultUser) - { - installState = (installState | InstallState.HasNonDefaultUser) & ~InstallState.Unknown; - } + ConnectionStrings? databaseSettings = _connectionStrings.Get(Constants.System.UmbracoConnectionName); - return installState; + var hasConnString = databaseSettings != null && _databaseBuilder.IsDatabaseConfigured; + if (hasConnString) + { + installState = (installState | InstallState.HasConnectionString) & ~InstallState.Unknown; } - private bool ShowView() + var connStringConfigured = databaseSettings?.IsConnectionStringConfigured() ?? false; + if (connStringConfigured) { - var installState = GetInstallState(); - - return installState.HasFlag(InstallState.Unknown) - || !installState.HasFlag(InstallState.UmbracoInstalled); + installState = (installState | InstallState.ConnectionStringConfigured) & ~InstallState.Unknown; } - public override bool RequiresExecution(UserModel model) + + DbProviderFactory? factory = _dbProviderFactoryCreator.CreateFactory(databaseSettings?.ProviderName); + var canConnect = connStringConfigured && + DbConnectionExtensions.IsConnectionAvailable(databaseSettings?.ConnectionString, factory); + if (canConnect) { - var installState = GetInstallState(); + installState = (installState | InstallState.CanConnect) & ~InstallState.Unknown; + } - if (installState.HasFlag(InstallState.Unknown)) - { - // In this one case when it's a brand new install and nothing has been configured, make sure the - // back office cookie is cleared so there's no old cookies lying around causing problems - _cookieManager.ExpireCookie(_securitySettings.AuthCookieName); - } + var umbracoInstalled = canConnect ? _databaseBuilder.IsUmbracoInstalled() : false; + if (umbracoInstalled) + { + installState = (installState | InstallState.UmbracoInstalled) & ~InstallState.Unknown; + } - return installState.HasFlag(InstallState.Unknown) - || !installState.HasFlag(InstallState.HasNonDefaultUser); + var hasNonDefaultUser = umbracoInstalled ? _databaseBuilder.HasSomeNonDefaultUser() : false; + if (hasNonDefaultUser) + { + installState = (installState | InstallState.HasNonDefaultUser) & ~InstallState.Unknown; } - [Flags] - private enum InstallState : short + return installState; + } + + private bool ShowView() + { + InstallState installState = GetInstallState(); + + return installState.HasFlag(InstallState.Unknown) + || !installState.HasFlag(InstallState.UmbracoInstalled); + } + + public override bool RequiresExecution(UserModel model) + { + InstallState installState = GetInstallState(); + + if (installState.HasFlag(InstallState.Unknown)) { - // This is an easy way to avoid 0 enum assignment and not worry about - // manual calcs. https://www.codeproject.com/Articles/396851/Ending-the-Great-Debate-on-Enum-Flags - Unknown = 1, - HasVersion = 1 << 1, - HasConnectionString = 1 << 2, - ConnectionStringConfigured = 1 << 3, - CanConnect = 1 << 4, - UmbracoInstalled = 1 << 5, - HasNonDefaultUser = 1 << 6 + // In this one case when it's a brand new install and nothing has been configured, make sure the + // back office cookie is cleared so there's no old cookies lying around causing problems + _cookieManager.ExpireCookie(_securitySettings.AuthCookieName); } + + return installState.HasFlag(InstallState.Unknown) + || !installState.HasFlag(InstallState.HasNonDefaultUser); + } + + [Flags] + private enum InstallState : short + { + // This is an easy way to avoid 0 enum assignment and not worry about + // manual calcs. https://www.codeproject.com/Articles/396851/Ending-the-Great-Debate-on-Enum-Flags + Unknown = 1, + HasVersion = 1 << 1, + HasConnectionString = 1 << 2, + ConnectionStringConfigured = 1 << 3, + CanConnect = 1 << 4, + UmbracoInstalled = 1 << 5, + HasNonDefaultUser = 1 << 6 } } diff --git a/src/Umbraco.Infrastructure/Install/PackageMigrationRunner.cs b/src/Umbraco.Infrastructure/Install/PackageMigrationRunner.cs index 100e3ee26fda..faf79d1a452b 100644 --- a/src/Umbraco.Infrastructure/Install/PackageMigrationRunner.cs +++ b/src/Umbraco.Infrastructure/Install/PackageMigrationRunner.cs @@ -1,108 +1,105 @@ -using System; -using System.Linq; -using System.Collections.Generic; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Logging; -using Umbraco.Cms.Core.Packaging; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Infrastructure.Migrations.Upgrade; -using Umbraco.Extensions; using Umbraco.Cms.Core.Migrations; +using Umbraco.Cms.Core.Packaging; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Migrations; using Umbraco.Cms.Infrastructure.Migrations.Notifications; -using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Migrations.Upgrade; +using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Install +namespace Umbraco.Cms.Infrastructure.Install; + +/// +/// Runs the package migration plans +/// +public class PackageMigrationRunner { + private readonly IEventAggregator _eventAggregator; + private readonly IKeyValueService _keyValueService; + private readonly IMigrationPlanExecutor _migrationPlanExecutor; + private readonly Dictionary _packageMigrationPlans; + private readonly PendingPackageMigrations _pendingPackageMigrations; + private readonly IProfilingLogger _profilingLogger; + private readonly ICoreScopeProvider _scopeProvider; + + public PackageMigrationRunner( + IProfilingLogger profilingLogger, + ICoreScopeProvider scopeProvider, + PendingPackageMigrations pendingPackageMigrations, + PackageMigrationPlanCollection packageMigrationPlans, + IMigrationPlanExecutor migrationPlanExecutor, + IKeyValueService keyValueService, + IEventAggregator eventAggregator) + { + _profilingLogger = profilingLogger; + _scopeProvider = scopeProvider; + _pendingPackageMigrations = pendingPackageMigrations; + _migrationPlanExecutor = migrationPlanExecutor; + _keyValueService = keyValueService; + _eventAggregator = eventAggregator; + _packageMigrationPlans = packageMigrationPlans.ToDictionary(x => x.Name); + } + /// - /// Runs the package migration plans + /// Runs all migration plans for a package name if any are pending. /// - public class PackageMigrationRunner + /// + /// + public IEnumerable RunPackageMigrationsIfPending(string packageName) { - private readonly IProfilingLogger _profilingLogger; - private readonly ICoreScopeProvider _scopeProvider; - private readonly PendingPackageMigrations _pendingPackageMigrations; - private readonly IMigrationPlanExecutor _migrationPlanExecutor; - private readonly IKeyValueService _keyValueService; - private readonly IEventAggregator _eventAggregator; - private readonly Dictionary _packageMigrationPlans; + IReadOnlyDictionary? keyValues = + _keyValueService.FindByKeyPrefix(Constants.Conventions.Migrations.KeyValuePrefix); + IReadOnlyList pendingMigrations = _pendingPackageMigrations.GetPendingPackageMigrations(keyValues); - public PackageMigrationRunner( - IProfilingLogger profilingLogger, - ICoreScopeProvider scopeProvider, - PendingPackageMigrations pendingPackageMigrations, - PackageMigrationPlanCollection packageMigrationPlans, - IMigrationPlanExecutor migrationPlanExecutor, - IKeyValueService keyValueService, - IEventAggregator eventAggregator) - { - _profilingLogger = profilingLogger; - _scopeProvider = scopeProvider; - _pendingPackageMigrations = pendingPackageMigrations; - _migrationPlanExecutor = migrationPlanExecutor; - _keyValueService = keyValueService; - _eventAggregator = eventAggregator; - _packageMigrationPlans = packageMigrationPlans.ToDictionary(x => x.Name); - } - - /// - /// Runs all migration plans for a package name if any are pending. - /// - /// - /// - public IEnumerable RunPackageMigrationsIfPending(string packageName) - { - IReadOnlyDictionary? keyValues = _keyValueService.FindByKeyPrefix(Constants.Conventions.Migrations.KeyValuePrefix); - IReadOnlyList pendingMigrations = _pendingPackageMigrations.GetPendingPackageMigrations(keyValues); + IEnumerable packagePlans = _packageMigrationPlans.Values + .Where(x => x.PackageName.InvariantEquals(packageName)) + .Where(x => pendingMigrations.Contains(x.Name)) + .Select(x => x.Name); - IEnumerable packagePlans = _packageMigrationPlans.Values - .Where(x => x.PackageName.InvariantEquals(packageName)) - .Where(x => pendingMigrations.Contains(x.Name)) - .Select(x => x.Name); + return RunPackagePlans(packagePlans); + } - return RunPackagePlans(packagePlans); - } + /// + /// Runs the all specified package migration plans and publishes a + /// if all are successful. + /// + /// + /// + /// If any plan fails it will throw an exception. + public IEnumerable RunPackagePlans(IEnumerable plansToRun) + { + var results = new List(); - /// - /// Runs the all specified package migration plans and publishes a - /// if all are successful. - /// - /// - /// - /// If any plan fails it will throw an exception. - public IEnumerable RunPackagePlans(IEnumerable plansToRun) + // Create an explicit scope around all package migrations so they are + // all executed in a single transaction. If one package migration fails, + // none of them will be committed. This is intended behavior so we can + // ensure when we publish the success notification that is is done when they all succeed. + using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true)) { - var results = new List(); - - // Create an explicit scope around all package migrations so they are - // all executed in a single transaction. If one package migration fails, - // none of them will be committed. This is intended behavior so we can - // ensure when we publish the success notification that is is done when they all succeed. - using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true)) + foreach (var migrationName in plansToRun) { - foreach (var migrationName in plansToRun) + if (!_packageMigrationPlans.TryGetValue(migrationName, out PackageMigrationPlan? plan)) { - if (!_packageMigrationPlans.TryGetValue(migrationName, out PackageMigrationPlan? plan)) - { - throw new InvalidOperationException("Cannot find package migration plan " + migrationName); - } + throw new InvalidOperationException("Cannot find package migration plan " + migrationName); + } - using (_profilingLogger.TraceDuration( - "Starting unattended package migration for " + migrationName, - "Unattended upgrade completed for " + migrationName)) - { - var upgrader = new Upgrader(plan); - // This may throw, if so the transaction will be rolled back - results.Add(upgrader.Execute(_migrationPlanExecutor, _scopeProvider, _keyValueService)); - } + using (_profilingLogger.TraceDuration( + "Starting unattended package migration for " + migrationName, + "Unattended upgrade completed for " + migrationName)) + { + var upgrader = new Upgrader(plan); + // This may throw, if so the transaction will be rolled back + results.Add(upgrader.Execute(_migrationPlanExecutor, _scopeProvider, _keyValueService)); } } + } - var executedPlansNotification = new MigrationPlansExecutedNotification(results); - _eventAggregator.Publish(executedPlansNotification); + var executedPlansNotification = new MigrationPlansExecutedNotification(results); + _eventAggregator.Publish(executedPlansNotification); - return results; - } + return results; } } diff --git a/src/Umbraco.Infrastructure/Install/UnattendedInstaller.cs b/src/Umbraco.Infrastructure/Install/UnattendedInstaller.cs index bf38c2b6647a..e96dd27d8256 100644 --- a/src/Umbraco.Infrastructure/Install/UnattendedInstaller.cs +++ b/src/Umbraco.Infrastructure/Install/UnattendedInstaller.cs @@ -1,6 +1,3 @@ -using System; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -12,129 +9,130 @@ using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Infrastructure.Install +namespace Umbraco.Cms.Infrastructure.Install; + +public class UnattendedInstaller : INotificationAsyncHandler { - public class UnattendedInstaller : INotificationAsyncHandler + private readonly IUmbracoDatabaseFactory _databaseFactory; + + private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory; + private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator; + private readonly IEventAggregator _eventAggregator; + private readonly ILogger _logger; + private readonly IRuntimeState _runtimeState; + private readonly IOptions _unattendedSettings; + + public UnattendedInstaller( + DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, + IEventAggregator eventAggregator, + IOptions unattendedSettings, + IUmbracoDatabaseFactory databaseFactory, + IDbProviderFactoryCreator dbProviderFactoryCreator, + ILogger logger, + IRuntimeState runtimeState) { - private readonly IOptions _unattendedSettings; - - private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory; - private readonly IEventAggregator _eventAggregator; - private readonly IUmbracoDatabaseFactory _databaseFactory; - private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator; - private readonly ILogger _logger; - private readonly IRuntimeState _runtimeState; - - public UnattendedInstaller( - DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, - IEventAggregator eventAggregator, - IOptions unattendedSettings, - IUmbracoDatabaseFactory databaseFactory, - IDbProviderFactoryCreator dbProviderFactoryCreator, - ILogger logger, - IRuntimeState runtimeState) + _databaseSchemaCreatorFactory = databaseSchemaCreatorFactory ?? + throw new ArgumentNullException(nameof(databaseSchemaCreatorFactory)); + _eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator)); + _unattendedSettings = unattendedSettings; + _databaseFactory = databaseFactory; + _dbProviderFactoryCreator = dbProviderFactoryCreator; + _logger = logger; + _runtimeState = runtimeState; + } + + public Task HandleAsync(RuntimeUnattendedInstallNotification notification, CancellationToken cancellationToken) + { + // unattended install is not enabled + if (_unattendedSettings.Value.InstallUnattended == false) { - _databaseSchemaCreatorFactory = databaseSchemaCreatorFactory ?? throw new ArgumentNullException(nameof(databaseSchemaCreatorFactory)); - _eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator)); - _unattendedSettings = unattendedSettings; - _databaseFactory = databaseFactory; - _dbProviderFactoryCreator = dbProviderFactoryCreator; - _logger = logger; - _runtimeState = runtimeState; + return Task.CompletedTask; } - public Task HandleAsync(RuntimeUnattendedInstallNotification notification, CancellationToken cancellationToken) + // no connection string set + if (_databaseFactory.Configured == false) { - // unattended install is not enabled - if (_unattendedSettings.Value.InstallUnattended == false) - { - return Task.CompletedTask; - } - - // no connection string set - if (_databaseFactory.Configured == false) - { - return Task.CompletedTask; - } + return Task.CompletedTask; + } - _runtimeState.DetermineRuntimeLevel(); - if (_runtimeState.Reason == RuntimeLevelReason.InstallMissingDatabase) - { - _dbProviderFactoryCreator.CreateDatabase(_databaseFactory.ProviderName!, _databaseFactory.ConnectionString!); - } + _runtimeState.DetermineRuntimeLevel(); + if (_runtimeState.Reason == RuntimeLevelReason.InstallMissingDatabase) + { + _dbProviderFactoryCreator.CreateDatabase(_databaseFactory.ProviderName!, + _databaseFactory.ConnectionString!); + } - bool connect; - try + bool connect; + try + { + for (var i = 0;;) { - for (var i = 0; ;) + connect = _databaseFactory.CanConnect; + if (connect || ++i == 5) { - connect = _databaseFactory.CanConnect; - if (connect || ++i == 5) - { - break; - } + break; + } - _logger.LogDebug("Could not immediately connect to database, trying again."); + _logger.LogDebug("Could not immediately connect to database, trying again."); - Thread.Sleep(1000); - } + Thread.Sleep(1000); } - catch (Exception ex) - { - _logger.LogInformation(ex, "Error during unattended install."); + } + catch (Exception ex) + { + _logger.LogInformation(ex, "Error during unattended install."); - var innerException = new UnattendedInstallException("Unattended installation failed.", ex); - _runtimeState.Configure(Core.RuntimeLevel.BootFailed, Core.RuntimeLevelReason.BootFailedOnException, innerException); - return Task.CompletedTask; - } + var innerException = new UnattendedInstallException("Unattended installation failed.", ex); + _runtimeState.Configure(RuntimeLevel.BootFailed, RuntimeLevelReason.BootFailedOnException, innerException); + return Task.CompletedTask; + } - // could not connect to the database - if (connect == false) - { - return Task.CompletedTask; - } + // could not connect to the database + if (connect == false) + { + return Task.CompletedTask; + } - IUmbracoDatabase? database = null; - try + IUmbracoDatabase? database = null; + try + { + using (database = _databaseFactory.CreateDatabase()) { - using (database = _databaseFactory.CreateDatabase()) + var hasUmbracoTables = database?.IsUmbracoInstalled() ?? false; + + // database has umbraco tables, assume Umbraco is already installed + if (hasUmbracoTables) { - var hasUmbracoTables = database?.IsUmbracoInstalled() ?? false; - - // database has umbraco tables, assume Umbraco is already installed - if (hasUmbracoTables) - { - return Task.CompletedTask; - } - - // all conditions fulfilled, do the install - _logger.LogInformation("Starting unattended install."); - - database?.BeginTransaction(); - DatabaseSchemaCreator creator = _databaseSchemaCreatorFactory.Create(database); - creator.InitializeDatabaseSchema(); - database?.CompleteTransaction(); - _logger.LogInformation("Unattended install completed."); - - // Emit an event with EventAggregator that unattended install completed - // Then this event can be listened for and create an unattended user - _eventAggregator.Publish(new UnattendedInstallNotification()); + return Task.CompletedTask; } - } - catch (Exception ex) - { - _logger.LogInformation(ex, "Error during unattended install."); - database?.AbortTransaction(); - var innerException = new UnattendedInstallException( - "The database configuration failed." - + "\n Please check log file for additional information (can be found in '/Umbraco/Data/Logs/')", - ex); + // all conditions fulfilled, do the install + _logger.LogInformation("Starting unattended install."); + + database?.BeginTransaction(); + DatabaseSchemaCreator creator = _databaseSchemaCreatorFactory.Create(database); + creator.InitializeDatabaseSchema(); + database?.CompleteTransaction(); + _logger.LogInformation("Unattended install completed."); - _runtimeState.Configure(Core.RuntimeLevel.BootFailed, Core.RuntimeLevelReason.BootFailedOnException, innerException); + // Emit an event with EventAggregator that unattended install completed + // Then this event can be listened for and create an unattended user + _eventAggregator.Publish(new UnattendedInstallNotification()); } + } + catch (Exception ex) + { + _logger.LogInformation(ex, "Error during unattended install."); + database?.AbortTransaction(); - return Task.CompletedTask; + var innerException = new UnattendedInstallException( + "The database configuration failed." + + "\n Please check log file for additional information (can be found in '/Umbraco/Data/Logs/')", + ex); + + _runtimeState.Configure(RuntimeLevel.BootFailed, RuntimeLevelReason.BootFailedOnException, innerException); } + + return Task.CompletedTask; } } diff --git a/src/Umbraco.Infrastructure/Install/UnattendedUpgrader.cs b/src/Umbraco.Infrastructure/Install/UnattendedUpgrader.cs index 312106eccace..7b66fdbc5156 100644 --- a/src/Umbraco.Infrastructure/Install/UnattendedUpgrader.cs +++ b/src/Umbraco.Infrastructure/Install/UnattendedUpgrader.cs @@ -1,109 +1,114 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Exceptions; using Umbraco.Cms.Core.Logging; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Migrations; using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Migrations.Upgrade; using Umbraco.Cms.Infrastructure.Runtime; using Umbraco.Extensions; -using Umbraco.Cms.Core; -using Umbraco.Cms.Infrastructure.Migrations; -namespace Umbraco.Cms.Infrastructure.Install +namespace Umbraco.Cms.Infrastructure.Install; + +/// +/// Handles to execute the unattended Umbraco upgrader +/// or the unattended Package migrations runner. +/// +public class UnattendedUpgrader : INotificationAsyncHandler { - /// - /// Handles to execute the unattended Umbraco upgrader - /// or the unattended Package migrations runner. - /// - public class UnattendedUpgrader : INotificationAsyncHandler - { - private readonly IProfilingLogger _profilingLogger; - private readonly IUmbracoVersion _umbracoVersion; - private readonly DatabaseBuilder _databaseBuilder; - private readonly IRuntimeState _runtimeState; - private readonly PackageMigrationRunner _packageMigrationRunner; + private readonly DatabaseBuilder _databaseBuilder; + private readonly PackageMigrationRunner _packageMigrationRunner; + private readonly IProfilingLogger _profilingLogger; + private readonly IRuntimeState _runtimeState; + private readonly IUmbracoVersion _umbracoVersion; - public UnattendedUpgrader( - IProfilingLogger profilingLogger, - IUmbracoVersion umbracoVersion, - DatabaseBuilder databaseBuilder, - IRuntimeState runtimeState, - PackageMigrationRunner packageMigrationRunner) - { - _profilingLogger = profilingLogger ?? throw new ArgumentNullException(nameof(profilingLogger)); - _umbracoVersion = umbracoVersion ?? throw new ArgumentNullException(nameof(umbracoVersion)); - _databaseBuilder = databaseBuilder ?? throw new ArgumentNullException(nameof(databaseBuilder)); - _runtimeState = runtimeState ?? throw new ArgumentNullException(nameof(runtimeState)); - _packageMigrationRunner = packageMigrationRunner; - } + public UnattendedUpgrader( + IProfilingLogger profilingLogger, + IUmbracoVersion umbracoVersion, + DatabaseBuilder databaseBuilder, + IRuntimeState runtimeState, + PackageMigrationRunner packageMigrationRunner) + { + _profilingLogger = profilingLogger ?? throw new ArgumentNullException(nameof(profilingLogger)); + _umbracoVersion = umbracoVersion ?? throw new ArgumentNullException(nameof(umbracoVersion)); + _databaseBuilder = databaseBuilder ?? throw new ArgumentNullException(nameof(databaseBuilder)); + _runtimeState = runtimeState ?? throw new ArgumentNullException(nameof(runtimeState)); + _packageMigrationRunner = packageMigrationRunner; + } - public Task HandleAsync(RuntimeUnattendedUpgradeNotification notification, CancellationToken cancellationToken) + public Task HandleAsync(RuntimeUnattendedUpgradeNotification notification, CancellationToken cancellationToken) + { + if (_runtimeState.RunUnattendedBootLogic()) { - if (_runtimeState.RunUnattendedBootLogic()) + switch (_runtimeState.Reason) { - switch (_runtimeState.Reason) + case RuntimeLevelReason.UpgradeMigrations: { - case RuntimeLevelReason.UpgradeMigrations: + var plan = new UmbracoPlan(_umbracoVersion); + using (_profilingLogger.TraceDuration( + "Starting unattended upgrade.", + "Unattended upgrade completed.")) { - var plan = new UmbracoPlan(_umbracoVersion); - using (_profilingLogger.TraceDuration( - "Starting unattended upgrade.", - "Unattended upgrade completed.")) + DatabaseBuilder.Result? result = _databaseBuilder.UpgradeSchemaAndData(plan); + if (result?.Success == false) { - DatabaseBuilder.Result? result = _databaseBuilder.UpgradeSchemaAndData(plan); - if (result?.Success == false) - { - var innerException = new UnattendedInstallException("An error occurred while running the unattended upgrade.\n" + result.Message); - _runtimeState.Configure(Core.RuntimeLevel.BootFailed, Core.RuntimeLevelReason.BootFailedOnException, innerException); - } - - notification.UnattendedUpgradeResult = RuntimeUnattendedUpgradeNotification.UpgradeResult.CoreUpgradeComplete; + var innerException = new UnattendedInstallException( + "An error occurred while running the unattended upgrade.\n" + result.Message); + _runtimeState.Configure(RuntimeLevel.BootFailed, RuntimeLevelReason.BootFailedOnException, + innerException); } + + notification.UnattendedUpgradeResult = + RuntimeUnattendedUpgradeNotification.UpgradeResult.CoreUpgradeComplete; } + } break; - case RuntimeLevelReason.UpgradePackageMigrations: + case RuntimeLevelReason.UpgradePackageMigrations: + { + if (!_runtimeState.StartupState.TryGetValue(RuntimeState.PendingPacakgeMigrationsStateKey, + out var pm) + || pm is not IReadOnlyList pendingMigrations) { - if (!_runtimeState.StartupState.TryGetValue(RuntimeState.PendingPacakgeMigrationsStateKey, out var pm) - || pm is not IReadOnlyList pendingMigrations) - { - throw new InvalidOperationException($"The required key {RuntimeState.PendingPacakgeMigrationsStateKey} does not exist in startup state"); - } + throw new InvalidOperationException( + $"The required key {RuntimeState.PendingPacakgeMigrationsStateKey} does not exist in startup state"); + } - if (pendingMigrations.Count == 0) - { - throw new InvalidOperationException("No pending migrations found but the runtime level reason is " + Core.RuntimeLevelReason.UpgradePackageMigrations); - } + if (pendingMigrations.Count == 0) + { + throw new InvalidOperationException( + "No pending migrations found but the runtime level reason is " + + RuntimeLevelReason.UpgradePackageMigrations); + } - try - { - IEnumerable result = _packageMigrationRunner.RunPackagePlans(pendingMigrations); - notification.UnattendedUpgradeResult = RuntimeUnattendedUpgradeNotification.UpgradeResult.PackageMigrationComplete; - } - catch (Exception ex ) - { - SetRuntimeError(ex); - notification.UnattendedUpgradeResult = RuntimeUnattendedUpgradeNotification.UpgradeResult.HasErrors; - } + try + { + IEnumerable result = + _packageMigrationRunner.RunPackagePlans(pendingMigrations); + notification.UnattendedUpgradeResult = RuntimeUnattendedUpgradeNotification.UpgradeResult + .PackageMigrationComplete; + } + catch (Exception ex) + { + SetRuntimeError(ex); + notification.UnattendedUpgradeResult = + RuntimeUnattendedUpgradeNotification.UpgradeResult.HasErrors; } - break; - default: - throw new InvalidOperationException("Invalid reason " + _runtimeState.Reason); } + break; + default: + throw new InvalidOperationException("Invalid reason " + _runtimeState.Reason); } - - return Task.CompletedTask; } - private void SetRuntimeError(Exception exception) - => _runtimeState.Configure( - RuntimeLevel.BootFailed, - RuntimeLevelReason.BootFailedOnException, - exception); + return Task.CompletedTask; } + + private void SetRuntimeError(Exception exception) + => _runtimeState.Configure( + RuntimeLevel.BootFailed, + RuntimeLevelReason.BootFailedOnException, + exception); } diff --git a/src/Umbraco.Infrastructure/Logging/MessageTemplates.cs b/src/Umbraco.Infrastructure/Logging/MessageTemplates.cs index 305844d7d647..8f990169f4d3 100644 --- a/src/Umbraco.Infrastructure/Logging/MessageTemplates.cs +++ b/src/Umbraco.Infrastructure/Logging/MessageTemplates.cs @@ -1,52 +1,55 @@ -using System; -using System.IO; -using System.Linq; -using Serilog; +using Serilog; using Serilog.Events; using Serilog.Parsing; -using Umbraco.Cms.Core.Logging; -namespace Umbraco.Cms.Core.Logging +namespace Umbraco.Cms.Core.Logging; + +public class MessageTemplates : IMessageTemplates { - public class MessageTemplates : IMessageTemplates - { - // Umbraco now uses Message Templates (https://messagetemplates.org/) for logging, which means - // we cannot plainly use string.Format() to format them. There is a work-in-progress C# lib, - // derived from Serilog, which should help (https://github.com/messagetemplates/messagetemplates-csharp) - // but it only has a pre-release NuGet package. So, we've got to use Serilog's code, which - // means we cannot get rid of Serilog entirely. We may want to revisit this at some point. + // Umbraco now uses Message Templates (https://messagetemplates.org/) for logging, which means + // we cannot plainly use string.Format() to format them. There is a work-in-progress C# lib, + // derived from Serilog, which should help (https://github.com/messagetemplates/messagetemplates-csharp) + // but it only has a pre-release NuGet package. So, we've got to use Serilog's code, which + // means we cannot get rid of Serilog entirely. We may want to revisit this at some point. - // TODO: Do we still need this, is there a non-pre release package shipped? + // TODO: Do we still need this, is there a non-pre release package shipped? - private static readonly Lazy MinimalLogger = new Lazy(() => new LoggerConfiguration().CreateLogger()); + private static readonly Lazy MinimalLogger = new(() => new LoggerConfiguration().CreateLogger()); - public string Render(string messageTemplate, params object[] args) - { - // resolve a minimal logger instance which is used to bind message templates - var logger = MinimalLogger.Value; + public string Render(string messageTemplate, params object[] args) + { + // resolve a minimal logger instance which is used to bind message templates + ILogger logger = MinimalLogger.Value; - var bound = logger.BindMessageTemplate(messageTemplate, args, out var parsedTemplate, out var boundProperties); + var bound = logger.BindMessageTemplate(messageTemplate, args, out MessageTemplate? parsedTemplate, + out IEnumerable? boundProperties); - if (!bound) - throw new FormatException($"Could not format message \"{messageTemplate}\" with {args.Length} args."); + if (!bound) + { + throw new FormatException($"Could not format message \"{messageTemplate}\" with {args.Length} args."); + } - var values = boundProperties.ToDictionary(x => x.Name, x => x.Value); + var values = boundProperties.ToDictionary(x => x.Name, x => x.Value); - // this ends up putting every string parameter between quotes - //return parsedTemplate.Render(values); + // this ends up putting every string parameter between quotes + //return parsedTemplate.Render(values); - // this does not - var tw = new StringWriter(); - foreach (var t in parsedTemplate.Tokens) + // this does not + var tw = new StringWriter(); + foreach (MessageTemplateToken? t in parsedTemplate.Tokens) + { + if (t is PropertyToken pt && + values.TryGetValue(pt.PropertyName, out LogEventPropertyValue? propVal) && + (propVal as ScalarValue)?.Value is string s) { - if (t is PropertyToken pt && - values.TryGetValue(pt.PropertyName, out var propVal) && - (propVal as ScalarValue)?.Value is string s) - tw.Write(s); - else - t.Render(values, tw); + tw.Write(s); + } + else + { + t.Render(values, tw); } - return tw.ToString(); } + + return tw.ToString(); } } diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestIdEnricher.cs b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestIdEnricher.cs index 3d9182cb9630..6c53562f3239 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestIdEnricher.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestIdEnricher.cs @@ -1,45 +1,47 @@ -using System; -using Serilog.Core; +using Serilog.Core; using Serilog.Events; using Umbraco.Cms.Core.Cache; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Logging.Serilog.Enrichers +namespace Umbraco.Cms.Core.Logging.Serilog.Enrichers; + +/// +/// Enrich log events with a HttpRequestId GUID. +/// Original source - +/// https://github.com/serilog-web/classic/blob/master/src/SerilogWeb.Classic/Classic/Enrichers/HttpRequestIdEnricher.cs +/// Nupkg: 'Serilog.Web.Classic' contains handlers and extra bits we do not want +/// +public class HttpRequestIdEnricher : ILogEventEnricher { /// - /// Enrich log events with a HttpRequestId GUID. - /// Original source - https://github.com/serilog-web/classic/blob/master/src/SerilogWeb.Classic/Classic/Enrichers/HttpRequestIdEnricher.cs - /// Nupkg: 'Serilog.Web.Classic' contains handlers and extra bits we do not want + /// The property name added to enriched log events. /// - public class HttpRequestIdEnricher : ILogEventEnricher - { - private readonly IRequestCache _requestCache; + public const string HttpRequestIdPropertyName = "HttpRequestId"; - public HttpRequestIdEnricher(IRequestCache requestCache) - { - _requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache)); - } + private readonly IRequestCache _requestCache; - /// - /// The property name added to enriched log events. - /// - public const string HttpRequestIdPropertyName = "HttpRequestId"; + public HttpRequestIdEnricher(IRequestCache requestCache) => + _requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache)); - /// - /// Enrich the log event with an id assigned to the currently-executing HTTP request, if any. - /// - /// The log event to enrich. - /// Factory for creating new properties to add to the event. - public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + /// + /// Enrich the log event with an id assigned to the currently-executing HTTP request, if any. + /// + /// The log event to enrich. + /// Factory for creating new properties to add to the event. + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + if (logEvent == null) { - if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); - - Guid? requestId; - if (!LogHttpRequest.TryGetCurrentHttpRequestId(out requestId, _requestCache)) - return; + throw new ArgumentNullException(nameof(logEvent)); + } - var requestIdProperty = new LogEventProperty(HttpRequestIdPropertyName, new ScalarValue(requestId)); - logEvent.AddPropertyIfAbsent(requestIdProperty); + Guid? requestId; + if (!LogHttpRequest.TryGetCurrentHttpRequestId(out requestId, _requestCache)) + { + return; } + + var requestIdProperty = new LogEventProperty(HttpRequestIdPropertyName, new ScalarValue(requestId)); + logEvent.AddPropertyIfAbsent(requestIdProperty); } } diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestNumberEnricher.cs b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestNumberEnricher.cs index ee041f7abb5a..a5f6eaf8fec8 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestNumberEnricher.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestNumberEnricher.cs @@ -1,48 +1,48 @@ -using System; -using System.Threading; -using Serilog.Core; +using Serilog.Core; using Serilog.Events; using Umbraco.Cms.Core.Cache; -namespace Umbraco.Cms.Core.Logging.Serilog.Enrichers +namespace Umbraco.Cms.Core.Logging.Serilog.Enrichers; + +/// +/// Enrich log events with a HttpRequestNumber unique within the current +/// logging session. +/// Original source - +/// https://github.com/serilog-web/classic/blob/master/src/SerilogWeb.Classic/Classic/Enrichers/HttpRequestNumberEnricher.cs +/// Nupkg: 'Serilog.Web.Classic' contains handlers and extra bits we do not want +/// +public class HttpRequestNumberEnricher : ILogEventEnricher { /// - /// Enrich log events with a HttpRequestNumber unique within the current - /// logging session. - /// Original source - https://github.com/serilog-web/classic/blob/master/src/SerilogWeb.Classic/Classic/Enrichers/HttpRequestNumberEnricher.cs - /// Nupkg: 'Serilog.Web.Classic' contains handlers and extra bits we do not want + /// The property name added to enriched log events. /// - public class HttpRequestNumberEnricher : ILogEventEnricher - { - private readonly IRequestCache _requestCache; - private static int _lastRequestNumber; - private static readonly string _requestNumberItemName = typeof(HttpRequestNumberEnricher).Name + "+RequestNumber"; + private const string _httpRequestNumberPropertyName = "HttpRequestNumber"; - /// - /// The property name added to enriched log events. - /// - private const string _httpRequestNumberPropertyName = "HttpRequestNumber"; + private static int _lastRequestNumber; + private static readonly string _requestNumberItemName = typeof(HttpRequestNumberEnricher).Name + "+RequestNumber"; + private readonly IRequestCache _requestCache; - public HttpRequestNumberEnricher(IRequestCache requestCache) - { - _requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache)); - } + public HttpRequestNumberEnricher(IRequestCache requestCache) => + _requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache)); - /// - /// Enrich the log event with the number assigned to the currently-executing HTTP request, if any. - /// - /// The log event to enrich. - /// Factory for creating new properties to add to the event. - public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + /// + /// Enrich the log event with the number assigned to the currently-executing HTTP request, if any. + /// + /// The log event to enrich. + /// Factory for creating new properties to add to the event. + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + if (logEvent == null) { - if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); + throw new ArgumentNullException(nameof(logEvent)); + } - var requestNumber = _requestCache.Get(_requestNumberItemName, - () => Interlocked.Increment(ref _lastRequestNumber)); + var requestNumber = _requestCache.Get(_requestNumberItemName, + () => Interlocked.Increment(ref _lastRequestNumber)); - var requestNumberProperty = new LogEventProperty(_httpRequestNumberPropertyName, new ScalarValue(requestNumber)); - logEvent.AddPropertyIfAbsent(requestNumberProperty); - } + var requestNumberProperty = + new LogEventProperty(_httpRequestNumberPropertyName, new ScalarValue(requestNumber)); + logEvent.AddPropertyIfAbsent(requestNumberProperty); } } diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpSessionIdEnricher.cs b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpSessionIdEnricher.cs index e2b4f59065ed..bb04f440b871 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpSessionIdEnricher.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpSessionIdEnricher.cs @@ -1,43 +1,45 @@ -using System; -using Serilog.Core; +using Serilog.Core; using Serilog.Events; using Umbraco.Cms.Core.Net; -namespace Umbraco.Cms.Core.Logging.Serilog.Enrichers +namespace Umbraco.Cms.Core.Logging.Serilog.Enrichers; + +/// +/// Enrich log events with the HttpSessionId property. +/// Original source - +/// https://github.com/serilog-web/classic/blob/master/src/SerilogWeb.Classic/Classic/Enrichers/HttpSessionIdEnricher.cs +/// Nupkg: 'Serilog.Web.Classic' contains handlers and extra bits we do not want +/// +public class HttpSessionIdEnricher : ILogEventEnricher { /// - /// Enrich log events with the HttpSessionId property. - /// Original source - https://github.com/serilog-web/classic/blob/master/src/SerilogWeb.Classic/Classic/Enrichers/HttpSessionIdEnricher.cs - /// Nupkg: 'Serilog.Web.Classic' contains handlers and extra bits we do not want + /// The property name added to enriched log events. /// - public class HttpSessionIdEnricher : ILogEventEnricher - { - private readonly ISessionIdResolver _sessionIdResolver; + public const string HttpSessionIdPropertyName = "HttpSessionId"; - public HttpSessionIdEnricher(ISessionIdResolver sessionIdResolver) - { - _sessionIdResolver = sessionIdResolver; - } + private readonly ISessionIdResolver _sessionIdResolver; - /// - /// The property name added to enriched log events. - /// - public const string HttpSessionIdPropertyName = "HttpSessionId"; + public HttpSessionIdEnricher(ISessionIdResolver sessionIdResolver) => _sessionIdResolver = sessionIdResolver; - /// - /// Enrich the log event with the current ASP.NET session id, if sessions are enabled. - /// The log event to enrich. - /// Factory for creating new properties to add to the event. - public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + /// + /// Enrich the log event with the current ASP.NET session id, if sessions are enabled. + /// + /// The log event to enrich. + /// Factory for creating new properties to add to the event. + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + if (logEvent == null) { - if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); - - var sessionId = _sessionIdResolver.SessionId; - if (sessionId is null) - return; + throw new ArgumentNullException(nameof(logEvent)); + } - var sessionIdProperty = new LogEventProperty(HttpSessionIdPropertyName, new ScalarValue(sessionId)); - logEvent.AddPropertyIfAbsent(sessionIdProperty); + var sessionId = _sessionIdResolver.SessionId; + if (sessionId is null) + { + return; } + + var sessionIdProperty = new LogEventProperty(HttpSessionIdPropertyName, new ScalarValue(sessionId)); + logEvent.AddPropertyIfAbsent(sessionIdProperty); } } diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/Log4NetLevelMapperEnricher.cs b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/Log4NetLevelMapperEnricher.cs index a9ae85e2f0cb..d203cc4bcd2e 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/Log4NetLevelMapperEnricher.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/Log4NetLevelMapperEnricher.cs @@ -1,49 +1,51 @@ using Serilog.Core; using Serilog.Events; -namespace Umbraco.Cms.Core.Logging.Serilog.Enrichers +namespace Umbraco.Cms.Core.Logging.Serilog.Enrichers; + +/// +/// This is used to create a new property in Logs called 'Log4NetLevel' +/// So that we can map Serilog levels to Log4Net levels - so log files stay consistent +/// +internal class Log4NetLevelMapperEnricher : ILogEventEnricher { - /// - /// This is used to create a new property in Logs called 'Log4NetLevel' - /// So that we can map Serilog levels to Log4Net levels - so log files stay consistent - /// - internal class Log4NetLevelMapperEnricher : ILogEventEnricher + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { - public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + string log4NetLevel; + + switch (logEvent.Level) { - string log4NetLevel; - - switch (logEvent.Level) - { - case LogEventLevel.Debug: - log4NetLevel = "DEBUG"; - break; - - case LogEventLevel.Error: - log4NetLevel = "ERROR"; - break; - - case LogEventLevel.Fatal: - log4NetLevel = "FATAL"; - break; - - case LogEventLevel.Information: - log4NetLevel = "INFO "; //Padded string so that all log levels are 5 chars long (needed to keep the txt log file lined up nicely) - break; - - case LogEventLevel.Verbose: - log4NetLevel = "ALL "; //Padded string so that all log levels are 5 chars long (needed to keep the txt log file lined up nicely) - break; - - case LogEventLevel.Warning: - log4NetLevel = "WARN "; //Padded string so that all log levels are 5 chars long (needed to keep the txt log file lined up nicely) - break; - default: - log4NetLevel = string.Empty; - break; - } - - logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("Log4NetLevel", log4NetLevel)); + case LogEventLevel.Debug: + log4NetLevel = "DEBUG"; + break; + + case LogEventLevel.Error: + log4NetLevel = "ERROR"; + break; + + case LogEventLevel.Fatal: + log4NetLevel = "FATAL"; + break; + + case LogEventLevel.Information: + log4NetLevel = + "INFO "; //Padded string so that all log levels are 5 chars long (needed to keep the txt log file lined up nicely) + break; + + case LogEventLevel.Verbose: + log4NetLevel = + "ALL "; //Padded string so that all log levels are 5 chars long (needed to keep the txt log file lined up nicely) + break; + + case LogEventLevel.Warning: + log4NetLevel = + "WARN "; //Padded string so that all log levels are 5 chars long (needed to keep the txt log file lined up nicely) + break; + default: + log4NetLevel = string.Empty; + break; } + + logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("Log4NetLevel", log4NetLevel)); } } diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/ThreadAbortExceptionEnricher.cs b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/ThreadAbortExceptionEnricher.cs index bfe7c4172bb3..6183857d1e9a 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/ThreadAbortExceptionEnricher.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/ThreadAbortExceptionEnricher.cs @@ -1,100 +1,116 @@ -using System; using System.Reflection; -using System.Threading; using Microsoft.Extensions.Options; using Serilog.Core; using Serilog.Events; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Diagnostics; using Umbraco.Cms.Core.Hosting; -using CoreDebugSettings = Umbraco.Cms.Core.Configuration.Models.CoreDebugSettings; -namespace Umbraco.Cms.Core.Logging.Serilog.Enrichers +namespace Umbraco.Cms.Core.Logging.Serilog.Enrichers; + +/// +/// Enriches the log if there are ThreadAbort exceptions and will automatically create a minidump if it can +/// +public class ThreadAbortExceptionEnricher : ILogEventEnricher { - /// - /// Enriches the log if there are ThreadAbort exceptions and will automatically create a minidump if it can - /// - public class ThreadAbortExceptionEnricher : ILogEventEnricher + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IMarchal _marchal; + private CoreDebugSettings _coreDebugSettings; + + public ThreadAbortExceptionEnricher(IOptionsMonitor coreDebugSettings, + IHostingEnvironment hostingEnvironment, IMarchal marchal) { - private CoreDebugSettings _coreDebugSettings; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly IMarchal _marchal; + _coreDebugSettings = coreDebugSettings.CurrentValue; + _hostingEnvironment = hostingEnvironment; + _marchal = marchal; + coreDebugSettings.OnChange(x => _coreDebugSettings = x); + } - public ThreadAbortExceptionEnricher(IOptionsMonitor coreDebugSettings, IHostingEnvironment hostingEnvironment, IMarchal marchal) + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + switch (logEvent.Level) { - _coreDebugSettings = coreDebugSettings.CurrentValue; - _hostingEnvironment = hostingEnvironment; - _marchal = marchal; - coreDebugSettings.OnChange(x => _coreDebugSettings = x); + case LogEventLevel.Error: + case LogEventLevel.Fatal: + DumpThreadAborts(logEvent, propertyFactory); + break; } + } - public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + private void DumpThreadAborts(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + if (!IsTimeoutThreadAbortException(logEvent.Exception)) { - switch (logEvent.Level) - { - case LogEventLevel.Error: - case LogEventLevel.Fatal: - DumpThreadAborts(logEvent, propertyFactory); - break; - } + return; } - private void DumpThreadAborts(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) - { - if (!IsTimeoutThreadAbortException(logEvent.Exception)) return; - - var message = "The thread has been aborted, because the request has timed out."; + var message = "The thread has been aborted, because the request has timed out."; - // dump if configured, or if stacktrace contains Monitor.ReliableEnter - var dump = _coreDebugSettings.DumpOnTimeoutThreadAbort || IsMonitorEnterThreadAbortException(logEvent.Exception); + // dump if configured, or if stacktrace contains Monitor.ReliableEnter + var dump = _coreDebugSettings.DumpOnTimeoutThreadAbort || + IsMonitorEnterThreadAbortException(logEvent.Exception); - // dump if it is ok to dump (might have a cap on number of dump...) - dump &= MiniDump.OkToDump(_hostingEnvironment); + // dump if it is ok to dump (might have a cap on number of dump...) + dump &= MiniDump.OkToDump(_hostingEnvironment); - if (!dump) + if (!dump) + { + message += ". No minidump was created."; + logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ThreadAbortExceptionInfo", message)); + } + else + { + try { - message += ". No minidump was created."; + var dumped = MiniDump.Dump(_marchal, _hostingEnvironment, withException: true); + message += dumped + ? ". A minidump was created in App_Data/MiniDump." + : ". Failed to create a minidump."; logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ThreadAbortExceptionInfo", message)); } - else + catch (Exception ex) { - try - { - var dumped = MiniDump.Dump(_marchal, _hostingEnvironment, withException: true); - message += dumped - ? ". A minidump was created in App_Data/MiniDump." - : ". Failed to create a minidump."; - logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ThreadAbortExceptionInfo", message)); - } - catch (Exception ex) - { - message = "Failed to create a minidump. " + ex; - logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ThreadAbortExceptionInfo", message)); - } + message = "Failed to create a minidump. " + ex; + logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ThreadAbortExceptionInfo", message)); } } + } - private static bool IsTimeoutThreadAbortException(Exception exception) + private static bool IsTimeoutThreadAbortException(Exception exception) + { + if (!(exception is ThreadAbortException abort)) { - if (!(exception is ThreadAbortException abort)) return false; - if (abort.ExceptionState == null) return false; - - var stateType = abort.ExceptionState.GetType(); - if (stateType.FullName != "System.Web.HttpApplication+CancelModuleException") return false; - - var timeoutField = stateType.GetField("_timeout", BindingFlags.Instance | BindingFlags.NonPublic); - if (timeoutField == null) return false; + return false; + } - return (bool?)timeoutField.GetValue(abort.ExceptionState) ?? false; + if (abort.ExceptionState == null) + { + return false; } - private static bool IsMonitorEnterThreadAbortException(Exception exception) + Type stateType = abort.ExceptionState.GetType(); + if (stateType.FullName != "System.Web.HttpApplication+CancelModuleException") { - if (!(exception is ThreadAbortException abort)) return false; + return false; + } - var stacktrace = abort.StackTrace; - return stacktrace?.Contains("System.Threading.Monitor.ReliableEnter") ?? false; + FieldInfo? timeoutField = stateType.GetField("_timeout", BindingFlags.Instance | BindingFlags.NonPublic); + if (timeoutField == null) + { + return false; } + return (bool?)timeoutField.GetValue(abort.ExceptionState) ?? false; + } + + private static bool IsMonitorEnterThreadAbortException(Exception exception) + { + if (!(exception is ThreadAbortException abort)) + { + return false; + } + var stacktrace = abort.StackTrace; + return stacktrace?.Contains("System.Threading.Monitor.ReliableEnter") ?? false; } } diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs b/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs index 733410cf91df..068b1fc2ca4d 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs @@ -1,11 +1,11 @@ -using System; -using System.IO; +using System.Diagnostics; using System.Text; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Serilog; using Serilog.Configuration; using Serilog.Core; +using Serilog.Debugging; using Serilog.Events; using Serilog.Formatting; using Serilog.Formatting.Compact; @@ -13,207 +13,226 @@ using Umbraco.Cms.Core.Logging; using Umbraco.Cms.Core.Logging.Serilog.Enrichers; using Umbraco.Cms.Infrastructure.Logging.Serilog; +using Constants = Umbraco.Cms.Core.Constants; +using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class LoggerConfigExtensions { - public static class LoggerConfigExtensions + /// + /// This configures Serilog with some defaults + /// Such as adding ProcessID, Thread, AppDomain etc + /// It is highly recommended that you keep/use this default in your own logging config customizations + /// + [Obsolete("Please use an alternative method.")] + public static LoggerConfiguration MinimalConfiguration( + this LoggerConfiguration logConfig, + IHostingEnvironment hostingEnvironment, + ILoggingConfiguration loggingConfiguration, + IConfiguration configuration) => + MinimalConfiguration(logConfig, hostingEnvironment, loggingConfiguration, configuration, out _); + + /// + /// This configures Serilog with some defaults + /// Such as adding ProcessID, Thread, AppDomain etc + /// It is highly recommended that you keep/use this default in your own logging config customizations + /// + [Obsolete("Please use an alternative method.")] + public static LoggerConfiguration MinimalConfiguration( + this LoggerConfiguration logConfig, + IHostingEnvironment hostingEnvironment, + ILoggingConfiguration loggingConfiguration, + IConfiguration configuration, + out UmbracoFileConfiguration umbFileConfiguration) + { + SelfLog.Enable(msg => Debug.WriteLine(msg)); + + //Set this environment variable - so that it can be used in external config file + //add key="serilog:write-to:RollingFile.pathFormat" value="%BASEDIR%\logs\log.txt" /> + Environment.SetEnvironmentVariable("BASEDIR", + hostingEnvironment.MapPathContentRoot("/").TrimEnd(Path.DirectorySeparatorChar), + EnvironmentVariableTarget.Process); + Environment.SetEnvironmentVariable("UMBLOGDIR", loggingConfiguration.LogDirectory, + EnvironmentVariableTarget.Process); + Environment.SetEnvironmentVariable("MACHINENAME", Environment.MachineName, EnvironmentVariableTarget.Process); + + logConfig.MinimumLevel + .Verbose() //Set to highest level of logging (as any sinks may want to restrict it to Errors only) + .Enrich.WithProcessId() + .Enrich.WithProcessName() + .Enrich.WithThreadId() + .Enrich + .WithProperty("ApplicationId", hostingEnvironment.ApplicationId) // Updated later by ApplicationIdEnricher + .Enrich.WithProperty("MachineName", Environment.MachineName) + .Enrich.With() + .Enrich.FromLogContext(); // allows us to dynamically enrich + + //This is not optimal, but seems to be the only way if we do not make an Serilog.Sink.UmbracoFile sink all the way. + var umbracoFileConfiguration = new UmbracoFileConfiguration(configuration); + + umbFileConfiguration = umbracoFileConfiguration; + + logConfig.WriteTo.UmbracoFile( + umbracoFileConfiguration.GetPath(loggingConfiguration.LogDirectory), + fileSizeLimitBytes: umbracoFileConfiguration.FileSizeLimitBytes, + restrictedToMinimumLevel: umbracoFileConfiguration.RestrictedToMinimumLevel, + rollingInterval: umbracoFileConfiguration.RollingInterval, + flushToDiskInterval: umbracoFileConfiguration.FlushToDiskInterval, + rollOnFileSizeLimit: umbracoFileConfiguration.RollOnFileSizeLimit, + retainedFileCountLimit: umbracoFileConfiguration.RetainedFileCountLimit + ); + + return logConfig; + } + + + /// + /// This configures Serilog with some defaults + /// Such as adding ProcessID, Thread, AppDomain etc + /// It is highly recommended that you keep/use this default in your own logging config customizations + /// + public static LoggerConfiguration MinimalConfiguration( + this LoggerConfiguration logConfig, + IHostEnvironment hostEnvironment, + ILoggingConfiguration loggingConfiguration, + UmbracoFileConfiguration umbracoFileConfiguration) { - /// - /// This configures Serilog with some defaults - /// Such as adding ProcessID, Thread, AppDomain etc - /// It is highly recommended that you keep/use this default in your own logging config customizations - /// - [Obsolete("Please use an alternative method.")] - public static LoggerConfiguration MinimalConfiguration( - this LoggerConfiguration logConfig, - Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment, - ILoggingConfiguration loggingConfiguration, - IConfiguration configuration) - { - return MinimalConfiguration(logConfig, hostingEnvironment, loggingConfiguration, configuration, out _); - } - - /// - /// This configures Serilog with some defaults - /// Such as adding ProcessID, Thread, AppDomain etc - /// It is highly recommended that you keep/use this default in your own logging config customizations - /// - [Obsolete("Please use an alternative method.")] - public static LoggerConfiguration MinimalConfiguration( - this LoggerConfiguration logConfig, - Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment, - ILoggingConfiguration loggingConfiguration, - IConfiguration configuration, - out UmbracoFileConfiguration umbFileConfiguration) - { - global::Serilog.Debugging.SelfLog.Enable(msg => System.Diagnostics.Debug.WriteLine(msg)); - - //Set this environment variable - so that it can be used in external config file - //add key="serilog:write-to:RollingFile.pathFormat" value="%BASEDIR%\logs\log.txt" /> - Environment.SetEnvironmentVariable("BASEDIR", hostingEnvironment.MapPathContentRoot("/").TrimEnd(Path.DirectorySeparatorChar), EnvironmentVariableTarget.Process); - Environment.SetEnvironmentVariable("UMBLOGDIR", loggingConfiguration.LogDirectory, EnvironmentVariableTarget.Process); - Environment.SetEnvironmentVariable("MACHINENAME", Environment.MachineName, EnvironmentVariableTarget.Process); - - logConfig.MinimumLevel.Verbose() //Set to highest level of logging (as any sinks may want to restrict it to Errors only) - .Enrich.WithProcessId() - .Enrich.WithProcessName() - .Enrich.WithThreadId() - .Enrich.WithProperty("ApplicationId", hostingEnvironment.ApplicationId) // Updated later by ApplicationIdEnricher - .Enrich.WithProperty("MachineName", Environment.MachineName) - .Enrich.With() - .Enrich.FromLogContext(); // allows us to dynamically enrich - - //This is not optimal, but seems to be the only way if we do not make an Serilog.Sink.UmbracoFile sink all the way. - var umbracoFileConfiguration = new UmbracoFileConfiguration(configuration); - - umbFileConfiguration = umbracoFileConfiguration; - - logConfig.WriteTo.UmbracoFile( - path : umbracoFileConfiguration.GetPath(loggingConfiguration.LogDirectory), - fileSizeLimitBytes: umbracoFileConfiguration.FileSizeLimitBytes, - restrictedToMinimumLevel: umbracoFileConfiguration.RestrictedToMinimumLevel, - rollingInterval: umbracoFileConfiguration.RollingInterval, - flushToDiskInterval: umbracoFileConfiguration.FlushToDiskInterval, - rollOnFileSizeLimit: umbracoFileConfiguration.RollOnFileSizeLimit, - retainedFileCountLimit: umbracoFileConfiguration.RetainedFileCountLimit - ); - - return logConfig; - } - - - /// - /// This configures Serilog with some defaults - /// Such as adding ProcessID, Thread, AppDomain etc - /// It is highly recommended that you keep/use this default in your own logging config customizations - /// - public static LoggerConfiguration MinimalConfiguration( - this LoggerConfiguration logConfig, - IHostEnvironment hostEnvironment, - ILoggingConfiguration loggingConfiguration, - UmbracoFileConfiguration umbracoFileConfiguration) - { - global::Serilog.Debugging.SelfLog.Enable(msg => System.Diagnostics.Debug.WriteLine(msg)); - - //Set this environment variable - so that it can be used in external config file - //add key="serilog:write-to:RollingFile.pathFormat" value="%BASEDIR%\logs\log.txt" /> - Environment.SetEnvironmentVariable("BASEDIR", hostEnvironment.MapPathContentRoot("/").TrimEnd("\\"), EnvironmentVariableTarget.Process); - Environment.SetEnvironmentVariable("UMBLOGDIR", loggingConfiguration.LogDirectory, EnvironmentVariableTarget.Process); - Environment.SetEnvironmentVariable("MACHINENAME", Environment.MachineName, EnvironmentVariableTarget.Process); - - logConfig.MinimumLevel.Verbose() //Set to highest level of logging (as any sinks may want to restrict it to Errors only) - .Enrich.WithProcessId() - .Enrich.WithProcessName() - .Enrich.WithThreadId() - .Enrich.WithProperty("ApplicationId", hostEnvironment.GetTemporaryApplicationId()) // Updated later by ApplicationIdEnricher - .Enrich.WithProperty("MachineName", Environment.MachineName) - .Enrich.With() - .Enrich.FromLogContext(); // allows us to dynamically enrich - - logConfig.WriteTo.UmbracoFile( - path: umbracoFileConfiguration.GetPath(loggingConfiguration.LogDirectory), - fileSizeLimitBytes: umbracoFileConfiguration.FileSizeLimitBytes, - restrictedToMinimumLevel: umbracoFileConfiguration.RestrictedToMinimumLevel, - rollingInterval: umbracoFileConfiguration.RollingInterval, - flushToDiskInterval: umbracoFileConfiguration.FlushToDiskInterval, - rollOnFileSizeLimit: umbracoFileConfiguration.RollOnFileSizeLimit, - retainedFileCountLimit: umbracoFileConfiguration.RetainedFileCountLimit - ); - - return logConfig; - } - - /// - /// Outputs a .txt format log at /App_Data/Logs/ - /// - /// A Serilog LoggerConfiguration - /// - /// The log level you wish the JSON file to collect - default is Verbose (highest) - /// The number of days to keep log files. Default is set to null which means all logs are kept - public static LoggerConfiguration OutputDefaultTextFile( - this LoggerConfiguration logConfig, - Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment, - LogEventLevel minimumLevel = LogEventLevel.Verbose) - { - //Main .txt logfile - in similar format to older Log4Net output - //Ends with ..txt as Date is inserted before file extension substring - logConfig.WriteTo.File(Path.Combine(hostingEnvironment.MapPathContentRoot(Cms.Core.Constants.SystemDirectories.LogFiles), $"UmbracoTraceLog.{Environment.MachineName}..txt"), - shared: true, - rollingInterval: RollingInterval.Day, - restrictedToMinimumLevel: minimumLevel, - retainedFileCountLimit: null, //Setting to null means we keep all files - default is 31 days - outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss,fff} [P{ProcessId}/D{AppDomainId}/T{ThreadId}] {Log4NetLevel} {SourceContext} - {Message:lj}{NewLine}{Exception}"); - - return logConfig; - } - - - /// - /// Used in config - If renamed or moved to other assembly the config file also has be updated. - /// - public static LoggerConfiguration UmbracoFile( - this LoggerSinkConfiguration configuration, - string path, - ITextFormatter? formatter = null, - LogEventLevel restrictedToMinimumLevel = LogEventLevel.Verbose, - LoggingLevelSwitch? levelSwitch = null, - long? fileSizeLimitBytes = 1073741824, - TimeSpan? flushToDiskInterval = null, - RollingInterval rollingInterval = RollingInterval.Day, - bool rollOnFileSizeLimit = false, - int? retainedFileCountLimit = 31, - Encoding? encoding = null - ) - { - formatter ??= new CompactJsonFormatter(); - - /* Async sink has an event buffer of 10k events (by default) so we're not constantly thrashing the disk. - * I noticed that with File buffered + large number of log entries (global minimum Debug) - * an ungraceful shutdown would consistently result in output that just stops halfway through an entry. - * with buffered false on the inner sink ungraceful shutdowns still don't seem to wreck the file. - */ - return configuration.Async( - cfg => - cfg.File( - formatter, - path, - restrictedToMinimumLevel, - fileSizeLimitBytes, - levelSwitch, - buffered: false, // see notes above. - shared: false, - flushToDiskInterval, - rollingInterval, - rollOnFileSizeLimit, - retainedFileCountLimit, - encoding, - null)); - } - - - /// - /// Outputs a CLEF format JSON log at /App_Data/Logs/ - /// - /// A Serilog LoggerConfiguration - /// The logging configuration - /// The log level you wish the JSON file to collect - default is Verbose (highest) - /// The number of days to keep log files. Default is set to null which means all logs are kept - public static LoggerConfiguration OutputDefaultJsonFile( - this LoggerConfiguration logConfig, - Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment, - ILoggingConfiguration loggingConfiguration, LogEventLevel minimumLevel = LogEventLevel.Verbose, int? retainedFileCount = null) - { - // .clef format (Compact log event format, that can be imported into local SEQ & will make searching/filtering logs easier) - // Ends with ..txt as Date is inserted before file extension substring - logConfig.WriteTo.File(new CompactJsonFormatter(), - Path.Combine(hostingEnvironment.MapPathContentRoot(Cms.Core.Constants.SystemDirectories.LogFiles) ,$"UmbracoTraceLog.{Environment.MachineName}..json"), - shared: true, - rollingInterval: RollingInterval.Day, // Create a new JSON file every day - retainedFileCountLimit: retainedFileCount, // Setting to null means we keep all files - default is 31 days - restrictedToMinimumLevel: minimumLevel); - - return logConfig; - } + SelfLog.Enable(msg => Debug.WriteLine(msg)); + + //Set this environment variable - so that it can be used in external config file + //add key="serilog:write-to:RollingFile.pathFormat" value="%BASEDIR%\logs\log.txt" /> + Environment.SetEnvironmentVariable("BASEDIR", hostEnvironment.MapPathContentRoot("/").TrimEnd("\\"), + EnvironmentVariableTarget.Process); + Environment.SetEnvironmentVariable("UMBLOGDIR", loggingConfiguration.LogDirectory, + EnvironmentVariableTarget.Process); + Environment.SetEnvironmentVariable("MACHINENAME", Environment.MachineName, EnvironmentVariableTarget.Process); + + logConfig.MinimumLevel + .Verbose() //Set to highest level of logging (as any sinks may want to restrict it to Errors only) + .Enrich.WithProcessId() + .Enrich.WithProcessName() + .Enrich.WithThreadId() + .Enrich + .WithProperty("ApplicationId", + hostEnvironment.GetTemporaryApplicationId()) // Updated later by ApplicationIdEnricher + .Enrich.WithProperty("MachineName", Environment.MachineName) + .Enrich.With() + .Enrich.FromLogContext(); // allows us to dynamically enrich + + logConfig.WriteTo.UmbracoFile( + umbracoFileConfiguration.GetPath(loggingConfiguration.LogDirectory), + fileSizeLimitBytes: umbracoFileConfiguration.FileSizeLimitBytes, + restrictedToMinimumLevel: umbracoFileConfiguration.RestrictedToMinimumLevel, + rollingInterval: umbracoFileConfiguration.RollingInterval, + flushToDiskInterval: umbracoFileConfiguration.FlushToDiskInterval, + rollOnFileSizeLimit: umbracoFileConfiguration.RollOnFileSizeLimit, + retainedFileCountLimit: umbracoFileConfiguration.RetainedFileCountLimit + ); + + return logConfig; + } + /// + /// Outputs a .txt format log at /App_Data/Logs/ + /// + /// A Serilog LoggerConfiguration + /// + /// The log level you wish the JSON file to collect - default is Verbose (highest) + /// + /// The number of days to keep log files. Default is set to null which means all logs are + /// kept + /// + public static LoggerConfiguration OutputDefaultTextFile( + this LoggerConfiguration logConfig, + IHostingEnvironment hostingEnvironment, + LogEventLevel minimumLevel = LogEventLevel.Verbose) + { + //Main .txt logfile - in similar format to older Log4Net output + //Ends with ..txt as Date is inserted before file extension substring + logConfig.WriteTo.File( + Path.Combine(hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.LogFiles), + $"UmbracoTraceLog.{Environment.MachineName}..txt"), + shared: true, + rollingInterval: RollingInterval.Day, + restrictedToMinimumLevel: minimumLevel, + retainedFileCountLimit: null, //Setting to null means we keep all files - default is 31 days + outputTemplate: + "{Timestamp:yyyy-MM-dd HH:mm:ss,fff} [P{ProcessId}/D{AppDomainId}/T{ThreadId}] {Log4NetLevel} {SourceContext} - {Message:lj}{NewLine}{Exception}"); + + return logConfig; + } + + + /// + /// Used in config - If renamed or moved to other assembly the config file also has be updated. + /// + public static LoggerConfiguration UmbracoFile( + this LoggerSinkConfiguration configuration, + string path, + ITextFormatter? formatter = null, + LogEventLevel restrictedToMinimumLevel = LogEventLevel.Verbose, + LoggingLevelSwitch? levelSwitch = null, + long? fileSizeLimitBytes = 1073741824, + TimeSpan? flushToDiskInterval = null, + RollingInterval rollingInterval = RollingInterval.Day, + bool rollOnFileSizeLimit = false, + int? retainedFileCountLimit = 31, + Encoding? encoding = null + ) + { + formatter ??= new CompactJsonFormatter(); + + /* Async sink has an event buffer of 10k events (by default) so we're not constantly thrashing the disk. + * I noticed that with File buffered + large number of log entries (global minimum Debug) + * an ungraceful shutdown would consistently result in output that just stops halfway through an entry. + * with buffered false on the inner sink ungraceful shutdowns still don't seem to wreck the file. + */ + return configuration.Async( + cfg => + cfg.File( + formatter, + path, + restrictedToMinimumLevel, + fileSizeLimitBytes, + levelSwitch, + false, // see notes above. + false, + flushToDiskInterval, + rollingInterval, + rollOnFileSizeLimit, + retainedFileCountLimit, + encoding, + null)); + } + + + /// + /// Outputs a CLEF format JSON log at /App_Data/Logs/ + /// + /// A Serilog LoggerConfiguration + /// The logging configuration + /// The log level you wish the JSON file to collect - default is Verbose (highest) + /// + /// The number of days to keep log files. Default is set to null which means all logs are + /// kept + /// + public static LoggerConfiguration OutputDefaultJsonFile( + this LoggerConfiguration logConfig, + IHostingEnvironment hostingEnvironment, + ILoggingConfiguration loggingConfiguration, LogEventLevel minimumLevel = LogEventLevel.Verbose, + int? retainedFileCount = null) + { + // .clef format (Compact log event format, that can be imported into local SEQ & will make searching/filtering logs easier) + // Ends with ..txt as Date is inserted before file extension substring + logConfig.WriteTo.File(new CompactJsonFormatter(), + Path.Combine(hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.LogFiles), + $"UmbracoTraceLog.{Environment.MachineName}..json"), + shared: true, + rollingInterval: RollingInterval.Day, // Create a new JSON file every day + retainedFileCountLimit: retainedFileCount, // Setting to null means we keep all files - default is 31 days + restrictedToMinimumLevel: minimumLevel); + + return logConfig; } } diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs b/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs index 2ca43efd0ca4..a4a57279cd29 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs @@ -1,219 +1,175 @@ -using System; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; using Serilog; using Serilog.Events; +using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Infrastructure.Logging.Serilog; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Logging.Serilog +namespace Umbraco.Cms.Core.Logging.Serilog; + +/// +/// Implements MS ILogger on top of Serilog. +/// +public class SerilogLogger : IDisposable { - /// - /// Implements MS ILogger on top of Serilog. - /// - public class SerilogLogger : IDisposable + public SerilogLogger(LoggerConfiguration logConfig) => + //Configure Serilog static global logger with config passed in + SerilogLog = logConfig.CreateLogger(); + + public ILogger SerilogLog { get; } + + public void Dispose() => SerilogLog.DisposeIfDisposable(); + + [Obsolete] + public static SerilogLogger CreateWithDefaultConfiguration( + IHostingEnvironment hostingEnvironment, + ILoggingConfiguration loggingConfiguration, + IConfiguration configuration) => + CreateWithDefaultConfiguration(hostingEnvironment, loggingConfiguration, configuration, out _); + + /// + /// Creates a logger with some pre-defined configuration and remainder from config file + /// + /// Used by UmbracoApplicationBase to get its logger. + [Obsolete] + public static SerilogLogger CreateWithDefaultConfiguration( + IHostingEnvironment hostingEnvironment, + ILoggingConfiguration loggingConfiguration, + IConfiguration configuration, + out UmbracoFileConfiguration umbracoFileConfig) { - public global::Serilog.ILogger SerilogLog { get; } - - public SerilogLogger(LoggerConfiguration logConfig) - { - //Configure Serilog static global logger with config passed in - SerilogLog = logConfig.CreateLogger(); - } - - [Obsolete] - public static SerilogLogger CreateWithDefaultConfiguration( - Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment, - ILoggingConfiguration loggingConfiguration, - IConfiguration configuration) - { - return CreateWithDefaultConfiguration(hostingEnvironment, loggingConfiguration, configuration, out _); - } - - /// - /// Creates a logger with some pre-defined configuration and remainder from config file - /// - /// Used by UmbracoApplicationBase to get its logger. - [Obsolete] - public static SerilogLogger CreateWithDefaultConfiguration( - Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment, - ILoggingConfiguration loggingConfiguration, - IConfiguration configuration, - out UmbracoFileConfiguration umbracoFileConfig) - { - var serilogConfig = new LoggerConfiguration() - .MinimalConfiguration(hostingEnvironment, loggingConfiguration, configuration, out umbracoFileConfig) - .ReadFrom.Configuration(configuration); + LoggerConfiguration? serilogConfig = new LoggerConfiguration() + .MinimalConfiguration(hostingEnvironment, loggingConfiguration, configuration, out umbracoFileConfig) + .ReadFrom.Configuration(configuration); - return new SerilogLogger(serilogConfig); - } - - /// - /// Gets a contextualized logger. - /// - private global::Serilog.ILogger LoggerFor(Type reporting) - => SerilogLog.ForContext(reporting); - - /// - /// Maps Umbraco's log level to Serilog's. - /// - private LogEventLevel MapLevel(LogLevel level) - { - switch (level) - { - case LogLevel.Debug: - return LogEventLevel.Debug; - case LogLevel.Error: - return LogEventLevel.Error; - case LogLevel.Fatal: - return LogEventLevel.Fatal; - case LogLevel.Information: - return LogEventLevel.Information; - case LogLevel.Verbose: - return LogEventLevel.Verbose; - case LogLevel.Warning: - return LogEventLevel.Warning; - } - - throw new NotSupportedException($"LogLevel \"{level}\" is not supported."); - } + return new SerilogLogger(serilogConfig); + } - /// - public bool IsEnabled(Type reporting, LogLevel level) - => LoggerFor(reporting).IsEnabled(MapLevel(level)); + /// + /// Gets a contextualized logger. + /// + private ILogger LoggerFor(Type reporting) + => SerilogLog.ForContext(reporting); - /// - public void Fatal(Type reporting, Exception exception, string message) - { - var logger = LoggerFor(reporting); - logger.Fatal(exception, message); - } + /// + /// Maps Umbraco's log level to Serilog's. + /// + private LogEventLevel MapLevel(LogLevel level) + { + switch (level) + { + case LogLevel.Debug: + return LogEventLevel.Debug; + case LogLevel.Error: + return LogEventLevel.Error; + case LogLevel.Fatal: + return LogEventLevel.Fatal; + case LogLevel.Information: + return LogEventLevel.Information; + case LogLevel.Verbose: + return LogEventLevel.Verbose; + case LogLevel.Warning: + return LogEventLevel.Warning; + } + + throw new NotSupportedException($"LogLevel \"{level}\" is not supported."); + } - /// - public void Fatal(Type reporting, Exception exception) - { - var logger = LoggerFor(reporting); - var message = "Exception."; - logger.Fatal(exception, message); - } + /// + public bool IsEnabled(Type reporting, LogLevel level) + => LoggerFor(reporting).IsEnabled(MapLevel(level)); - /// - public void Fatal(Type reporting, string message) - { - LoggerFor(reporting).Fatal(message); - } + /// + public void Fatal(Type reporting, Exception exception, string message) + { + ILogger logger = LoggerFor(reporting); + logger.Fatal(exception, message); + } - /// - public void Fatal(Type reporting, string messageTemplate, params object[] propertyValues) - { - LoggerFor(reporting).Fatal(messageTemplate, propertyValues); - } + /// + public void Fatal(Type reporting, Exception exception) + { + ILogger logger = LoggerFor(reporting); + var message = "Exception."; + logger.Fatal(exception, message); + } - /// - public void Fatal(Type reporting, Exception exception, string messageTemplate, params object[] propertyValues) - { - var logger = LoggerFor(reporting); - logger.Fatal(exception, messageTemplate, propertyValues); - } + /// + public void Fatal(Type reporting, string message) => LoggerFor(reporting).Fatal(message); - /// - public void Error(Type reporting, Exception exception, string message) - { - var logger = LoggerFor(reporting); - logger.Error(exception, message); - } + /// + public void Fatal(Type reporting, string messageTemplate, params object[] propertyValues) => + LoggerFor(reporting).Fatal(messageTemplate, propertyValues); - /// - public void Error(Type reporting, Exception exception) - { - var logger = LoggerFor(reporting); - var message = "Exception"; - logger.Error(exception, message); - } + /// + public void Fatal(Type reporting, Exception exception, string messageTemplate, params object[] propertyValues) + { + ILogger logger = LoggerFor(reporting); + logger.Fatal(exception, messageTemplate, propertyValues); + } - /// - public void Error(Type reporting, string message) - { - LoggerFor(reporting).Error(message); - } + /// + public void Error(Type reporting, Exception exception, string message) + { + ILogger logger = LoggerFor(reporting); + logger.Error(exception, message); + } - /// - public void Error(Type reporting, string messageTemplate, params object[] propertyValues) - { - LoggerFor(reporting).Error(messageTemplate, propertyValues); - } + /// + public void Error(Type reporting, Exception exception) + { + ILogger logger = LoggerFor(reporting); + var message = "Exception"; + logger.Error(exception, message); + } - /// - public void Error(Type reporting, Exception exception, string messageTemplate, params object[] propertyValues) - { - var logger = LoggerFor(reporting); - logger.Error(exception, messageTemplate, propertyValues); - } + /// + public void Error(Type reporting, string message) => LoggerFor(reporting).Error(message); - /// - public void Warn(Type reporting, string message) - { - LoggerFor(reporting).Warning(message); - } + /// + public void Error(Type reporting, string messageTemplate, params object[] propertyValues) => + LoggerFor(reporting).Error(messageTemplate, propertyValues); - /// - public void Warn(Type reporting, string message, params object[] propertyValues) - { - LoggerFor(reporting).Warning(message, propertyValues); - } + /// + public void Error(Type reporting, Exception exception, string messageTemplate, params object[] propertyValues) + { + ILogger logger = LoggerFor(reporting); + logger.Error(exception, messageTemplate, propertyValues); + } - /// - public void Warn(Type reporting, Exception exception, string message) - { - LoggerFor(reporting).Warning(exception, message); - } + /// + public void Warn(Type reporting, string message) => LoggerFor(reporting).Warning(message); - /// - public void Warn(Type reporting, Exception exception, string messageTemplate, params object[] propertyValues) - { - LoggerFor(reporting).Warning(exception, messageTemplate, propertyValues); - } + /// + public void Warn(Type reporting, string message, params object[] propertyValues) => + LoggerFor(reporting).Warning(message, propertyValues); - /// - public void Info(Type reporting, string message) - { - LoggerFor(reporting).Information(message); - } + /// + public void Warn(Type reporting, Exception exception, string message) => + LoggerFor(reporting).Warning(exception, message); - /// - public void Info(Type reporting, string messageTemplate, params object[] propertyValues) - { - LoggerFor(reporting).Information(messageTemplate, propertyValues); - } + /// + public void Warn(Type reporting, Exception exception, string messageTemplate, params object[] propertyValues) => + LoggerFor(reporting).Warning(exception, messageTemplate, propertyValues); - /// - public void Debug(Type reporting, string message) - { - LoggerFor(reporting).Debug(message); - } + /// + public void Info(Type reporting, string message) => LoggerFor(reporting).Information(message); - /// - public void Debug(Type reporting, string messageTemplate, params object[] propertyValues) - { - LoggerFor(reporting).Debug(messageTemplate, propertyValues); - } + /// + public void Info(Type reporting, string messageTemplate, params object[] propertyValues) => + LoggerFor(reporting).Information(messageTemplate, propertyValues); - /// - public void Verbose(Type reporting, string message) - { - LoggerFor(reporting).Verbose(message); - } + /// + public void Debug(Type reporting, string message) => LoggerFor(reporting).Debug(message); - /// - public void Verbose(Type reporting, string messageTemplate, params object[] propertyValues) - { - LoggerFor(reporting).Verbose(messageTemplate, propertyValues); - } + /// + public void Debug(Type reporting, string messageTemplate, params object[] propertyValues) => + LoggerFor(reporting).Debug(messageTemplate, propertyValues); - public void Dispose() - { - SerilogLog.DisposeIfDisposable(); - } + /// + public void Verbose(Type reporting, string message) => LoggerFor(reporting).Verbose(message); - } + /// + public void Verbose(Type reporting, string messageTemplate, params object[] propertyValues) => + LoggerFor(reporting).Verbose(messageTemplate, propertyValues); } diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/UmbracoFileConfiguration.cs b/src/Umbraco.Infrastructure/Logging/Serilog/UmbracoFileConfiguration.cs index f306416cf2b6..206f138c6ae4 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/UmbracoFileConfiguration.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/UmbracoFileConfiguration.cs @@ -1,47 +1,42 @@ -using System; -using System.IO; -using System.Linq; using Microsoft.Extensions.Configuration; using Serilog; using Serilog.Events; -namespace Umbraco.Cms.Infrastructure.Logging.Serilog +namespace Umbraco.Cms.Infrastructure.Logging.Serilog; + +public class UmbracoFileConfiguration { - public class UmbracoFileConfiguration + public UmbracoFileConfiguration(IConfiguration configuration) { - public UmbracoFileConfiguration(IConfiguration configuration) + if (configuration == null) { - if (configuration == null) - { - throw new ArgumentNullException(nameof(configuration)); - } - - var appSettings = configuration.GetSection("Serilog:WriteTo"); - var umbracoFileAppSettings = appSettings.GetChildren().LastOrDefault(x => x.GetValue("Name") == "UmbracoFile"); - - if (umbracoFileAppSettings is not null) - { - var args = umbracoFileAppSettings.GetSection("Args"); - - RestrictedToMinimumLevel = args.GetValue(nameof(RestrictedToMinimumLevel), RestrictedToMinimumLevel); - FileSizeLimitBytes = args.GetValue(nameof(FileSizeLimitBytes), FileSizeLimitBytes); - RollingInterval = args.GetValue(nameof(RollingInterval), RollingInterval); - FlushToDiskInterval = args.GetValue(nameof(FlushToDiskInterval), FlushToDiskInterval); - RollOnFileSizeLimit = args.GetValue(nameof(RollOnFileSizeLimit), RollOnFileSizeLimit); - RetainedFileCountLimit = args.GetValue(nameof(RetainedFileCountLimit), RetainedFileCountLimit); - } + throw new ArgumentNullException(nameof(configuration)); } - public LogEventLevel RestrictedToMinimumLevel { get; set; } = LogEventLevel.Verbose; - public long FileSizeLimitBytes { get; set; } = 1073741824; - public RollingInterval RollingInterval { get; set; } = RollingInterval.Day; - public TimeSpan? FlushToDiskInterval { get; set; } = null; - public bool RollOnFileSizeLimit { get; set; } = false; - public int RetainedFileCountLimit { get; set; } = 31; + IConfigurationSection? appSettings = configuration.GetSection("Serilog:WriteTo"); + IConfigurationSection? umbracoFileAppSettings = + appSettings.GetChildren().LastOrDefault(x => x.GetValue("Name") == "UmbracoFile"); - public string GetPath(string logDirectory) + if (umbracoFileAppSettings is not null) { - return Path.Combine(logDirectory, $"UmbracoTraceLog.{Environment.MachineName}..json"); + IConfigurationSection? args = umbracoFileAppSettings.GetSection("Args"); + + RestrictedToMinimumLevel = args.GetValue(nameof(RestrictedToMinimumLevel), RestrictedToMinimumLevel); + FileSizeLimitBytes = args.GetValue(nameof(FileSizeLimitBytes), FileSizeLimitBytes); + RollingInterval = args.GetValue(nameof(RollingInterval), RollingInterval); + FlushToDiskInterval = args.GetValue(nameof(FlushToDiskInterval), FlushToDiskInterval); + RollOnFileSizeLimit = args.GetValue(nameof(RollOnFileSizeLimit), RollOnFileSizeLimit); + RetainedFileCountLimit = args.GetValue(nameof(RetainedFileCountLimit), RetainedFileCountLimit); } } + + public LogEventLevel RestrictedToMinimumLevel { get; set; } = LogEventLevel.Verbose; + public long FileSizeLimitBytes { get; set; } = 1073741824; + public RollingInterval RollingInterval { get; set; } = RollingInterval.Day; + public TimeSpan? FlushToDiskInterval { get; set; } + public bool RollOnFileSizeLimit { get; set; } + public int RetainedFileCountLimit { get; set; } = 31; + + public string GetPath(string logDirectory) => + Path.Combine(logDirectory, $"UmbracoTraceLog.{Environment.MachineName}..json"); } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/CountingFilter.cs b/src/Umbraco.Infrastructure/Logging/Viewer/CountingFilter.cs index 36d12dee0de2..b123ba3d8594 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/CountingFilter.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/CountingFilter.cs @@ -1,49 +1,43 @@ -using System; -using Serilog.Events; +using Serilog.Events; -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +internal class CountingFilter : ILogFilter { - internal class CountingFilter : ILogFilter - { - public CountingFilter() - { - Counts = new LogLevelCounts(); - } + public CountingFilter() => Counts = new LogLevelCounts(); - public LogLevelCounts Counts { get; } + public LogLevelCounts Counts { get; } - public bool TakeLogEvent(LogEvent e) + public bool TakeLogEvent(LogEvent e) + { + switch (e.Level) { - - switch (e.Level) - { - case LogEventLevel.Debug: - Counts.Debug++; - break; - - case LogEventLevel.Information: - Counts.Information++; - break; - - case LogEventLevel.Warning: - Counts.Warning++; - break; - - case LogEventLevel.Error: - Counts.Error++; - break; - - case LogEventLevel.Fatal: - Counts.Fatal++; - break; - case LogEventLevel.Verbose: - break; - default: - throw new ArgumentOutOfRangeException(); - } - - //Don't add it to the list - return false; + case LogEventLevel.Debug: + Counts.Debug++; + break; + + case LogEventLevel.Information: + Counts.Information++; + break; + + case LogEventLevel.Warning: + Counts.Warning++; + break; + + case LogEventLevel.Error: + Counts.Error++; + break; + + case LogEventLevel.Fatal: + Counts.Fatal++; + break; + case LogEventLevel.Verbose: + break; + default: + throw new ArgumentOutOfRangeException(); } + + //Don't add it to the list + return false; } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/ErrorCounterFilter.cs b/src/Umbraco.Infrastructure/Logging/Viewer/ErrorCounterFilter.cs index 1a4ececff682..befc1726f809 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/ErrorCounterFilter.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/ErrorCounterFilter.cs @@ -1,18 +1,19 @@ using Serilog.Events; -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +internal class ErrorCounterFilter : ILogFilter { - internal class ErrorCounterFilter : ILogFilter - { - public int Count { get; private set; } + public int Count { get; private set; } - public bool TakeLogEvent(LogEvent e) + public bool TakeLogEvent(LogEvent e) + { + if (e.Level == LogEventLevel.Fatal || e.Level == LogEventLevel.Error || e.Exception != null) { - if (e.Level == LogEventLevel.Fatal || e.Level == LogEventLevel.Error || e.Exception != null) - Count++; - - //Don't add it to the list - return false; + Count++; } + + //Don't add it to the list + return false; } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/ExpressionFilter.cs b/src/Umbraco.Infrastructure/Logging/Viewer/ExpressionFilter.cs index a1c549add860..46d49f414440 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/ExpressionFilter.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/ExpressionFilter.cs @@ -1,79 +1,74 @@ -using System; -using System.Linq; using Serilog.Events; using Serilog.Expressions; using Umbraco.Cms.Infrastructure.Logging.Viewer; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Logging.Viewer -{ - //Log Expression Filters (pass in filter exp string) - internal class ExpressionFilter : ILogFilter - { - private readonly Func? _filter; - private const string s_expressionOperators = "()+=*<>%-"; - - public ExpressionFilter(string? filterExpression) - { - Func? filter; +namespace Umbraco.Cms.Core.Logging.Viewer; - // Our custom Serilog Functions to extend Serilog.Expressions - // In this case we are plugging the gap for the missing Has() - // function from porting away from Serilog.Filters.Expressions to Serilog.Expressions - // Along with patching support for the more verbose built in property names - var customSerilogFunctions = new SerilogLegacyNameResolver(typeof(SerilogExpressionsFunctions)); +//Log Expression Filters (pass in filter exp string) +internal class ExpressionFilter : ILogFilter +{ + private const string s_expressionOperators = "()+=*<>%-"; + private readonly Func? _filter; - if (string.IsNullOrEmpty(filterExpression)) - { - return; - } + public ExpressionFilter(string? filterExpression) + { + Func? filter; - // If the expression is one word and doesn't contain a serilog operator then we can perform a like search - if (!filterExpression.Contains(" ") && !filterExpression.ContainsAny(s_expressionOperators.Select(c => c))) - { - filter = PerformMessageLikeFilter(filterExpression); - } - else // check if it's a valid expression - { - // If the expression evaluates then make it into a filter - if (SerilogExpression.TryCompile(filterExpression, null, customSerilogFunctions, out CompiledExpression? compiled, out var error)) - { - filter = evt => - { - LogEventPropertyValue? result = compiled(evt); - return ExpressionResult.IsTrue(result); - }; - } - else - { - // 'error' describes a syntax error, where it was unable to compile an expression - // Assume the expression was a search string and make a Like filter from that - filter = PerformMessageLikeFilter(filterExpression); - } - } + // Our custom Serilog Functions to extend Serilog.Expressions + // In this case we are plugging the gap for the missing Has() + // function from porting away from Serilog.Filters.Expressions to Serilog.Expressions + // Along with patching support for the more verbose built in property names + var customSerilogFunctions = new SerilogLegacyNameResolver(typeof(SerilogExpressionsFunctions)); - _filter = filter; + if (string.IsNullOrEmpty(filterExpression)) + { + return; } - public bool TakeLogEvent(LogEvent e) + // If the expression is one word and doesn't contain a serilog operator then we can perform a like search + if (!filterExpression.Contains(" ") && !filterExpression.ContainsAny(s_expressionOperators.Select(c => c))) { - return _filter == null || _filter(e); + filter = PerformMessageLikeFilter(filterExpression); } - - private Func? PerformMessageLikeFilter(string filterExpression) + else // check if it's a valid expression { - var filterSearch = $"@Message like '%{SerilogExpression.EscapeLikeExpressionContent(filterExpression)}%'"; - if (SerilogExpression.TryCompile(filterSearch, out CompiledExpression? compiled, out var error)) + // If the expression evaluates then make it into a filter + if (SerilogExpression.TryCompile(filterExpression, null, customSerilogFunctions, + out CompiledExpression? compiled, out var error)) { - // `compiled` is a function that can be executed against `LogEvent`s: - return evt => + filter = evt => { LogEventPropertyValue? result = compiled(evt); return ExpressionResult.IsTrue(result); }; } + else + { + // 'error' describes a syntax error, where it was unable to compile an expression + // Assume the expression was a search string and make a Like filter from that + filter = PerformMessageLikeFilter(filterExpression); + } + } - return null; + _filter = filter; + } + + public bool TakeLogEvent(LogEvent e) => _filter == null || _filter(e); + + private Func? PerformMessageLikeFilter(string filterExpression) + { + var filterSearch = $"@Message like '%{SerilogExpression.EscapeLikeExpressionContent(filterExpression)}%'"; + if (SerilogExpression.TryCompile(filterSearch, out CompiledExpression? compiled, out var error)) + { + // `compiled` is a function that can be executed against `LogEvent`s: + return evt => + { + LogEventPropertyValue? result = compiled(evt); + return ExpressionResult.IsTrue(result); + }; } + + return null; } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/ILogFilter.cs b/src/Umbraco.Infrastructure/Logging/Viewer/ILogFilter.cs index 4619df2b1320..5bcbceec6bca 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/ILogFilter.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/ILogFilter.cs @@ -1,9 +1,8 @@ using Serilog.Events; -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +public interface ILogFilter { - public interface ILogFilter - { - bool TakeLogEvent(LogEvent e); - } + bool TakeLogEvent(LogEvent e); } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/ILogLevelLoader.cs b/src/Umbraco.Infrastructure/Logging/Viewer/ILogLevelLoader.cs index 1566b962822a..25576b88da29 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/ILogLevelLoader.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/ILogLevelLoader.cs @@ -1,18 +1,17 @@ using System.Collections.ObjectModel; using Serilog.Events; -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +public interface ILogLevelLoader { - public interface ILogLevelLoader - { - /// - /// Get the Serilog level values of the global minimum and the UmbracoFile one from the config file. - /// - ReadOnlyDictionary GetLogLevelsFromSinks(); + /// + /// Get the Serilog level values of the global minimum and the UmbracoFile one from the config file. + /// + ReadOnlyDictionary GetLogLevelsFromSinks(); - /// - /// Get the Serilog minimum-level value from the config file. - /// - LogEventLevel? GetGlobalMinLogLevel(); - } + /// + /// Get the Serilog minimum-level value from the config file. + /// + LogEventLevel? GetGlobalMinLogLevel(); } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs b/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs index 2bda63c96bb7..3566d79df6b1 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs @@ -1,64 +1,58 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using Serilog.Events; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Logging.Viewer -{ - public interface ILogViewer - { - /// - /// Get all saved searches from your chosen data source - /// - IReadOnlyList? GetSavedSearches(); - - /// - /// Adds a new saved search to chosen data source and returns the updated searches - /// - IReadOnlyList? AddSavedSearch(string? name, string? query); - - /// - /// Deletes a saved search to chosen data source and returns the remaining searches - /// - IReadOnlyList? DeleteSavedSearch(string? name, string? query); - - /// - /// A count of number of errors - /// By counting Warnings with Exceptions, Errors & Fatal messages - /// - int GetNumberOfErrors(LogTimePeriod logTimePeriod); - - /// - /// Returns a number of the different log level entries - /// - LogLevelCounts GetLogLevelCounts(LogTimePeriod logTimePeriod); - - /// - /// Returns a list of all unique message templates and their counts - /// - IEnumerable GetMessageTemplates(LogTimePeriod logTimePeriod); +namespace Umbraco.Cms.Core.Logging.Viewer; - bool CanHandleLargeLogs { get; } - - bool CheckCanOpenLogs(LogTimePeriod logTimePeriod); - - /// - /// Gets the current Serilog minimum log level - /// - /// - [Obsolete("Please use GetLogLevels() instead. Scheduled for removal in V11.")] - string GetLogLevel(); - - /// - /// Returns the collection of logs - /// - PagedResult GetLogs(LogTimePeriod logTimePeriod, - int pageNumber = 1, - int pageSize = 100, - Direction orderDirection = Direction.Descending, - string? filterExpression = null, - string[]? logLevels = null); - - } +public interface ILogViewer +{ + bool CanHandleLargeLogs { get; } + + /// + /// Get all saved searches from your chosen data source + /// + IReadOnlyList? GetSavedSearches(); + + /// + /// Adds a new saved search to chosen data source and returns the updated searches + /// + IReadOnlyList? AddSavedSearch(string? name, string? query); + + /// + /// Deletes a saved search to chosen data source and returns the remaining searches + /// + IReadOnlyList? DeleteSavedSearch(string? name, string? query); + + /// + /// A count of number of errors + /// By counting Warnings with Exceptions, Errors & Fatal messages + /// + int GetNumberOfErrors(LogTimePeriod logTimePeriod); + + /// + /// Returns a number of the different log level entries + /// + LogLevelCounts GetLogLevelCounts(LogTimePeriod logTimePeriod); + + /// + /// Returns a list of all unique message templates and their counts + /// + IEnumerable GetMessageTemplates(LogTimePeriod logTimePeriod); + + bool CheckCanOpenLogs(LogTimePeriod logTimePeriod); + + /// + /// Gets the current Serilog minimum log level + /// + /// + [Obsolete("Please use GetLogLevels() instead. Scheduled for removal in V11.")] + string GetLogLevel(); + + /// + /// Returns the collection of logs + /// + PagedResult GetLogs(LogTimePeriod logTimePeriod, + int pageNumber = 1, + int pageSize = 100, + Direction orderDirection = Direction.Descending, + string? filterExpression = null, + string[]? logLevels = null); } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewerConfig.cs b/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewerConfig.cs index 5be26a109943..aa91c8507cc3 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewerConfig.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewerConfig.cs @@ -1,12 +1,8 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Logging.Viewer; -namespace Umbraco.Cms.Core.Logging.Viewer +public interface ILogViewerConfig { - public interface ILogViewerConfig - { - IReadOnlyList? GetSavedSearches(); - IReadOnlyList? AddSavedSearch(string? name, string? query); - IReadOnlyList? DeleteSavedSearch(string? name, string? query); - } + IReadOnlyList? GetSavedSearches(); + IReadOnlyList? AddSavedSearch(string? name, string? query); + IReadOnlyList? DeleteSavedSearch(string? name, string? query); } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogLevelCounts.cs b/src/Umbraco.Infrastructure/Logging/Viewer/LogLevelCounts.cs index f397c1ab7c07..600f29132eb9 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/LogLevelCounts.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/LogLevelCounts.cs @@ -1,15 +1,14 @@ -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +public class LogLevelCounts { - public class LogLevelCounts - { - public int Information { get; set; } + public int Information { get; set; } - public int Debug { get; set; } + public int Debug { get; set; } - public int Warning { get; set; } + public int Warning { get; set; } - public int Error { get; set; } + public int Error { get; set; } - public int Fatal { get; set; } - } + public int Fatal { get; set; } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogLevelLoader.cs b/src/Umbraco.Infrastructure/Logging/Viewer/LogLevelLoader.cs index 37c7923cca78..6ed15a9b447b 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/LogLevelLoader.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/LogLevelLoader.cs @@ -1,41 +1,36 @@ -using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Linq; -using System.Text; using Serilog; using Serilog.Events; using Umbraco.Cms.Infrastructure.Logging.Serilog; -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +public class LogLevelLoader : ILogLevelLoader { - public class LogLevelLoader : ILogLevelLoader - { - private readonly UmbracoFileConfiguration _umbracoFileConfig; + private readonly UmbracoFileConfiguration _umbracoFileConfig; - public LogLevelLoader(UmbracoFileConfiguration umbracoFileConfig) => _umbracoFileConfig = umbracoFileConfig; + public LogLevelLoader(UmbracoFileConfiguration umbracoFileConfig) => _umbracoFileConfig = umbracoFileConfig; - /// - /// Get the Serilog level values of the global minimum and the UmbracoFile one from the config file. - /// - public ReadOnlyDictionary GetLogLevelsFromSinks() + /// + /// Get the Serilog level values of the global minimum and the UmbracoFile one from the config file. + /// + public ReadOnlyDictionary GetLogLevelsFromSinks() + { + var configuredLogLevels = new Dictionary { - var configuredLogLevels = new Dictionary - { - { "Global", GetGlobalMinLogLevel() }, - { "UmbracoFile", _umbracoFileConfig.RestrictedToMinimumLevel } - }; + {"Global", GetGlobalMinLogLevel()}, {"UmbracoFile", _umbracoFileConfig.RestrictedToMinimumLevel} + }; - return new ReadOnlyDictionary(configuredLogLevels); - } + return new ReadOnlyDictionary(configuredLogLevels); + } - /// - /// Get the Serilog minimum-level value from the config file. - /// - public LogEventLevel? GetGlobalMinLogLevel() - { - var logLevel = Enum.GetValues(typeof(LogEventLevel)).Cast().Where(Log.IsEnabled).DefaultIfEmpty(LogEventLevel.Information)?.Min() ?? null; - return (LogEventLevel?)logLevel; - } + /// + /// Get the Serilog minimum-level value from the config file. + /// + public LogEventLevel? GetGlobalMinLogLevel() + { + LogEventLevel? logLevel = Enum.GetValues(typeof(LogEventLevel)).Cast().Where(Log.IsEnabled) + .DefaultIfEmpty(LogEventLevel.Information)?.Min() ?? null; + return logLevel; } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogMessage.cs b/src/Umbraco.Infrastructure/Logging/Viewer/LogMessage.cs index 9bdea3f6506f..941e766067c7 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/LogMessage.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/LogMessage.cs @@ -1,43 +1,40 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Serilog.Events; -using System; -using System.Collections.Generic; // ReSharper disable UnusedAutoPropertyAccessor.Global -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +public class LogMessage { - public class LogMessage - { - /// - /// The time at which the log event occurred. - /// - public DateTimeOffset Timestamp { get; set; } + /// + /// The time at which the log event occurred. + /// + public DateTimeOffset Timestamp { get; set; } - /// - /// The level of the event. - /// - [JsonConverter(typeof(StringEnumConverter))] - public LogEventLevel Level { get; set; } + /// + /// The level of the event. + /// + [JsonConverter(typeof(StringEnumConverter))] + public LogEventLevel Level { get; set; } - /// - /// The message template describing the log event. - /// - public string? MessageTemplateText { get; set; } + /// + /// The message template describing the log event. + /// + public string? MessageTemplateText { get; set; } - /// - /// The message template filled with the log event properties. - /// - public string? RenderedMessage { get; set; } + /// + /// The message template filled with the log event properties. + /// + public string? RenderedMessage { get; set; } - /// - /// Properties associated with the log event, including those presented in Serilog.Events.LogEvent.MessageTemplate. - /// - public IReadOnlyDictionary? Properties { get; set; } + /// + /// Properties associated with the log event, including those presented in Serilog.Events.LogEvent.MessageTemplate. + /// + public IReadOnlyDictionary? Properties { get; set; } - /// - /// An exception associated with the log event, or null. - /// - public string? Exception { get; set; } - } + /// + /// An exception associated with the log event, or null. + /// + public string? Exception { get; set; } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogTemplate.cs b/src/Umbraco.Infrastructure/Logging/Viewer/LogTemplate.cs index ecded4d35bf2..2333da80caa3 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/LogTemplate.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/LogTemplate.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +public class LogTemplate { - public class LogTemplate - { - public string? MessageTemplate { get; set; } + public string? MessageTemplate { get; set; } - public int Count { get; set; } - } + public int Count { get; set; } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogTimePeriod.cs b/src/Umbraco.Infrastructure/Logging/Viewer/LogTimePeriod.cs index 446f7bf160c6..5c19f0cda95a 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/LogTimePeriod.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/LogTimePeriod.cs @@ -1,16 +1,13 @@ -using System; +namespace Umbraco.Cms.Core.Logging.Viewer; -namespace Umbraco.Cms.Core.Logging.Viewer +public class LogTimePeriod { - public class LogTimePeriod + public LogTimePeriod(DateTime startTime, DateTime endTime) { - public LogTimePeriod(DateTime startTime, DateTime endTime) - { - StartTime = startTime; - EndTime = endTime; - } - - public DateTime StartTime { get; } - public DateTime EndTime { get; } + StartTime = startTime; + EndTime = endTime; } + + public DateTime StartTime { get; } + public DateTime EndTime { get; } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs b/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs index 8b1163969622..85d6deb66453 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs @@ -1,49 +1,48 @@ -using System.Collections.Generic; -using System.Linq; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; +using IScope = Umbraco.Cms.Infrastructure.Scoping.IScope; -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +public class LogViewerConfig : ILogViewerConfig { - public class LogViewerConfig : ILogViewerConfig - { - private readonly ILogViewerQueryRepository _logViewerQueryRepository; - private readonly IScopeProvider _scopeProvider; + private readonly ILogViewerQueryRepository _logViewerQueryRepository; + private readonly IScopeProvider _scopeProvider; - public LogViewerConfig(ILogViewerQueryRepository logViewerQueryRepository, IScopeProvider scopeProvider) - { - _logViewerQueryRepository = logViewerQueryRepository; - _scopeProvider = scopeProvider; - } + public LogViewerConfig(ILogViewerQueryRepository logViewerQueryRepository, IScopeProvider scopeProvider) + { + _logViewerQueryRepository = logViewerQueryRepository; + _scopeProvider = scopeProvider; + } - public IReadOnlyList? GetSavedSearches() - { - using var scope = _scopeProvider.CreateScope(autoComplete: true); - var logViewerQueries = _logViewerQueryRepository.GetMany(); - var result = logViewerQueries?.Select(x => new SavedLogSearch() { Name = x.Name, Query = x.Query }).ToArray(); - return result; - } + public IReadOnlyList? GetSavedSearches() + { + using IScope scope = _scopeProvider.CreateScope(autoComplete: true); + IEnumerable? logViewerQueries = _logViewerQueryRepository.GetMany(); + SavedLogSearch[]? result = logViewerQueries?.Select(x => new SavedLogSearch {Name = x.Name, Query = x.Query}) + .ToArray(); + return result; + } - public IReadOnlyList? AddSavedSearch(string? name, string? query) - { - using var scope = _scopeProvider.CreateScope(autoComplete: true); - _logViewerQueryRepository.Save(new LogViewerQuery(name, query)); + public IReadOnlyList? AddSavedSearch(string? name, string? query) + { + using IScope scope = _scopeProvider.CreateScope(autoComplete: true); + _logViewerQueryRepository.Save(new LogViewerQuery(name, query)); - return GetSavedSearches(); - } + return GetSavedSearches(); + } - public IReadOnlyList? DeleteSavedSearch(string? name, string? query) + public IReadOnlyList? DeleteSavedSearch(string? name, string? query) + { + using IScope scope = _scopeProvider.CreateScope(autoComplete: true); + ILogViewerQuery? item = name is null ? null : _logViewerQueryRepository.GetByName(name); + if (item is not null) { - using var scope = _scopeProvider.CreateScope(autoComplete: true); - var item = name is null ? null : _logViewerQueryRepository.GetByName(name); - if (item is not null) - { - _logViewerQueryRepository.Delete(item); - } - - //Return the updated object - so we can instantly reset the entire array from the API response - return GetSavedSearches(); + _logViewerQueryRepository.Delete(item); } + + //Return the updated object - so we can instantly reset the entire array from the API response + return GetSavedSearches(); } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/MessageTemplateFilter.cs b/src/Umbraco.Infrastructure/Logging/Viewer/MessageTemplateFilter.cs index 1b8971625681..be3866ffca11 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/MessageTemplateFilter.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/MessageTemplateFilter.cs @@ -1,28 +1,26 @@ -using System.Collections.Generic; -using Serilog.Events; +using Serilog.Events; -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +internal class MessageTemplateFilter : ILogFilter { - internal class MessageTemplateFilter : ILogFilter - { - public readonly Dictionary Counts = new Dictionary(); + public readonly Dictionary Counts = new(); - public bool TakeLogEvent(LogEvent e) + public bool TakeLogEvent(LogEvent e) + { + var templateText = e.MessageTemplate.Text; + if (Counts.TryGetValue(templateText, out var count)) { - var templateText = e.MessageTemplate.Text; - if (Counts.TryGetValue(templateText, out var count)) - { - count++; - } - else - { - count = 1; - } + count++; + } + else + { + count = 1; + } - Counts[templateText] = count; + Counts[templateText] = count; - //Don't add it to the list - return false; - } + //Don't add it to the list + return false; } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/SavedLogSearch.cs b/src/Umbraco.Infrastructure/Logging/Viewer/SavedLogSearch.cs index adbd1a643101..69d263b6b3ba 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/SavedLogSearch.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/SavedLogSearch.cs @@ -1,13 +1,10 @@ using Newtonsoft.Json; -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +public class SavedLogSearch { - public class SavedLogSearch - { - [JsonProperty("name")] - public string? Name { get; set; } + [JsonProperty("name")] public string? Name { get; set; } - [JsonProperty("query")] - public string? Query { get; set; } - } + [JsonProperty("query")] public string? Query { get; set; } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogExpressionsFunctions.cs b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogExpressionsFunctions.cs index 92b16b972968..9cf9cc730764 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogExpressionsFunctions.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogExpressionsFunctions.cs @@ -1,14 +1,10 @@ using Serilog.Events; -namespace Umbraco.Cms.Infrastructure.Logging.Viewer +namespace Umbraco.Cms.Infrastructure.Logging.Viewer; + +public class SerilogExpressionsFunctions { - public class SerilogExpressionsFunctions - { - // This Has() code is the same as the renamed IsDefined() function - // Added this to help backport and ensure saved queries continue to work if using Has() - public static LogEventPropertyValue? Has(LogEventPropertyValue? value) - { - return new ScalarValue(value != null); - } - } + // This Has() code is the same as the renamed IsDefined() function + // Added this to help backport and ensure saved queries continue to work if using Has() + public static LogEventPropertyValue? Has(LogEventPropertyValue? value) => new ScalarValue(value != null); } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogJsonLogViewer.cs b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogJsonLogViewer.cs index ba148a1bdac8..d80db8921e20 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogJsonLogViewer.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogJsonLogViewer.cs @@ -1,140 +1,133 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Serilog.Events; using Serilog.Formatting.Compact.Reader; +using ILogger = Serilog.ILogger; -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +internal class SerilogJsonLogViewer : SerilogLogViewerSourceBase { - internal class SerilogJsonLogViewer : SerilogLogViewerSourceBase + private const int FileSizeCap = 100; + private readonly ILogger _logger; + private readonly string _logsPath; + + public SerilogJsonLogViewer( + ILogger logger, + ILogViewerConfig logViewerConfig, + ILoggingConfiguration loggingConfiguration, + ILogLevelLoader logLevelLoader, + ILogger serilogLog) + : base(logViewerConfig, logLevelLoader, serilogLog) { - private readonly string _logsPath; - private readonly ILogger _logger; - - public SerilogJsonLogViewer( - ILogger logger, - ILogViewerConfig logViewerConfig, - ILoggingConfiguration loggingConfiguration, - ILogLevelLoader logLevelLoader, - global::Serilog.ILogger serilogLog) - : base(logViewerConfig, logLevelLoader, serilogLog) - { - _logger = logger; - _logsPath = loggingConfiguration.LogDirectory; - } + _logger = logger; + _logsPath = loggingConfiguration.LogDirectory; + } - private const int FileSizeCap = 100; + public override bool CanHandleLargeLogs => false; - public override bool CanHandleLargeLogs => false; + public override bool CheckCanOpenLogs(LogTimePeriod logTimePeriod) + { + //Log Directory + var logDirectory = _logsPath; - public override bool CheckCanOpenLogs(LogTimePeriod logTimePeriod) + //Number of entries + long fileSizeCount = 0; + + //foreach full day in the range - see if we can find one or more filenames that end with + //yyyyMMdd.json - Ends with due to MachineName in filenames - could be 1 or more due to load balancing + for (DateTime day = logTimePeriod.StartTime.Date; day.Date <= logTimePeriod.EndTime.Date; day = day.AddDays(1)) { - //Log Directory - var logDirectory = _logsPath; + //Filename ending to search for (As could be multiple) + var filesToFind = GetSearchPattern(day); - //Number of entries - long fileSizeCount = 0; + var filesForCurrentDay = Directory.GetFiles(logDirectory, filesToFind); - //foreach full day in the range - see if we can find one or more filenames that end with - //yyyyMMdd.json - Ends with due to MachineName in filenames - could be 1 or more due to load balancing - for (var day = logTimePeriod.StartTime.Date; day.Date <= logTimePeriod.EndTime.Date; day = day.AddDays(1)) - { - //Filename ending to search for (As could be multiple) - var filesToFind = GetSearchPattern(day); + fileSizeCount += filesForCurrentDay.Sum(x => new FileInfo(x).Length); + } - var filesForCurrentDay = Directory.GetFiles(logDirectory, filesToFind); + //The GetLogSize call on JsonLogViewer returns the total file size in bytes + //Check if the log size is not greater than 100Mb (FileSizeCap) + var logSizeAsMegabytes = fileSizeCount / 1024 / 1024; + return logSizeAsMegabytes <= FileSizeCap; + } - fileSizeCount += filesForCurrentDay.Sum(x => new FileInfo(x).Length); - } + private string GetSearchPattern(DateTime day) => $"*{day:yyyyMMdd}*.json"; - //The GetLogSize call on JsonLogViewer returns the total file size in bytes - //Check if the log size is not greater than 100Mb (FileSizeCap) - var logSizeAsMegabytes = fileSizeCount / 1024 / 1024; - return logSizeAsMegabytes <= FileSizeCap; - } + protected override IReadOnlyList GetLogs(LogTimePeriod logTimePeriod, ILogFilter filter, int skip, + int take) + { + var logs = new List(); - private string GetSearchPattern(DateTime day) - { - return $"*{day:yyyyMMdd}*.json"; - } + var count = 0; - protected override IReadOnlyList GetLogs(LogTimePeriod logTimePeriod, ILogFilter filter, int skip, int take) + //foreach full day in the range - see if we can find one or more filenames that end with + //yyyyMMdd.json - Ends with due to MachineName in filenames - could be 1 or more due to load balancing + for (DateTime day = logTimePeriod.StartTime.Date; day.Date <= logTimePeriod.EndTime.Date; day = day.AddDays(1)) { - var logs = new List(); + //Filename ending to search for (As could be multiple) + var filesToFind = GetSearchPattern(day); - var count = 0; + var filesForCurrentDay = Directory.GetFiles(_logsPath, filesToFind); - //foreach full day in the range - see if we can find one or more filenames that end with - //yyyyMMdd.json - Ends with due to MachineName in filenames - could be 1 or more due to load balancing - for (var day = logTimePeriod.StartTime.Date; day.Date <= logTimePeriod.EndTime.Date; day = day.AddDays(1)) + //Foreach file we find - open it + foreach (var filePath in filesForCurrentDay) { - //Filename ending to search for (As could be multiple) - var filesToFind = GetSearchPattern(day); - - var filesForCurrentDay = Directory.GetFiles(_logsPath, filesToFind); - - //Foreach file we find - open it - foreach (var filePath in filesForCurrentDay) + //Open log file & add contents to the log collection + //Which we then use LINQ to page over + using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { - //Open log file & add contents to the log collection - //Which we then use LINQ to page over - using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + using (var stream = new StreamReader(fs)) { - using (var stream = new StreamReader(fs)) + var reader = new LogEventReader(stream); + while (TryRead(reader, out LogEvent? evt)) { - var reader = new LogEventReader(stream); - while (TryRead(reader, out var evt)) + //We may get a null if log line is malformed + if (evt == null) { - //We may get a null if log line is malformed - if (evt == null) - { - continue; - } - - if (count > skip + take) - { - break; - } - - if (count < skip) - { - count++; - continue; - } - - if (filter.TakeLogEvent(evt)) - { - logs.Add(evt); - } + continue; + } + if (count > skip + take) + { + break; + } + + if (count < skip) + { count++; + continue; } + + if (filter.TakeLogEvent(evt)) + { + logs.Add(evt); + } + + count++; } } } } - - return logs; } - private bool TryRead(LogEventReader reader, out LogEvent? evt) + return logs; + } + + private bool TryRead(LogEventReader reader, out LogEvent? evt) + { + try { - try - { - return reader.TryRead(out evt); - } - catch (JsonReaderException ex) - { - // As we are reading/streaming one line at a time in the JSON file - // Thus we can not report the line number, as it will always be 1 - _logger.LogError(ex, "Unable to parse a line in the JSON log file"); + return reader.TryRead(out evt); + } + catch (JsonReaderException ex) + { + // As we are reading/streaming one line at a time in the JSON file + // Thus we can not report the line number, as it will always be 1 + _logger.LogError(ex, "Unable to parse a line in the JSON log file"); - evt = null; - return true; - } + evt = null; + return true; } } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLegacyNameResolver.cs b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLegacyNameResolver.cs index 8e24f40b6c6c..69905b15047c 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLegacyNameResolver.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLegacyNameResolver.cs @@ -1,39 +1,37 @@ -using System; using System.Diagnostics.CodeAnalysis; using Serilog.Expressions; -namespace Umbraco.Cms.Infrastructure.Logging.Viewer +namespace Umbraco.Cms.Infrastructure.Logging.Viewer; + +/// +/// Inherits Serilog's StaticMemberNameResolver to ensure we get same functionality +/// Of easily allowing any static methods definied in the passed in class/type +/// To extend as functions to use for filtering logs such as Has() and any other custom ones +/// +public class SerilogLegacyNameResolver : StaticMemberNameResolver { + public SerilogLegacyNameResolver(Type type) : base(type) + { + } + /// - /// Inherits Serilog's StaticMemberNameResolver to ensure we get same functionality - /// Of easily allowing any static methods definied in the passed in class/type - /// To extend as functions to use for filtering logs such as Has() and any other custom ones + /// Allows us to fix the gap from migrating away from Serilog.Filters.Expressions + /// So we can still support the more verbose built in property names such as + /// Exception, Level, MessageTemplate etc /// - public class SerilogLegacyNameResolver : StaticMemberNameResolver + public override bool TryResolveBuiltInPropertyName(string alias, [MaybeNullWhen(false)] out string target) { - public SerilogLegacyNameResolver(Type type) : base(type) - { - } - - /// - /// Allows us to fix the gap from migrating away from Serilog.Filters.Expressions - /// So we can still support the more verbose built in property names such as - /// Exception, Level, MessageTemplate etc - /// - public override bool TryResolveBuiltInPropertyName(string alias, [MaybeNullWhen(false)] out string target) + target = alias switch { - target = alias switch - { - "Exception" => "x", - "Level" => "l", - "Message" => "m", - "MessageTemplate" => "mt", - "Properties" => "p", - "Timestamp" => "t", - _ => null - }; + "Exception" => "x", + "Level" => "l", + "Message" => "m", + "MessageTemplate" => "mt", + "Properties" => "p", + "Timestamp" => "t", + _ => null + }; - return target != null; - } + return target != null; } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs index ca5c5c3a46aa..37466e9ea208 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs @@ -1,154 +1,149 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; +using System.Collections.ObjectModel; using Microsoft.Extensions.DependencyInjection; +using Serilog; using Serilog.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Logging.Viewer -{ - public abstract class SerilogLogViewerSourceBase : ILogViewer - { - private readonly ILogViewerConfig _logViewerConfig; - private readonly ILogLevelLoader _logLevelLoader; - private readonly global::Serilog.ILogger _serilogLog; +namespace Umbraco.Cms.Core.Logging.Viewer; - [Obsolete("Please use ctor with all params instead. Scheduled for removal in V11.")] - protected SerilogLogViewerSourceBase(ILogViewerConfig logViewerConfig, global::Serilog.ILogger serilogLog) - { - _logViewerConfig = logViewerConfig; - _logLevelLoader = StaticServiceProvider.Instance.GetRequiredService(); - _serilogLog = serilogLog; - } +public abstract class SerilogLogViewerSourceBase : ILogViewer +{ + private readonly ILogLevelLoader _logLevelLoader; + private readonly ILogViewerConfig _logViewerConfig; + private readonly ILogger _serilogLog; - protected SerilogLogViewerSourceBase(ILogViewerConfig logViewerConfig, ILogLevelLoader logLevelLoader, global::Serilog.ILogger serilogLog) - { - _logViewerConfig = logViewerConfig; - _logLevelLoader = logLevelLoader; - _serilogLog = serilogLog; - } + [Obsolete("Please use ctor with all params instead. Scheduled for removal in V11.")] + protected SerilogLogViewerSourceBase(ILogViewerConfig logViewerConfig, ILogger serilogLog) + { + _logViewerConfig = logViewerConfig; + _logLevelLoader = StaticServiceProvider.Instance.GetRequiredService(); + _serilogLog = serilogLog; + } - public abstract bool CanHandleLargeLogs { get; } + protected SerilogLogViewerSourceBase(ILogViewerConfig logViewerConfig, ILogLevelLoader logLevelLoader, + ILogger serilogLog) + { + _logViewerConfig = logViewerConfig; + _logLevelLoader = logLevelLoader; + _serilogLog = serilogLog; + } - /// - /// Get all logs from your chosen data source back as Serilog LogEvents - /// - protected abstract IReadOnlyList GetLogs(LogTimePeriod logTimePeriod, ILogFilter filter, int skip, int take); + public abstract bool CanHandleLargeLogs { get; } - public abstract bool CheckCanOpenLogs(LogTimePeriod logTimePeriod); + public abstract bool CheckCanOpenLogs(LogTimePeriod logTimePeriod); - public virtual IReadOnlyList? GetSavedSearches() - => _logViewerConfig.GetSavedSearches(); + public virtual IReadOnlyList? GetSavedSearches() + => _logViewerConfig.GetSavedSearches(); - public virtual IReadOnlyList? AddSavedSearch(string? name, string? query) - => _logViewerConfig.AddSavedSearch(name, query); + public virtual IReadOnlyList? AddSavedSearch(string? name, string? query) + => _logViewerConfig.AddSavedSearch(name, query); - public virtual IReadOnlyList? DeleteSavedSearch(string? name, string? query) - => _logViewerConfig.DeleteSavedSearch(name, query); + public virtual IReadOnlyList? DeleteSavedSearch(string? name, string? query) + => _logViewerConfig.DeleteSavedSearch(name, query); - public int GetNumberOfErrors(LogTimePeriod logTimePeriod) - { - var errorCounter = new ErrorCounterFilter(); - GetLogs(logTimePeriod, errorCounter, 0, int.MaxValue); - return errorCounter.Count; - } + public int GetNumberOfErrors(LogTimePeriod logTimePeriod) + { + var errorCounter = new ErrorCounterFilter(); + GetLogs(logTimePeriod, errorCounter, 0, int.MaxValue); + return errorCounter.Count; + } - /// - /// Get the Serilog minimum-level and UmbracoFile-level values from the config file. - /// - public ReadOnlyDictionary GetLogLevels() - { - return _logLevelLoader.GetLogLevelsFromSinks(); - } + /// + /// Get the Serilog minimum-level value from the config file. + /// + [Obsolete("Please use LogLevelLoader.GetGlobalMinLogLevel() instead. Scheduled for removal in V11.")] + public string GetLogLevel() + { + LogEventLevel? logLevel = Enum.GetValues(typeof(LogEventLevel)).Cast() + .Where(_serilogLog.IsEnabled).DefaultIfEmpty(LogEventLevel.Information)?.Min() ?? null; + return logLevel?.ToString() ?? string.Empty; + } - /// - /// Get the Serilog minimum-level value from the config file. - /// - [Obsolete("Please use LogLevelLoader.GetGlobalMinLogLevel() instead. Scheduled for removal in V11.")] - public string GetLogLevel() - { - var logLevel = Enum.GetValues(typeof(LogEventLevel)).Cast().Where(_serilogLog.IsEnabled).DefaultIfEmpty(LogEventLevel.Information)?.Min() ?? null; - return logLevel?.ToString() ?? string.Empty; - } + public LogLevelCounts GetLogLevelCounts(LogTimePeriod logTimePeriod) + { + var counter = new CountingFilter(); + GetLogs(logTimePeriod, counter, 0, int.MaxValue); + return counter.Counts; + } - public LogLevelCounts GetLogLevelCounts(LogTimePeriod logTimePeriod) - { - var counter = new CountingFilter(); - GetLogs(logTimePeriod, counter, 0, int.MaxValue); - return counter.Counts; - } + public IEnumerable GetMessageTemplates(LogTimePeriod logTimePeriod) + { + var messageTemplates = new MessageTemplateFilter(); + GetLogs(logTimePeriod, messageTemplates, 0, int.MaxValue); - public IEnumerable GetMessageTemplates(LogTimePeriod logTimePeriod) - { - var messageTemplates = new MessageTemplateFilter(); - GetLogs(logTimePeriod, messageTemplates, 0, int.MaxValue); + IOrderedEnumerable templates = messageTemplates.Counts + .Select(x => new LogTemplate {MessageTemplate = x.Key, Count = x.Value}) + .OrderByDescending(x => x.Count); - var templates = messageTemplates.Counts. - Select(x => new LogTemplate { MessageTemplate = x.Key, Count = x.Value }) - .OrderByDescending(x=> x.Count); + return templates; + } - return templates; - } + public PagedResult GetLogs(LogTimePeriod logTimePeriod, + int pageNumber = 1, int pageSize = 100, + Direction orderDirection = Direction.Descending, + string? filterExpression = null, + string[]? logLevels = null) + { + var expression = new ExpressionFilter(filterExpression); + IReadOnlyList filteredLogs = GetLogs(logTimePeriod, expression, 0, int.MaxValue); - public PagedResult GetLogs(LogTimePeriod logTimePeriod, - int pageNumber = 1, int pageSize = 100, - Direction orderDirection = Direction.Descending, - string? filterExpression = null, - string[]? logLevels = null) + //This is user used the checkbox UI to toggle which log levels they wish to see + //If an empty array or null - its implied all levels to be viewed + if (logLevels?.Length > 0) { - var expression = new ExpressionFilter(filterExpression); - var filteredLogs = GetLogs(logTimePeriod, expression, 0, int.MaxValue); - - //This is user used the checkbox UI to toggle which log levels they wish to see - //If an empty array or null - its implied all levels to be viewed - if (logLevels?.Length > 0) + var logsAfterLevelFilters = new List(); + var validLogType = true; + foreach (var level in logLevels) { - var logsAfterLevelFilters = new List(); - var validLogType = true; - foreach (var level in logLevels) + //Check if level string is part of the LogEventLevel enum + if (Enum.IsDefined(typeof(LogEventLevel), level)) { - //Check if level string is part of the LogEventLevel enum - if(Enum.IsDefined(typeof(LogEventLevel), level)) - { - validLogType = true; - logsAfterLevelFilters.AddRange(filteredLogs.Where(x => string.Equals(x.Level.ToString(), level, StringComparison.InvariantCultureIgnoreCase))); - } - else - { - validLogType = false; - } + validLogType = true; + logsAfterLevelFilters.AddRange(filteredLogs.Where(x => + string.Equals(x.Level.ToString(), level, StringComparison.InvariantCultureIgnoreCase))); } - - if (validLogType) + else { - filteredLogs = logsAfterLevelFilters; + validLogType = false; } } - long totalRecords = filteredLogs.Count; - - //Order By, Skip, Take & Select - var logMessages = filteredLogs - .OrderBy(l => l.Timestamp, orderDirection) - .Skip(pageSize * (pageNumber - 1)) - .Take(pageSize) - .Select(x => new LogMessage - { - Timestamp = x.Timestamp, - Level = x.Level, - MessageTemplateText = x.MessageTemplate.Text, - Exception = x.Exception?.ToString(), - Properties = x.Properties, - RenderedMessage = x.RenderMessage() - }); - - return new PagedResult(totalRecords, pageNumber, pageSize) + if (validLogType) { - Items = logMessages - }; + filteredLogs = logsAfterLevelFilters; + } } + + long totalRecords = filteredLogs.Count; + + //Order By, Skip, Take & Select + IEnumerable logMessages = filteredLogs + .OrderBy(l => l.Timestamp, orderDirection) + .Skip(pageSize * (pageNumber - 1)) + .Take(pageSize) + .Select(x => new LogMessage + { + Timestamp = x.Timestamp, + Level = x.Level, + MessageTemplateText = x.MessageTemplate.Text, + Exception = x.Exception?.ToString(), + Properties = x.Properties, + RenderedMessage = x.RenderMessage() + }); + + return new PagedResult(totalRecords, pageNumber, pageSize) {Items = logMessages}; } + + /// + /// Get all logs from your chosen data source back as Serilog LogEvents + /// + protected abstract IReadOnlyList GetLogs(LogTimePeriod logTimePeriod, ILogFilter filter, int skip, + int take); + + /// + /// Get the Serilog minimum-level and UmbracoFile-level values from the config file. + /// + public ReadOnlyDictionary GetLogLevels() => _logLevelLoader.GetLogLevelsFromSinks(); } diff --git a/src/Umbraco.Infrastructure/Macros/MacroTagParser.cs b/src/Umbraco.Infrastructure/Macros/MacroTagParser.cs index 13b03339848e..1e18fd6f4069 100644 --- a/src/Umbraco.Infrastructure/Macros/MacroTagParser.cs +++ b/src/Umbraco.Infrastructure/Macros/MacroTagParser.cs @@ -1,208 +1,210 @@ -using System; -using System.Collections.Generic; -using System.Text; +using System.Text; using System.Text.RegularExpressions; using HtmlAgilityPack; using Umbraco.Cms.Core.Xml; -namespace Umbraco.Cms.Infrastructure.Macros +namespace Umbraco.Cms.Infrastructure.Macros; + +/// +/// Parses the macro syntax in a string and renders out it's contents +/// +public class MacroTagParser { - /// - /// Parses the macro syntax in a string and renders out it's contents - /// - public class MacroTagParser - { - private static readonly Regex MacroRteContent = new Regex(@"()", + private static readonly Regex MacroRteContent = new(@"()", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Singleline); + + private static readonly Regex MacroPersistedFormat = + new(@"(<\?UMBRACO_MACRO (?:.+?)??macroAlias=[""']([^""\'\n\r]+?)[""'].+?)(?:/>|>.*?)", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Singleline); - private static readonly Regex MacroPersistedFormat = - new Regex(@"(<\?UMBRACO_MACRO (?:.+?)??macroAlias=[""']([^""\'\n\r]+?)[""'].+?)(?:/>|>.*?)", - RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Singleline); - - /// - /// This formats the persisted string to something useful for the rte so that the macro renders properly since we - /// persist all macro formats like {?UMBRACO_MACRO macroAlias=\"myMacro\" /} - /// - /// - /// The HTML attributes to be added to the div - /// - /// - /// This converts the persisted macro format to this: - /// - /// {div class='umb-macro-holder'} - /// - /// {ins}Macro alias: {strong}My Macro{/strong}{/ins} - /// {/div} - /// - /// - public static string FormatRichTextPersistedDataForEditor(string persistedContent, IDictionary htmlAttributes) + /// + /// This formats the persisted string to something useful for the rte so that the macro renders properly since we + /// persist all macro formats like {?UMBRACO_MACRO macroAlias=\"myMacro\" /} + /// + /// + /// The HTML attributes to be added to the div + /// + /// + /// This converts the persisted macro format to this: + /// {div class='umb-macro-holder'} + /// + /// {ins}Macro alias: {strong}My Macro{/strong}{/ins} + /// {/div} + /// + public static string FormatRichTextPersistedDataForEditor(string persistedContent, + IDictionary htmlAttributes) => + MacroPersistedFormat.Replace(persistedContent, match => { - return MacroPersistedFormat.Replace(persistedContent, match => + if (match.Groups.Count >= 3) { - if (match.Groups.Count >= 3) + //
+ var alias = match.Groups[2].Value; + var sb = new StringBuilder("
htmlAttribute in htmlAttributes) { - //
- var alias = match.Groups[2].Value; - var sb = new StringBuilder("
"); - sb.Append(""); - sb.Append(""); - sb.Append("Macro alias: "); - sb.Append(""); - sb.Append(alias); - sb.Append("
"); - return sb.ToString(); + sb.Append(" "); + sb.Append(htmlAttribute.Key); + sb.Append("=\""); + sb.Append(htmlAttribute.Value); + sb.Append("\""); } - //replace with nothing if we couldn't find the syntax for whatever reason - return ""; - }); + + sb.AppendLine(">"); + sb.Append(""); + sb.Append(""); + sb.Append("Macro alias: "); + sb.Append(""); + sb.Append(alias); + sb.Append("
"); + return sb.ToString(); + } + + //replace with nothing if we couldn't find the syntax for whatever reason + return ""; + }); + + /// + /// This formats the string content posted from a rich text editor that contains macro contents to be persisted. + /// + /// + /// + /// This is required because when editors are using the rte, the HTML that is contained in the editor might actually be + /// displaying + /// the entire macro content, when the data is submitted the editor will clear most of this data out but we'll still + /// need to parse it properly + /// and ensure the correct syntax is persisted to the db. + /// When a macro is inserted into the rte editor, the HTML will be: + /// {div class='umb-macro-holder'} + /// + /// This could be some macro content + /// {/div} + /// What this method will do is remove the {div} and parse out the commented special macro syntax: {?UMBRACO_MACRO + /// macroAlias=\"myMacro\" /} + /// since this is exactly how we need to persist it to the db. + /// + public static string FormatRichTextContentForPersistence(string rteContent) + { + if (string.IsNullOrEmpty(rteContent)) + { + return string.Empty; } - /// - /// This formats the string content posted from a rich text editor that contains macro contents to be persisted. - /// - /// - /// - /// - /// This is required because when editors are using the rte, the HTML that is contained in the editor might actually be displaying - /// the entire macro content, when the data is submitted the editor will clear most of this data out but we'll still need to parse it properly - /// and ensure the correct syntax is persisted to the db. - /// - /// When a macro is inserted into the rte editor, the HTML will be: - /// - /// {div class='umb-macro-holder'} - /// - /// This could be some macro content - /// {/div} - /// - /// What this method will do is remove the {div} and parse out the commented special macro syntax: {?UMBRACO_MACRO macroAlias=\"myMacro\" /} - /// since this is exactly how we need to persist it to the db. - /// - /// - public static string FormatRichTextContentForPersistence(string rteContent) + var html = new HtmlDocument(); + html.LoadHtml(rteContent); + + //get all the comment nodes we want + HtmlNodeCollection? commentNodes = html.DocumentNode.SelectNodes("//comment()[contains(., ' with the comment node itself. + foreach (HtmlNode? c in commentNodes) + { + HtmlNode? div = c.ParentNode; + HtmlNode? divContainer = div.ParentNode; + divContainer.ReplaceChild(c, div); + } - //get all the comment nodes we want - var commentNodes = html.DocumentNode.SelectNodes("//comment()[contains(., ' with the comment node itself. - foreach (var c in commentNodes) + //now replace all the with nothing + return MacroRteContent.Replace(parsed, match => + { + if (match.Groups.Count >= 3) { - var div = c.ParentNode; - var divContainer = div.ParentNode; - divContainer.ReplaceChild(c, div); + //get the 3rd group which is the macro syntax + return match.Groups[2].Value; } - var parsed = html.DocumentNode.OuterHtml; + //replace with nothing if we couldn't find the syntax for whatever reason + return string.Empty; + }); + } - //now replace all the with nothing - return MacroRteContent.Replace(parsed, match => - { - if (match.Groups.Count >= 3) - { - //get the 3rd group which is the macro syntax - return match.Groups[2].Value; - } - //replace with nothing if we couldn't find the syntax for whatever reason - return string.Empty; - }); + /// + /// This will accept a text block and search/parse it for macro markup. + /// When either a text block or a a macro is found, it will call the callback method. + /// + /// + /// + /// + /// + /// + /// This method simply parses the macro contents, it does not create a string or result, + /// this is up to the developer calling this method to implement this with the callbacks. + /// + public static void ParseMacros( + string text, + Action textFoundCallback, + Action> macroFoundCallback) + { + if (textFoundCallback == null) + { + throw new ArgumentNullException("textFoundCallback"); } - /// - /// This will accept a text block and search/parse it for macro markup. - /// When either a text block or a a macro is found, it will call the callback method. - /// - /// - /// - /// - /// - /// - /// This method simply parses the macro contents, it does not create a string or result, - /// this is up to the developer calling this method to implement this with the callbacks. - /// - public static void ParseMacros( - string text, - Action textFoundCallback, - Action> macroFoundCallback ) + if (macroFoundCallback == null) { - if (textFoundCallback == null) throw new ArgumentNullException("textFoundCallback"); - if (macroFoundCallback == null) throw new ArgumentNullException("macroFoundCallback"); + throw new ArgumentNullException("macroFoundCallback"); + } - string elementText = text; + var elementText = text; - var fieldResult = new StringBuilder(elementText); + var fieldResult = new StringBuilder(elementText); - //NOTE: This is legacy code, this is definitely not the correct way to do a while loop! :) - var stop = false; - while (!stop) + //NOTE: This is legacy code, this is definitely not the correct way to do a while loop! :) + var stop = false; + while (!stop) + { + var tagIndex = fieldResult.ToString().ToLower().IndexOf(" -1) { - var tagIndex = fieldResult.ToString().ToLower().IndexOf(" -1) - { - var tempElementContent = ""; + var tempElementContent = ""; - //text block found, call the call back method - textFoundCallback(fieldResult.ToString().Substring(0, tagIndex)); + //text block found, call the call back method + textFoundCallback(fieldResult.ToString().Substring(0, tagIndex)); - fieldResult.Remove(0, tagIndex); + fieldResult.Remove(0, tagIndex); - var tag = fieldResult.ToString().Substring(0, fieldResult.ToString().IndexOf(">") + 1); - var attributes = XmlHelper.GetAttributesFromElement(tag); + var tag = fieldResult.ToString().Substring(0, fieldResult.ToString().IndexOf(">") + 1); + Dictionary attributes = XmlHelper.GetAttributesFromElement(tag); - // Check whether it's a single tag () or a tag with children (...) - if (tag.Substring(tag.Length - 2, 1) != "/" && tag.IndexOf(" ") > -1) + // Check whether it's a single tag () or a tag with children (...) + if (tag.Substring(tag.Length - 2, 1) != "/" && tag.IndexOf(" ") > -1) + { + var closingTag = ""; + // Tag with children are only used when a macro is inserted by the umbraco-editor, in the + // following format: "", so we + // need to delete extra information inserted which is the image-tag and the closing + // umbraco_macro tag + if (fieldResult.ToString().IndexOf(closingTag) > -1) { - string closingTag = ""; - // Tag with children are only used when a macro is inserted by the umbraco-editor, in the - // following format: "", so we - // need to delete extra information inserted which is the image-tag and the closing - // umbraco_macro tag - if (fieldResult.ToString().IndexOf(closingTag) > -1) - { - fieldResult.Remove(0, fieldResult.ToString().IndexOf(closingTag)); - } + fieldResult.Remove(0, fieldResult.ToString().IndexOf(closingTag)); } + } - var macroAlias = attributes.ContainsKey("macroalias") ? attributes["macroalias"] : attributes["alias"]; + var macroAlias = attributes.ContainsKey("macroalias") ? attributes["macroalias"] : attributes["alias"]; - //call the callback now that we have the macro parsed - macroFoundCallback(macroAlias, attributes); + //call the callback now that we have the macro parsed + macroFoundCallback(macroAlias, attributes); - fieldResult.Remove(0, fieldResult.ToString().IndexOf(">") + 1); - fieldResult.Insert(0, tempElementContent); - } - else - { - //text block found, call the call back method - textFoundCallback(fieldResult.ToString()); + fieldResult.Remove(0, fieldResult.ToString().IndexOf(">") + 1); + fieldResult.Insert(0, tempElementContent); + } + else + { + //text block found, call the call back method + textFoundCallback(fieldResult.ToString()); - stop = true; //break; - } + stop = true; //break; } } } diff --git a/src/Umbraco.Infrastructure/Mail/EmailSender.cs b/src/Umbraco.Infrastructure/Mail/EmailSender.cs index 6f94942aed68..a3c9ee4b7a02 100644 --- a/src/Umbraco.Infrastructure/Mail/EmailSender.cs +++ b/src/Umbraco.Infrastructure/Mail/EmailSender.cs @@ -1,10 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.IO; using System.Net.Mail; -using System.Threading.Tasks; using MailKit.Net.Smtp; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -16,147 +13,160 @@ using Umbraco.Cms.Core.Models.Email; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Infrastructure.Extensions; +using SecureSocketOptions = MailKit.Security.SecureSocketOptions; using SmtpClient = MailKit.Net.Smtp.SmtpClient; -namespace Umbraco.Cms.Infrastructure.Mail +namespace Umbraco.Cms.Infrastructure.Mail; + +/// +/// A utility class for sending emails +/// +public class EmailSender : IEmailSender { - /// - /// A utility class for sending emails - /// - public class EmailSender : IEmailSender + // TODO: This should encapsulate a BackgroundTaskRunner with a queue to send these emails! + private readonly IEventAggregator _eventAggregator; + private readonly ILogger _logger; + private readonly bool _notificationHandlerRegistered; + private GlobalSettings _globalSettings; + + public EmailSender( + ILogger logger, + IOptionsMonitor globalSettings, + IEventAggregator eventAggregator) + : this(logger, globalSettings, eventAggregator, null, null) { - // TODO: This should encapsulate a BackgroundTaskRunner with a queue to send these emails! - private readonly IEventAggregator _eventAggregator; - private GlobalSettings _globalSettings; - private readonly bool _notificationHandlerRegistered; - private readonly ILogger _logger; - - public EmailSender( - ILogger logger, - IOptionsMonitor globalSettings, - IEventAggregator eventAggregator) - : this(logger, globalSettings, eventAggregator, null, null) { } - - public EmailSender( - ILogger logger, - IOptionsMonitor globalSettings, - IEventAggregator eventAggregator, - INotificationHandler? handler1, - INotificationAsyncHandler? handler2) - { - _logger = logger; - _eventAggregator = eventAggregator; - _globalSettings = globalSettings.CurrentValue; - _notificationHandlerRegistered = handler1 is not null || handler2 is not null; - globalSettings.OnChange(x => _globalSettings = x); - } + } - /// - /// Sends the message async - /// - /// - /// - public async Task SendAsync(EmailMessage message, string emailType) => await SendAsyncInternal(message, emailType, false); + public EmailSender( + ILogger logger, + IOptionsMonitor globalSettings, + IEventAggregator eventAggregator, + INotificationHandler? handler1, + INotificationAsyncHandler? handler2) + { + _logger = logger; + _eventAggregator = eventAggregator; + _globalSettings = globalSettings.CurrentValue; + _notificationHandlerRegistered = handler1 is not null || handler2 is not null; + globalSettings.OnChange(x => _globalSettings = x); + } - public async Task SendAsync(EmailMessage message, string emailType, bool enableNotification) => - await SendAsyncInternal(message, emailType, enableNotification); + /// + /// Sends the message async + /// + /// + /// + public async Task SendAsync(EmailMessage message, string emailType) => + await SendAsyncInternal(message, emailType, false); - private async Task SendAsyncInternal(EmailMessage message, string emailType, bool enableNotification) - { - if (enableNotification) - { - var notification = new SendEmailNotification(message.ToNotificationEmail(_globalSettings.Smtp?.From), emailType); - await _eventAggregator.PublishAsync(notification); + public async Task SendAsync(EmailMessage message, string emailType, bool enableNotification) => + await SendAsyncInternal(message, emailType, enableNotification); - // if a handler handled sending the email then don't continue. - if (notification.IsHandled) - { - _logger.LogDebug("The email sending for {Subject} was handled by a notification handler", notification.Message.Subject); - return; - } - } + /// + /// Returns true if the application should be able to send a required application email + /// + /// + /// We assume this is possible if either an event handler is registered or an smtp server is configured + /// or a pickup directory location is configured + /// + public bool CanSendRequiredEmail() => _globalSettings.IsSmtpServerConfigured + || _globalSettings.IsPickupDirectoryLocationConfigured + || _notificationHandlerRegistered; + + private async Task SendAsyncInternal(EmailMessage message, string emailType, bool enableNotification) + { + if (enableNotification) + { + var notification = + new SendEmailNotification(message.ToNotificationEmail(_globalSettings.Smtp?.From), emailType); + await _eventAggregator.PublishAsync(notification); - if (!_globalSettings.IsSmtpServerConfigured && !_globalSettings.IsPickupDirectoryLocationConfigured) + // if a handler handled sending the email then don't continue. + if (notification.IsHandled) { - _logger.LogDebug("Could not send email for {Subject}. It was not handled by a notification handler and there is no SMTP configured.", message.Subject); + _logger.LogDebug("The email sending for {Subject} was handled by a notification handler", + notification.Message.Subject); return; } + } - if (_globalSettings.IsPickupDirectoryLocationConfigured && !string.IsNullOrWhiteSpace(_globalSettings.Smtp?.From)) - { + if (!_globalSettings.IsSmtpServerConfigured && !_globalSettings.IsPickupDirectoryLocationConfigured) + { + _logger.LogDebug( + "Could not send email for {Subject}. It was not handled by a notification handler and there is no SMTP configured.", + message.Subject); + return; + } + + if (_globalSettings.IsPickupDirectoryLocationConfigured && + !string.IsNullOrWhiteSpace(_globalSettings.Smtp?.From)) + { // The following code snippet is the recommended way to handle PickupDirectoryLocation. // See more https://github.com/jstedfast/MailKit/blob/master/FAQ.md#q-how-can-i-send-email-to-a-specifiedpickupdirectory - do { - var path = Path.Combine(_globalSettings.Smtp.PickupDirectoryLocation!, Guid.NewGuid () + ".eml"); - Stream stream; + do + { + var path = Path.Combine(_globalSettings.Smtp.PickupDirectoryLocation!, Guid.NewGuid() + ".eml"); + Stream stream; - try - { - stream = File.Open(path, FileMode.CreateNew); - } - catch (IOException) + try + { + stream = File.Open(path, FileMode.CreateNew); + } + catch (IOException) + { + if (File.Exists(path)) { - if (File.Exists(path)) - { - continue; - } - throw; + continue; } - try { - using (stream) - { - using var filtered = new FilteredStream(stream); - filtered.Add(new SmtpDataFilter()); + throw; + } - FormatOptions options = FormatOptions.Default.Clone(); - options.NewLineFormat = NewLineFormat.Dos; + try + { + using (stream) + { + using var filtered = new FilteredStream(stream); + filtered.Add(new SmtpDataFilter()); - await message.ToMimeMessage(_globalSettings.Smtp.From).WriteToAsync(options, filtered); - filtered.Flush(); - return; + FormatOptions options = FormatOptions.Default.Clone(); + options.NewLineFormat = NewLineFormat.Dos; - } - } catch { - File.Delete(path); - throw; + await message.ToMimeMessage(_globalSettings.Smtp.From).WriteToAsync(options, filtered); + filtered.Flush(); + return; } - } while (true); - } - - using var client = new SmtpClient(); + } + catch + { + File.Delete(path); + throw; + } + } while (true); + } - await client.ConnectAsync(_globalSettings.Smtp!.Host, - _globalSettings.Smtp.Port, - (MailKit.Security.SecureSocketOptions)(int)_globalSettings.Smtp.SecureSocketOptions); + using var client = new SmtpClient(); - if (!string.IsNullOrWhiteSpace(_globalSettings.Smtp.Username) && !string.IsNullOrWhiteSpace(_globalSettings.Smtp.Password)) - { - await client.AuthenticateAsync(_globalSettings.Smtp.Username, _globalSettings.Smtp.Password); - } + await client.ConnectAsync(_globalSettings.Smtp!.Host, + _globalSettings.Smtp.Port, + (SecureSocketOptions)(int)_globalSettings.Smtp.SecureSocketOptions); - var mailMessage = message.ToMimeMessage(_globalSettings.Smtp.From); - if (_globalSettings.Smtp.DeliveryMethod == SmtpDeliveryMethod.Network) - { - await client.SendAsync(mailMessage); - } - else - { - client.Send(mailMessage); - } + if (!string.IsNullOrWhiteSpace(_globalSettings.Smtp.Username) && + !string.IsNullOrWhiteSpace(_globalSettings.Smtp.Password)) + { + await client.AuthenticateAsync(_globalSettings.Smtp.Username, _globalSettings.Smtp.Password); + } - await client.DisconnectAsync(true); + var mailMessage = message.ToMimeMessage(_globalSettings.Smtp.From); + if (_globalSettings.Smtp.DeliveryMethod == SmtpDeliveryMethod.Network) + { + await client.SendAsync(mailMessage); + } + else + { + client.Send(mailMessage); } - /// - /// Returns true if the application should be able to send a required application email - /// - /// - /// We assume this is possible if either an event handler is registered or an smtp server is configured - /// or a pickup directory location is configured - /// - public bool CanSendRequiredEmail() => _globalSettings.IsSmtpServerConfigured - || _globalSettings.IsPickupDirectoryLocationConfigured - || _notificationHandlerRegistered; + await client.DisconnectAsync(true); } } diff --git a/src/Umbraco.Infrastructure/Manifest/DashboardAccessRuleConverter.cs b/src/Umbraco.Infrastructure/Manifest/DashboardAccessRuleConverter.cs index 1a34fe373cd9..8f067fd3ec0d 100644 --- a/src/Umbraco.Infrastructure/Manifest/DashboardAccessRuleConverter.cs +++ b/src/Umbraco.Infrastructure/Manifest/DashboardAccessRuleConverter.cs @@ -1,46 +1,58 @@ -using System; -using Newtonsoft.Json; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Cms.Core.Dashboards; using Umbraco.Cms.Core.Exceptions; using Umbraco.Cms.Infrastructure.Serialization; -namespace Umbraco.Cms.Core.Manifest +namespace Umbraco.Cms.Core.Manifest; + +/// +/// Implements a json read converter for . +/// +internal class DashboardAccessRuleConverter : JsonReadConverter { - /// - /// Implements a json read converter for . - /// - internal class DashboardAccessRuleConverter : JsonReadConverter + /// + protected override IAccessRule Create(Type objectType, string path, JObject jObject) => new AccessRule(); + + /// + protected override void Deserialize(JObject jobject, IAccessRule target, JsonSerializer serializer) { - /// - protected override IAccessRule Create(Type objectType, string path, JObject jObject) + // see Create above, target is either DataEditor (parameter) or ConfiguredDataEditor (property) + + if (!(target is AccessRule accessRule)) { - return new AccessRule(); + throw new PanicException("panic."); } - /// - protected override void Deserialize(JObject jobject, IAccessRule target, JsonSerializer serializer) - { - // see Create above, target is either DataEditor (parameter) or ConfiguredDataEditor (property) + GetRule(accessRule, jobject, "grant", AccessRuleType.Grant); + GetRule(accessRule, jobject, "deny", AccessRuleType.Deny); + GetRule(accessRule, jobject, "grantBySection", AccessRuleType.GrantBySection); - if (!(target is AccessRule accessRule)) - throw new PanicException("panic."); + if (accessRule.Type == AccessRuleType.Unknown) + { + throw new InvalidOperationException("Rule is not defined."); + } + } - GetRule(accessRule, jobject, "grant", AccessRuleType.Grant); - GetRule(accessRule, jobject, "deny", AccessRuleType.Deny); - GetRule(accessRule, jobject, "grantBySection", AccessRuleType.GrantBySection); + private void GetRule(AccessRule rule, JObject jobject, string name, AccessRuleType type) + { + JToken? token = jobject[name]; + if (token == null) + { + return; + } - if (accessRule.Type == AccessRuleType.Unknown) throw new InvalidOperationException("Rule is not defined."); + if (rule.Type != AccessRuleType.Unknown) + { + throw new InvalidOperationException("Multiple definition of a rule."); } - private void GetRule(AccessRule rule, JObject jobject, string name, AccessRuleType type) + if (token.Type != JTokenType.String) { - var token = jobject[name]; - if (token == null) return; - if (rule.Type != AccessRuleType.Unknown) throw new InvalidOperationException("Multiple definition of a rule."); - if (token.Type != JTokenType.String) throw new InvalidOperationException("Rule value is not a string."); - rule.Type = type; - rule.Value = token.Value(); + throw new InvalidOperationException("Rule value is not a string."); } + + rule.Type = type; + rule.Value = token.Value(); } } diff --git a/src/Umbraco.Infrastructure/Manifest/DataEditorConverter.cs b/src/Umbraco.Infrastructure/Manifest/DataEditorConverter.cs index aa10cd694359..8a3393d81a6c 100644 --- a/src/Umbraco.Infrastructure/Manifest/DataEditorConverter.cs +++ b/src/Umbraco.Infrastructure/Manifest/DataEditorConverter.cs @@ -1,8 +1,5 @@ -using System; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Serialization; @@ -11,198 +8,215 @@ using Umbraco.Cms.Infrastructure.Serialization; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Manifest +namespace Umbraco.Cms.Core.Manifest; + +/// +/// Provides a json read converter for in manifests. +/// +internal class DataEditorConverter : JsonReadConverter { + private readonly IDataValueEditorFactory _dataValueEditorFactory; + private readonly IIOHelper _ioHelper; + private readonly IJsonSerializer _jsonSerializer; + private readonly IShortStringHelper _shortStringHelper; + private readonly ILocalizedTextService _textService; + /// - /// Provides a json read converter for in manifests. + /// Initializes a new instance of the class. /// - internal class DataEditorConverter : JsonReadConverter + public DataEditorConverter( + IDataValueEditorFactory dataValueEditorFactory, + IIOHelper ioHelper, + ILocalizedTextService textService, + IShortStringHelper shortStringHelper, + IJsonSerializer jsonSerializer) { - private readonly IDataValueEditorFactory _dataValueEditorFactory; - private readonly IIOHelper _ioHelper; - private readonly ILocalizedTextService _textService; - private readonly IShortStringHelper _shortStringHelper; - private readonly IJsonSerializer _jsonSerializer; - - /// - /// Initializes a new instance of the class. - /// - public DataEditorConverter( - IDataValueEditorFactory dataValueEditorFactory, - IIOHelper ioHelper, - ILocalizedTextService textService, - IShortStringHelper shortStringHelper, - IJsonSerializer jsonSerializer) - { - _dataValueEditorFactory = dataValueEditorFactory; - _ioHelper = ioHelper; - _textService = textService; - _shortStringHelper = shortStringHelper; - _jsonSerializer = jsonSerializer; - } + _dataValueEditorFactory = dataValueEditorFactory; + _ioHelper = ioHelper; + _textService = textService; + _shortStringHelper = shortStringHelper; + _jsonSerializer = jsonSerializer; + } - /// - protected override IDataEditor Create(Type objectType, string path, JObject jobject) - { - // in PackageManifest, property editors are IConfiguredDataEditor[] whereas - // parameter editors are IDataEditor[] - both will end up here because we handle - // IDataEditor and IConfiguredDataEditor implements it, but we can check the - // type to figure out what to create + /// + protected override IDataEditor Create(Type objectType, string path, JObject jobject) + { + // in PackageManifest, property editors are IConfiguredDataEditor[] whereas + // parameter editors are IDataEditor[] - both will end up here because we handle + // IDataEditor and IConfiguredDataEditor implements it, but we can check the + // type to figure out what to create - var type = EditorType.PropertyValue; + EditorType type = EditorType.PropertyValue; - var isPropertyEditor = path.StartsWith("propertyEditors["); + var isPropertyEditor = path.StartsWith("propertyEditors["); - if (isPropertyEditor) - { - // property editor - jobject["isPropertyEditor"] = JToken.FromObject(true); - if (jobject["isParameterEditor"] is JToken jToken && jToken.Value()) - type |= EditorType.MacroParameter; - } - else + if (isPropertyEditor) + { + // property editor + jobject["isPropertyEditor"] = JToken.FromObject(true); + if (jobject["isParameterEditor"] is JToken jToken && jToken.Value()) { - // parameter editor - type = EditorType.MacroParameter; + type |= EditorType.MacroParameter; } + } + else + { + // parameter editor + type = EditorType.MacroParameter; + } + + return new DataEditor(_dataValueEditorFactory, type); + } - return new DataEditor(_dataValueEditorFactory, type); + /// + protected override void Deserialize(JObject jobject, IDataEditor target, JsonSerializer serializer) + { + // see Create above, target is either DataEditor (parameter) or ConfiguredDataEditor (property) + + if (!(target is DataEditor dataEditor)) + { + throw new Exception("panic."); } - /// - protected override void Deserialize(JObject jobject, IDataEditor target, JsonSerializer serializer) + if (jobject["isPropertyEditor"] is JToken jtoken && jtoken.Value()) { - // see Create above, target is either DataEditor (parameter) or ConfiguredDataEditor (property) + PrepareForPropertyEditor(jobject, dataEditor); + } + else + { + PrepareForParameterEditor(jobject, dataEditor); + } - if (!(target is DataEditor dataEditor)) - throw new Exception("panic."); + base.Deserialize(jobject, target, serializer); + } - if (jobject["isPropertyEditor"] is JToken jtoken && jtoken.Value()) - PrepareForPropertyEditor(jobject, dataEditor); - else - PrepareForParameterEditor(jobject, dataEditor); + private void PrepareForPropertyEditor(JObject jobject, DataEditor target) + { + if (jobject["editor"] == null) + { + throw new InvalidOperationException("Missing 'editor' value."); + } - base.Deserialize(jobject, target, serializer); + // explicitly assign a value editor of type ValueEditor + // (else the deserializer will try to read it before setting it) + // (and besides it's an interface) + target.ExplicitValueEditor = new DataValueEditor(_textService, _shortStringHelper, _jsonSerializer); + + // in the manifest, validators are a simple dictionary eg + // { + // required: true, + // regex: '\\d*' + // } + // and we need to turn this into a list of IPropertyValidator + // so, rewrite the json structure accordingly + if (jobject["editor"]?["validation"] is JObject validation) + { + jobject["editor"]!["validation"] = RewriteValidators(validation); } - private void PrepareForPropertyEditor(JObject jobject, DataEditor target) + if (jobject["editor"]?["view"] is JValue view) { - if (jobject["editor"] == null) - throw new InvalidOperationException("Missing 'editor' value."); + jobject["editor"]!["view"] = RewriteVirtualUrl(view); + } - // explicitly assign a value editor of type ValueEditor + var prevalues = jobject["prevalues"] as JObject; + var defaultConfig = jobject["defaultConfig"] as JObject; + if (prevalues != null || defaultConfig != null) + { + // explicitly assign a configuration editor of type ConfigurationEditor // (else the deserializer will try to read it before setting it) // (and besides it's an interface) - target.ExplicitValueEditor = new DataValueEditor(_textService, _shortStringHelper, _jsonSerializer); + target.ExplicitConfigurationEditor = new ConfigurationEditor(); - // in the manifest, validators are a simple dictionary eg - // { - // required: true, - // regex: '\\d*' - // } - // and we need to turn this into a list of IPropertyValidator - // so, rewrite the json structure accordingly - if (jobject["editor"]?["validation"] is JObject validation) - jobject["editor"]!["validation"] = RewriteValidators(validation); - - if(jobject["editor"]?["view"] is JValue view) - jobject["editor"]!["view"] = RewriteVirtualUrl(view); - - var prevalues = jobject["prevalues"] as JObject; - var defaultConfig = jobject["defaultConfig"] as JObject; - if (prevalues != null || defaultConfig != null) + var config = new JObject(); + if (prevalues != null) { - // explicitly assign a configuration editor of type ConfigurationEditor - // (else the deserializer will try to read it before setting it) - // (and besides it's an interface) - target.ExplicitConfigurationEditor = new ConfigurationEditor(); - - var config = new JObject(); - if (prevalues != null) + config = prevalues; + // see note about validators, above - same applies to field validators + if (config["fields"] is JArray jarray) { - config = prevalues; - // see note about validators, above - same applies to field validators - if (config["fields"] is JArray jarray) + foreach (JToken field in jarray) { - foreach (var field in jarray) + if (field["validation"] is JObject fvalidation) { - if (field["validation"] is JObject fvalidation) - field["validation"] = RewriteValidators(fvalidation); + field["validation"] = RewriteValidators(fvalidation); + } - if(field["view"] is JValue fview) - field["view"] = RewriteVirtualUrl(fview); + if (field["view"] is JValue fview) + { + field["view"] = RewriteVirtualUrl(fview); } } } + } - // in the manifest, default configuration is at editor level - // move it down to configuration editor level so it can be deserialized properly - if (defaultConfig != null) - { - config["defaultConfig"] = defaultConfig; - jobject.Remove("defaultConfig"); - } - - // in the manifest, configuration is named 'prevalues', rename - // it is important to do this LAST - jobject["config"] = config; - jobject.Remove("prevalues"); + // in the manifest, default configuration is at editor level + // move it down to configuration editor level so it can be deserialized properly + if (defaultConfig != null) + { + config["defaultConfig"] = defaultConfig; + jobject.Remove("defaultConfig"); } - } - private string? RewriteVirtualUrl(JValue view) - { - return _ioHelper.ResolveRelativeOrVirtualUrl(view.Value as string); + // in the manifest, configuration is named 'prevalues', rename + // it is important to do this LAST + jobject["config"] = config; + jobject.Remove("prevalues"); } + } - private void PrepareForParameterEditor(JObject jobject, DataEditor target) - { - // in a manifest, a parameter editor looks like: - // - // { - // "alias": "...", - // "name": "...", - // "view": "...", - // "config": { "key1": "value1", "key2": "value2" ... } - // } - // - // the view is at top level, but should be down one level to be properly - // deserialized as a ParameterValueEditor property -> need to move it - - if (jobject.Property("view") != null) - { - // explicitly assign a value editor of type ParameterValueEditor - target.ExplicitValueEditor = new DataValueEditor(_textService, _shortStringHelper, _jsonSerializer); + private string? RewriteVirtualUrl(JValue view) => _ioHelper.ResolveRelativeOrVirtualUrl(view.Value as string); - // move the 'view' property - jobject["editor"] = new JObject { ["view"] = jobject["view"] }; - jobject.Property("view")?.Remove(); - } + private void PrepareForParameterEditor(JObject jobject, DataEditor target) + { + // in a manifest, a parameter editor looks like: + // + // { + // "alias": "...", + // "name": "...", + // "view": "...", + // "config": { "key1": "value1", "key2": "value2" ... } + // } + // + // the view is at top level, but should be down one level to be properly + // deserialized as a ParameterValueEditor property -> need to move it + + if (jobject.Property("view") != null) + { + // explicitly assign a value editor of type ParameterValueEditor + target.ExplicitValueEditor = new DataValueEditor(_textService, _shortStringHelper, _jsonSerializer); - // in the manifest, default configuration is named 'config', rename - if (jobject["config"] is JObject config) - { - jobject["defaultConfig"] = config; - jobject.Remove("config"); - } + // move the 'view' property + jobject["editor"] = new JObject {["view"] = jobject["view"]}; + jobject.Property("view")?.Remove(); + } - if(jobject["editor"]?["view"] is JValue view) // We need to null check, if view do not exists, then editor do not exists - jobject["editor"]!["view"] = RewriteVirtualUrl(view); + // in the manifest, default configuration is named 'config', rename + if (jobject["config"] is JObject config) + { + jobject["defaultConfig"] = config; + jobject.Remove("config"); } - private static JArray RewriteValidators(JObject validation) + if (jobject["editor"]?[ + "view"] is JValue view) // We need to null check, if view do not exists, then editor do not exists { - var jarray = new JArray(); + jobject["editor"]!["view"] = RewriteVirtualUrl(view); + } + } - foreach (var v in validation) - { - var key = v.Key; - var val = v.Value; - var jo = new JObject { { "type", key }, { "configuration", val } }; - jarray.Add(jo); - } + private static JArray RewriteValidators(JObject validation) + { + var jarray = new JArray(); - return jarray; + foreach (KeyValuePair v in validation) + { + var key = v.Key; + JToken? val = v.Value; + var jo = new JObject {{"type", key}, {"configuration", val}}; + jarray.Add(jo); } + + return jarray; } } diff --git a/src/Umbraco.Infrastructure/Manifest/ManifestParser.cs b/src/Umbraco.Infrastructure/Manifest/ManifestParser.cs index bdf5e5c620bd..7fb59e882049 100644 --- a/src/Umbraco.Infrastructure/Manifest/ManifestParser.cs +++ b/src/Umbraco.Infrastructure/Manifest/ManifestParser.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Text; using Microsoft.Extensions.Logging; using Newtonsoft.Json; @@ -14,248 +10,271 @@ using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Manifest +namespace Umbraco.Cms.Core.Manifest; + +/// +/// Parses the Main.js file and replaces all tokens accordingly. +/// +public class ManifestParser : IManifestParser { + private static readonly string s_utf8Preamble = Encoding.UTF8.GetString(Encoding.UTF8.GetPreamble()); + + private readonly IAppPolicyCache _cache; + private readonly IDataValueEditorFactory _dataValueEditorFactory; + private readonly ManifestFilterCollection _filters; + private readonly IHostingEnvironment _hostingEnvironment; + + private readonly IIOHelper _ioHelper; + private readonly IJsonSerializer _jsonSerializer; + private readonly ILocalizedTextService _localizedTextService; + private readonly ILogger _logger; + private readonly IShortStringHelper _shortStringHelper; + private readonly ManifestValueValidatorCollection _validators; + + private string _path = null!; + /// - /// Parses the Main.js file and replaces all tokens accordingly. + /// Initializes a new instance of the class. /// - public class ManifestParser : IManifestParser + public ManifestParser( + AppCaches appCaches, + ManifestValueValidatorCollection validators, + ManifestFilterCollection filters, + ILogger logger, + IIOHelper ioHelper, + IHostingEnvironment hostingEnvironment, + IJsonSerializer jsonSerializer, + ILocalizedTextService localizedTextService, + IShortStringHelper shortStringHelper, + IDataValueEditorFactory dataValueEditorFactory) { - - private readonly IIOHelper _ioHelper; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly IJsonSerializer _jsonSerializer; - private readonly ILocalizedTextService _localizedTextService; - private readonly IShortStringHelper _shortStringHelper; - private readonly IDataValueEditorFactory _dataValueEditorFactory; - private static readonly string s_utf8Preamble = Encoding.UTF8.GetString(Encoding.UTF8.GetPreamble()); - - private readonly IAppPolicyCache _cache; - private readonly ILogger _logger; - private readonly ManifestValueValidatorCollection _validators; - private readonly ManifestFilterCollection _filters; - - private string _path = null!; - - /// - /// Initializes a new instance of the class. - /// - public ManifestParser( - AppCaches appCaches, - ManifestValueValidatorCollection validators, - ManifestFilterCollection filters, - ILogger logger, - IIOHelper ioHelper, - IHostingEnvironment hostingEnvironment, - IJsonSerializer jsonSerializer, - ILocalizedTextService localizedTextService, - IShortStringHelper shortStringHelper, - IDataValueEditorFactory dataValueEditorFactory) + if (appCaches == null) { - if (appCaches == null) throw new ArgumentNullException(nameof(appCaches)); - _cache = appCaches.RuntimeCache; - _validators = validators ?? throw new ArgumentNullException(nameof(validators)); - _filters = filters ?? throw new ArgumentNullException(nameof(filters)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _ioHelper = ioHelper; - _hostingEnvironment = hostingEnvironment; - AppPluginsPath = "~/App_Plugins"; - _jsonSerializer = jsonSerializer; - _localizedTextService = localizedTextService; - _shortStringHelper = shortStringHelper; - _dataValueEditorFactory = dataValueEditorFactory; + throw new ArgumentNullException(nameof(appCaches)); } - public string AppPluginsPath - { - get => _path; - set => _path = value.StartsWith("~/") ? _hostingEnvironment.MapPathContentRoot(value) : value; - } - - /// - /// Gets all manifests, merged into a single manifest object. - /// - /// - public CompositePackageManifest CombinedManifest - => _cache.GetCacheItem("Umbraco.Core.Manifest.ManifestParser::Manifests", () => - { - IEnumerable manifests = GetManifests(); - return MergeManifests(manifests); + _cache = appCaches.RuntimeCache; + _validators = validators ?? throw new ArgumentNullException(nameof(validators)); + _filters = filters ?? throw new ArgumentNullException(nameof(filters)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _ioHelper = ioHelper; + _hostingEnvironment = hostingEnvironment; + AppPluginsPath = "~/App_Plugins"; + _jsonSerializer = jsonSerializer; + _localizedTextService = localizedTextService; + _shortStringHelper = shortStringHelper; + _dataValueEditorFactory = dataValueEditorFactory; + } - }, new TimeSpan(0, 4, 0))!; + public string AppPluginsPath + { + get => _path; + set => _path = value.StartsWith("~/") ? _hostingEnvironment.MapPathContentRoot(value) : value; + } - /// - /// Gets all manifests. - /// - public IEnumerable GetManifests() + /// + /// Gets all manifests, merged into a single manifest object. + /// + /// + public CompositePackageManifest CombinedManifest + => _cache.GetCacheItem("Umbraco.Core.Manifest.ManifestParser::Manifests", () => { - var manifests = new List(); + IEnumerable manifests = GetManifests(); + return MergeManifests(manifests); + }, new TimeSpan(0, 4, 0))!; + + /// + /// Gets all manifests. + /// + public IEnumerable GetManifests() + { + var manifests = new List(); - foreach (var path in GetManifestFiles()) + foreach (var path in GetManifestFiles()) + { + try { - try - { - var text = File.ReadAllText(path); - text = TrimPreamble(text); - if (string.IsNullOrWhiteSpace(text)) - { - continue; - } - - PackageManifest manifest = ParseManifest(text); - manifest.Source = path; - manifests.Add(manifest); - } - catch (Exception e) + var text = File.ReadAllText(path); + text = TrimPreamble(text); + if (string.IsNullOrWhiteSpace(text)) { - _logger.LogError(e, "Failed to parse manifest at '{Path}', ignoring.", path); + continue; } + + PackageManifest manifest = ParseManifest(text); + manifest.Source = path; + manifests.Add(manifest); } + catch (Exception e) + { + _logger.LogError(e, "Failed to parse manifest at '{Path}', ignoring.", path); + } + } + + _filters.Filter(manifests); - _filters.Filter(manifests); + return manifests; + } - return manifests; + /// + /// Parses a manifest. + /// + public PackageManifest ParseManifest(string text) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); } - /// - /// Merges all manifests into one. - /// - private static CompositePackageManifest MergeManifests(IEnumerable manifests) + if (string.IsNullOrWhiteSpace(text)) { - var scripts = new Dictionary>(); - var stylesheets = new Dictionary>(); - var propertyEditors = new List(); - var parameterEditors = new List(); - var gridEditors = new List(); - var contentApps = new List(); - var dashboards = new List(); - var sections = new List(); - - foreach (PackageManifest manifest in manifests) - { - if (manifest.Scripts != null) - { - if (!scripts.TryGetValue(manifest.BundleOptions, out List? scriptsPerBundleOption)) - { - scriptsPerBundleOption = new List(); - scripts[manifest.BundleOptions] = scriptsPerBundleOption; - } + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", + nameof(text)); + } - scriptsPerBundleOption.Add(new ManifestAssets(manifest.PackageName, manifest.Scripts)); - } + PackageManifest? manifest = JsonConvert.DeserializeObject(text, + new DataEditorConverter(_dataValueEditorFactory, _ioHelper, _localizedTextService, _shortStringHelper, + _jsonSerializer), + new ValueValidatorConverter(_validators), + new DashboardAccessRuleConverter()); - if (manifest.Stylesheets != null) - { - if (!stylesheets.TryGetValue(manifest.BundleOptions, out List? stylesPerBundleOption)) - { - stylesPerBundleOption = new List(); - stylesheets[manifest.BundleOptions] = stylesPerBundleOption; - } + // scripts and stylesheets are raw string, must process here + for (var i = 0; i < manifest!.Scripts.Length; i++) + { + manifest.Scripts[i] = _ioHelper.ResolveRelativeOrVirtualUrl(manifest.Scripts[i])!; + } - stylesPerBundleOption.Add(new ManifestAssets(manifest.PackageName, manifest.Stylesheets)); - } + for (var i = 0; i < manifest.Stylesheets.Length; i++) + { + manifest.Stylesheets[i] = _ioHelper.ResolveRelativeOrVirtualUrl(manifest.Stylesheets[i])!; + } - if (manifest.PropertyEditors != null) - { - propertyEditors.AddRange(manifest.PropertyEditors); - } + foreach (ManifestContentAppDefinition contentApp in manifest.ContentApps) + { + contentApp.View = _ioHelper.ResolveRelativeOrVirtualUrl(contentApp.View); + } - if (manifest.ParameterEditors != null) - { - parameterEditors.AddRange(manifest.ParameterEditors); - } + foreach (ManifestDashboard dashboard in manifest.Dashboards) + { + dashboard.View = _ioHelper.ResolveRelativeOrVirtualUrl(dashboard.View)!; + } - if (manifest.GridEditors != null) - { - gridEditors.AddRange(manifest.GridEditors); - } + foreach (GridEditor gridEditor in manifest.GridEditors) + { + gridEditor.View = _ioHelper.ResolveRelativeOrVirtualUrl(gridEditor.View); + gridEditor.Render = _ioHelper.ResolveRelativeOrVirtualUrl(gridEditor.Render); + } - if (manifest.ContentApps != null) - { - contentApps.AddRange(manifest.ContentApps); - } + // add property editors that are also parameter editors, to the parameter editors list + // (the manifest format is kinda legacy) + var ppEditors = manifest.PropertyEditors.Where(x => (x.Type & EditorType.MacroParameter) > 0).ToList(); + if (ppEditors.Count > 0) + { + manifest.ParameterEditors = manifest.ParameterEditors.Union(ppEditors).ToArray(); + } + + return manifest; + } - if (manifest.Dashboards != null) + /// + /// Merges all manifests into one. + /// + private static CompositePackageManifest MergeManifests(IEnumerable manifests) + { + var scripts = new Dictionary>(); + var stylesheets = new Dictionary>(); + var propertyEditors = new List(); + var parameterEditors = new List(); + var gridEditors = new List(); + var contentApps = new List(); + var dashboards = new List(); + var sections = new List(); + + foreach (PackageManifest manifest in manifests) + { + if (manifest.Scripts != null) + { + if (!scripts.TryGetValue(manifest.BundleOptions, out List? scriptsPerBundleOption)) { - dashboards.AddRange(manifest.Dashboards); + scriptsPerBundleOption = new List(); + scripts[manifest.BundleOptions] = scriptsPerBundleOption; } - if (manifest.Sections != null) + scriptsPerBundleOption.Add(new ManifestAssets(manifest.PackageName, manifest.Scripts)); + } + + if (manifest.Stylesheets != null) + { + if (!stylesheets.TryGetValue(manifest.BundleOptions, out List? stylesPerBundleOption)) { - sections.AddRange(manifest.Sections.DistinctBy(x => x.Alias, StringComparer.OrdinalIgnoreCase)); + stylesPerBundleOption = new List(); + stylesheets[manifest.BundleOptions] = stylesPerBundleOption; } - } - return new CompositePackageManifest( - propertyEditors, - parameterEditors, - gridEditors, - contentApps, - dashboards, - sections, - scripts.ToDictionary(x => x.Key, x => (IReadOnlyList)x.Value), - stylesheets.ToDictionary(x => x.Key, x => (IReadOnlyList)x.Value)); - } + stylesPerBundleOption.Add(new ManifestAssets(manifest.PackageName, manifest.Stylesheets)); + } - // gets all manifest files (recursively) - private IEnumerable GetManifestFiles() - { - if (Directory.Exists(_path) == false) + if (manifest.PropertyEditors != null) { - return Array.Empty(); + propertyEditors.AddRange(manifest.PropertyEditors); } - return Directory.GetFiles(_path, "package.manifest", SearchOption.AllDirectories); - } - - private static string TrimPreamble(string text) - { - // strangely StartsWith(preamble) would always return true - if (text.Substring(0, 1) == s_utf8Preamble) - text = text.Remove(0, s_utf8Preamble.Length); + if (manifest.ParameterEditors != null) + { + parameterEditors.AddRange(manifest.ParameterEditors); + } - return text; - } + if (manifest.GridEditors != null) + { + gridEditors.AddRange(manifest.GridEditors); + } - /// - /// Parses a manifest. - /// - public PackageManifest ParseManifest(string text) - { - if (text == null) throw new ArgumentNullException(nameof(text)); - if (string.IsNullOrWhiteSpace(text)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(text)); - - var manifest = JsonConvert.DeserializeObject(text, - new DataEditorConverter(_dataValueEditorFactory, _ioHelper, _localizedTextService, _shortStringHelper, _jsonSerializer), - new ValueValidatorConverter(_validators), - new DashboardAccessRuleConverter()); - - // scripts and stylesheets are raw string, must process here - for (var i = 0; i < manifest!.Scripts.Length; i++) - manifest.Scripts[i] = _ioHelper.ResolveRelativeOrVirtualUrl(manifest.Scripts[i])!; - for (var i = 0; i < manifest.Stylesheets.Length; i++) - manifest.Stylesheets[i] = _ioHelper.ResolveRelativeOrVirtualUrl(manifest.Stylesheets[i])!; - foreach (var contentApp in manifest.ContentApps) + if (manifest.ContentApps != null) { - contentApp.View = _ioHelper.ResolveRelativeOrVirtualUrl(contentApp.View); + contentApps.AddRange(manifest.ContentApps); } - foreach (var dashboard in manifest.Dashboards) + + if (manifest.Dashboards != null) { - dashboard.View = _ioHelper.ResolveRelativeOrVirtualUrl(dashboard.View)!; + dashboards.AddRange(manifest.Dashboards); } - foreach (var gridEditor in manifest.GridEditors) + + if (manifest.Sections != null) { - gridEditor.View = _ioHelper.ResolveRelativeOrVirtualUrl(gridEditor.View); - gridEditor.Render = _ioHelper.ResolveRelativeOrVirtualUrl(gridEditor.Render); + sections.AddRange(manifest.Sections.DistinctBy(x => x.Alias, StringComparer.OrdinalIgnoreCase)); } + } - // add property editors that are also parameter editors, to the parameter editors list - // (the manifest format is kinda legacy) - var ppEditors = manifest.PropertyEditors.Where(x => (x.Type & EditorType.MacroParameter) > 0).ToList(); - if (ppEditors.Count > 0) - manifest.ParameterEditors = manifest.ParameterEditors.Union(ppEditors).ToArray(); + return new CompositePackageManifest( + propertyEditors, + parameterEditors, + gridEditors, + contentApps, + dashboards, + sections, + scripts.ToDictionary(x => x.Key, x => (IReadOnlyList)x.Value), + stylesheets.ToDictionary(x => x.Key, x => (IReadOnlyList)x.Value)); + } - return manifest; + // gets all manifest files (recursively) + private IEnumerable GetManifestFiles() + { + if (Directory.Exists(_path) == false) + { + return Array.Empty(); } + + return Directory.GetFiles(_path, "package.manifest", SearchOption.AllDirectories); + } + + private static string TrimPreamble(string text) + { + // strangely StartsWith(preamble) would always return true + if (text.Substring(0, 1) == s_utf8Preamble) + { + text = text.Remove(0, s_utf8Preamble.Length); + } + + return text; } } diff --git a/src/Umbraco.Infrastructure/Manifest/ValueValidatorConverter.cs b/src/Umbraco.Infrastructure/Manifest/ValueValidatorConverter.cs index 6d6483a8bbc4..28111785a3ea 100644 --- a/src/Umbraco.Infrastructure/Manifest/ValueValidatorConverter.cs +++ b/src/Umbraco.Infrastructure/Manifest/ValueValidatorConverter.cs @@ -1,34 +1,31 @@ -using System; -using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Linq; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Infrastructure.Serialization; -namespace Umbraco.Cms.Core.Manifest +namespace Umbraco.Cms.Core.Manifest; + +/// +/// Implements a json read converter for . +/// +internal class ValueValidatorConverter : JsonReadConverter { + private readonly ManifestValueValidatorCollection _validators; + /// - /// Implements a json read converter for . + /// Initializes a new instance of the class. /// - internal class ValueValidatorConverter : JsonReadConverter - { - private readonly ManifestValueValidatorCollection _validators; + public ValueValidatorConverter(ManifestValueValidatorCollection validators) => _validators = validators; - /// - /// Initializes a new instance of the class. - /// - public ValueValidatorConverter(ManifestValueValidatorCollection validators) + protected override IValueValidator Create(Type objectType, string path, JObject jObject) + { + var type = jObject["type"]?.Value(); + if (string.IsNullOrWhiteSpace(type)) { - _validators = validators; + throw new InvalidOperationException("Could not get the type of the validator."); } - protected override IValueValidator Create(Type objectType, string path, JObject jObject) - { - var type = jObject["type"]?.Value(); - if (string.IsNullOrWhiteSpace(type)) - throw new InvalidOperationException("Could not get the type of the validator."); - - return _validators.GetByName(type); + return _validators.GetByName(type); - // jObject["configuration"] is going to be deserialized in a Configuration property, if any - } + // jObject["configuration"] is going to be deserialized in a Configuration property, if any } } diff --git a/src/Umbraco.Infrastructure/Mapping/UmbracoMapper.cs b/src/Umbraco.Infrastructure/Mapping/UmbracoMapper.cs index 7a73e9303f5f..9f03a00f0a45 100644 --- a/src/Umbraco.Infrastructure/Mapping/UmbracoMapper.cs +++ b/src/Umbraco.Infrastructure/Mapping/UmbracoMapper.cs @@ -1,492 +1,560 @@ -using System; using System.Collections; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Exceptions; using Umbraco.Cms.Core.Scoping; -using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Mapping +namespace Umbraco.Cms.Core.Mapping; +// notes: +// AutoMapper maps null to empty arrays, lists, etc + +// TODO: +// when mapping from TSource, and no map is found, consider the actual source.GetType()? +// when mapping to TTarget, and no map is found, consider the actual target.GetType()? +// not sure we want to add magic to this simple mapper class, though + +/// +/// Umbraco Mapper. +/// +/// +/// +/// When a map is defined from TSource to TTarget, the mapper automatically knows how to map +/// from IEnumerable{TSource} to IEnumerable{TTarget} (using a List{TTarget}) and to TTarget[]. +/// +/// +/// When a map is defined from TSource to TTarget, the mapper automatically uses that map +/// for any source type that inherits from, or implements, TSource. +/// +/// +/// When a map is defined from TSource to TTarget, the mapper can map to TTarget exclusively +/// and cannot re-use that map for types that would inherit from, or implement, TTarget. +/// +/// +/// When using the Map{TSource, TTarget}(TSource source, ...) overloads, TSource is explicit. When +/// using the Map{TTarget}(object source, ...) TSource is defined as source.GetType(). +/// +/// In both cases, TTarget is explicit and not typeof(target). +/// +public class UmbracoMapper : IUmbracoMapper { - // notes: - // AutoMapper maps null to empty arrays, lists, etc + // note + // + // the outer dictionary *can* be modified, see GetCtor and GetMap, hence have to be ConcurrentDictionary + // the inner dictionaries are never modified and therefore can be simple Dictionary - // TODO: - // when mapping from TSource, and no map is found, consider the actual source.GetType()? - // when mapping to TTarget, and no map is found, consider the actual target.GetType()? - // not sure we want to add magic to this simple mapper class, though + private readonly ConcurrentDictionary>> _ctors = + new(); + + private readonly ConcurrentDictionary>> _maps = + new(); + + private readonly ICoreScopeProvider _scopeProvider; /// - /// Umbraco Mapper. + /// Initializes a new instance of the class. /// - /// - /// When a map is defined from TSource to TTarget, the mapper automatically knows how to map - /// from IEnumerable{TSource} to IEnumerable{TTarget} (using a List{TTarget}) and to TTarget[]. - /// When a map is defined from TSource to TTarget, the mapper automatically uses that map - /// for any source type that inherits from, or implements, TSource. - /// When a map is defined from TSource to TTarget, the mapper can map to TTarget exclusively - /// and cannot re-use that map for types that would inherit from, or implement, TTarget. - /// When using the Map{TSource, TTarget}(TSource source, ...) overloads, TSource is explicit. When - /// using the Map{TTarget}(object source, ...) TSource is defined as source.GetType(). - /// In both cases, TTarget is explicit and not typeof(target). - /// - public class UmbracoMapper : IUmbracoMapper + /// + /// + public UmbracoMapper(MapDefinitionCollection profiles, ICoreScopeProvider scopeProvider) { - // note - // - // the outer dictionary *can* be modified, see GetCtor and GetMap, hence have to be ConcurrentDictionary - // the inner dictionaries are never modified and therefore can be simple Dictionary + _scopeProvider = scopeProvider; - private readonly ConcurrentDictionary>> _ctors - = new ConcurrentDictionary>>(); + foreach (IMapDefinition profile in profiles) + { + profile.DefineMaps(this); + } + } - private readonly ConcurrentDictionary>> _maps - = new ConcurrentDictionary>>(); + #region Define - private readonly ICoreScopeProvider _scopeProvider; + private static TTarget ThrowCtor(TSource source, MapperContext context) + => throw new InvalidOperationException($"Don't know how to create {typeof(TTarget).FullName} instances."); - /// - /// Initializes a new instance of the class. - /// - /// - /// - public UmbracoMapper(MapDefinitionCollection profiles, ICoreScopeProvider scopeProvider) - { - _scopeProvider = scopeProvider; + private static void Identity(TSource source, TTarget target, MapperContext context) + { + } - foreach (var profile in profiles) - profile.DefineMaps(this); - } + /// + /// Defines a mapping. + /// + /// The source type. + /// The target type. + public void Define() + => Define(ThrowCtor, Identity); - #region Define - - private static TTarget ThrowCtor(TSource source, MapperContext context) - => throw new InvalidOperationException($"Don't know how to create {typeof(TTarget).FullName} instances."); - - private static void Identity(TSource source, TTarget target, MapperContext context) - { } - - /// - /// Defines a mapping. - /// - /// The source type. - /// The target type. - public void Define() - => Define(ThrowCtor, Identity); - - /// - /// Defines a mapping. - /// - /// The source type. - /// The target type. - /// A mapping method. - public void Define(Action map) - => Define(ThrowCtor, map); - - /// - /// Defines a mapping. - /// - /// The source type. - /// The target type. - /// A constructor method. - public void Define(Func ctor) - => Define(ctor, Identity); - - /// - /// Defines a mapping. - /// - /// The source type. - /// The target type. - /// A constructor method. - /// A mapping method. - public void Define(Func ctor, Action map) - { - var sourceType = typeof(TSource); - var targetType = typeof(TTarget); + /// + /// Defines a mapping. + /// + /// The source type. + /// The target type. + /// A mapping method. + public void Define(Action map) + => Define(ThrowCtor, map); - var sourceCtors = DefineCtors(sourceType); - if (ctor != null) - sourceCtors[targetType] = (source, context) => ctor((TSource)source, context)!; + /// + /// Defines a mapping. + /// + /// The source type. + /// The target type. + /// A constructor method. + public void Define(Func ctor) + => Define(ctor, Identity); - var sourceMaps = DefineMaps(sourceType); - sourceMaps[targetType] = (source, target, context) => map((TSource)source, (TTarget)target, context); - } + /// + /// Defines a mapping. + /// + /// The source type. + /// The target type. + /// A constructor method. + /// A mapping method. + public void Define(Func ctor, + Action map) + { + Type sourceType = typeof(TSource); + Type targetType = typeof(TTarget); - private Dictionary> DefineCtors(Type sourceType) + Dictionary> sourceCtors = DefineCtors(sourceType); + if (ctor != null) { - return _ctors.GetOrAdd(sourceType, _ => new Dictionary>()); + sourceCtors[targetType] = (source, context) => ctor((TSource)source, context)!; } - private Dictionary> DefineMaps(Type sourceType) + Dictionary> sourceMaps = DefineMaps(sourceType); + sourceMaps[targetType] = (source, target, context) => map((TSource)source, (TTarget)target, context); + } + + private Dictionary> DefineCtors(Type sourceType) => + _ctors.GetOrAdd(sourceType, _ => new Dictionary>()); + + private Dictionary> DefineMaps(Type sourceType) => + _maps.GetOrAdd(sourceType, _ => new Dictionary>()); + + #endregion + + #region Map + + /// + /// Maps a source object to a new target object. + /// + /// The target type. + /// The source object. + /// The target object. + public TTarget? Map(object? source) + => Map(source, new MapperContext(this)); + + /// + /// Maps a source object to a new target object. + /// + /// The target type. + /// The source object. + /// A mapper context preparation method. + /// The target object. + public TTarget? Map(object? source, Action f) + { + var context = new MapperContext(this); + f(context); + return Map(source, context); + } + + /// + /// Maps a source object to a new target object. + /// + /// The target type. + /// The source object. + /// A mapper context. + /// The target object. + public TTarget? Map(object? source, MapperContext context) + => Map(source, source?.GetType(), context); + + /// + /// Maps a source object to a new target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// The target object. + public TTarget? Map(TSource? source) + => Map(source, new MapperContext(this)); + + /// + /// Maps a source object to a new target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// A mapper context preparation method. + /// The target object. + public TTarget? Map(TSource source, Action f) + { + var context = new MapperContext(this); + f(context); + return Map(source, context); + } + + /// + /// Maps a source object to a new target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// A mapper context. + /// The target object. + public TTarget? Map(TSource? source, MapperContext context) + => Map(source, typeof(TSource), context); + + private TTarget? Map(object? source, Type? sourceType, MapperContext context) + { + if (source == null) { - return _maps.GetOrAdd(sourceType, _ => new Dictionary>()); + return default; } - #endregion - - #region Map - - /// - /// Maps a source object to a new target object. - /// - /// The target type. - /// The source object. - /// The target object. - public TTarget? Map(object? source) - => Map(source, new MapperContext(this)); - - /// - /// Maps a source object to a new target object. - /// - /// The target type. - /// The source object. - /// A mapper context preparation method. - /// The target object. - public TTarget? Map(object? source, Action f) + Type targetType = typeof(TTarget); + + Func? ctor = GetCtor(sourceType, targetType); + Action? map = GetMap(sourceType, targetType); + + // if there is a direct constructor, map + if (ctor != null && map != null) { - var context = new MapperContext(this); - f(context); - return Map(source, context); + var target = ctor(source, context); + using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true)) + { + map(source, target, context); + } + + return (TTarget)target; } - /// - /// Maps a source object to a new target object. - /// - /// The target type. - /// The source object. - /// A mapper context. - /// The target object. - public TTarget? Map(object? source, MapperContext context) - => Map(source, source?.GetType(), context); - - /// - /// Maps a source object to a new target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// The target object. - public TTarget? Map(TSource? source) - => Map(source, new MapperContext(this)); - - /// - /// Maps a source object to a new target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// A mapper context preparation method. - /// The target object. - public TTarget? Map(TSource source, Action f) + // otherwise, see if we can deal with enumerable + + Type? ienumerableOfT = typeof(IEnumerable<>); + + bool IsIEnumerableOfT(Type? type) { - var context = new MapperContext(this); - f(context); - return Map(source, context); + return type is not null && + type.IsGenericType && + type.GenericTypeArguments.Length == 1 && + type.GetGenericTypeDefinition() == ienumerableOfT; } - /// - /// Maps a source object to a new target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// A mapper context. - /// The target object. - public TTarget? Map(TSource? source, MapperContext context) - => Map(source, typeof(TSource), context); - - private TTarget? Map(object? source, Type? sourceType, MapperContext context) - { - if (source == null) - return default; + // try to get source as an IEnumerable + Type? sourceIEnumerable = IsIEnumerableOfT(sourceType) + ? sourceType + : sourceType?.GetInterfaces().FirstOrDefault(IsIEnumerableOfT); - var targetType = typeof(TTarget); + // if source is an IEnumerable and target is T[] or IEnumerable, we can create a map + if (sourceIEnumerable != null && IsEnumerableOrArrayOfType(targetType)) + { + Type sourceGenericArg = sourceIEnumerable.GenericTypeArguments[0]; + Type? targetGenericArg = GetEnumerableOrArrayTypeArgument(targetType); - var ctor = GetCtor(sourceType, targetType); - var map = GetMap(sourceType, targetType); + ctor = GetCtor(sourceGenericArg, targetGenericArg); + map = GetMap(sourceGenericArg, targetGenericArg); - // if there is a direct constructor, map + // if there is a constructor for the underlying type, create & invoke the map if (ctor != null && map != null) { - var target = ctor(source, context); - using (var scope = _scopeProvider.CreateCoreScope(autoComplete: true)) + // register (for next time) and do it now (for this time) + object NCtor(object s, MapperContext c) { - map(source, target, context); + return MapEnumerableInternal((IEnumerable)s, targetGenericArg!, ctor, map, c)!; } - return (TTarget)target; - } - // otherwise, see if we can deal with enumerable + DefineCtors(sourceType!)[targetType] = NCtor; + DefineMaps(sourceType!)[targetType] = Identity; + return (TTarget)NCtor(source, context); + } - var ienumerableOfT = typeof(IEnumerable<>); + throw new InvalidOperationException( + $"Don't know how to map {sourceGenericArg.FullName} to {targetGenericArg?.FullName}, so don't know how to map {sourceType?.FullName} to {targetType.FullName}."); + } - bool IsIEnumerableOfT(Type? type) => - type is not null && - type.IsGenericType && - type.GenericTypeArguments.Length == 1 && - type.GetGenericTypeDefinition() == ienumerableOfT; + throw new InvalidOperationException($"Don't know how to map {sourceType?.FullName} to {targetType.FullName}."); + } - // try to get source as an IEnumerable - var sourceIEnumerable = IsIEnumerableOfT(sourceType) ? sourceType : sourceType?.GetInterfaces().FirstOrDefault(IsIEnumerableOfT); + private TTarget? MapEnumerableInternal(IEnumerable source, Type targetGenericArg, + Func ctor, Action map, MapperContext context) + { + var targetList = (IList?)Activator.CreateInstance(typeof(List<>).MakeGenericType(targetGenericArg)); - // if source is an IEnumerable and target is T[] or IEnumerable, we can create a map - if (sourceIEnumerable != null && IsEnumerableOrArrayOfType(targetType)) + using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true)) + { + foreach (var sourceItem in source) { - var sourceGenericArg = sourceIEnumerable.GenericTypeArguments[0]; - var targetGenericArg = GetEnumerableOrArrayTypeArgument(targetType); - - ctor = GetCtor(sourceGenericArg, targetGenericArg); - map = GetMap(sourceGenericArg, targetGenericArg); + var targetItem = ctor(sourceItem, context); + map(sourceItem, targetItem, context); + targetList?.Add(targetItem); + } + } - // if there is a constructor for the underlying type, create & invoke the map - if (ctor != null && map != null) - { - // register (for next time) and do it now (for this time) - object NCtor(object s, MapperContext c) => MapEnumerableInternal((IEnumerable)s, targetGenericArg!, ctor, map, c)!; - DefineCtors(sourceType!)[targetType] = NCtor; - DefineMaps(sourceType!)[targetType] = Identity; - return (TTarget)NCtor(source, context); - } + object? target = targetList; - throw new InvalidOperationException($"Don't know how to map {sourceGenericArg.FullName} to {targetGenericArg?.FullName}, so don't know how to map {sourceType?.FullName} to {targetType.FullName}."); + if (typeof(TTarget).IsArray) + { + Type? elementType = typeof(TTarget).GetElementType(); + if (elementType == null) + { + throw new PanicException("elementType == null which should never occur"); } - throw new InvalidOperationException($"Don't know how to map {sourceType?.FullName} to {targetType.FullName}."); + var targetArray = Array.CreateInstance(elementType, targetList?.Count ?? 0); + targetList?.CopyTo(targetArray, 0); + target = targetArray; } - private TTarget? MapEnumerableInternal(IEnumerable source, Type targetGenericArg, Func ctor, Action map, MapperContext context) - { - var targetList = (IList?)Activator.CreateInstance(typeof(List<>).MakeGenericType(targetGenericArg)); + return (TTarget?)target; + } - using (var scope = _scopeProvider.CreateCoreScope(autoComplete: true)) - { - foreach (var sourceItem in source) - { - var targetItem = ctor(sourceItem, context); - map(sourceItem, targetItem, context); - targetList?.Add(targetItem); - } - } + /// + /// Maps a source object to an existing target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// The target object. + /// The target object. + public TTarget Map(TSource source, TTarget target) + => Map(source, target, new MapperContext(this)); - object? target = targetList; + /// + /// Maps a source object to an existing target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// The target object. + /// A mapper context preparation method. + /// The target object. + public TTarget Map(TSource source, TTarget target, Action f) + { + var context = new MapperContext(this); + f(context); + return Map(source, target, context); + } - if (typeof(TTarget).IsArray) + /// + /// Maps a source object to an existing target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// The target object. + /// A mapper context. + /// The target object. + public TTarget Map(TSource source, TTarget target, MapperContext context) + { + Type sourceType = typeof(TSource); + Type targetType = typeof(TTarget); + + Action? map = GetMap(sourceType, targetType); + + // if there is a direct map, map + if (map != null) + { + using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true)) { - var elementType = typeof(TTarget).GetElementType(); - if (elementType == null) throw new PanicException("elementType == null which should never occur"); - var targetArray = Array.CreateInstance(elementType, targetList?.Count ?? 0); - targetList?.CopyTo(targetArray, 0); - target = targetArray; + map(source!, target!, context); } - return (TTarget?)target; + return target; } - /// - /// Maps a source object to an existing target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// The target object. - /// The target object. - public TTarget Map(TSource source, TTarget target) - => Map(source, target, new MapperContext(this)); - - /// - /// Maps a source object to an existing target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// The target object. - /// A mapper context preparation method. - /// The target object. - public TTarget Map(TSource source, TTarget target, Action f) + // we cannot really map to an existing enumerable - give up + + throw new InvalidOperationException( + $"Don't know how to map {typeof(TSource).FullName} to {typeof(TTarget).FullName}."); + } + + private Func? GetCtor(Type? sourceType, Type? targetType) + { + if (sourceType is null || targetType is null) { - var context = new MapperContext(this); - f(context); - return Map(source, target, context); + return null; } - /// - /// Maps a source object to an existing target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// The target object. - /// A mapper context. - /// The target object. - public TTarget Map(TSource source, TTarget target, MapperContext context) + if (_ctors.TryGetValue(sourceType, out Dictionary>? sourceCtor) && + sourceCtor.TryGetValue(targetType, out Func? ctor)) { - var sourceType = typeof(TSource); - var targetType = typeof(TTarget); + return ctor; + } - var map = GetMap(sourceType, targetType); + // we *may* run this more than once but it does not matter - // if there is a direct map, map - if (map != null) + ctor = null; + foreach ((Type stype, Dictionary> sctors) in _ctors) + { + if (!stype.IsAssignableFrom(sourceType)) { - using (var scope = _scopeProvider.CreateCoreScope(autoComplete: true)) - { - map(source!, target!, context); - } - return target; + continue; } - // we cannot really map to an existing enumerable - give up + if (!sctors.TryGetValue(targetType, out ctor)) + { + continue; + } - throw new InvalidOperationException($"Don't know how to map {typeof(TSource).FullName} to {typeof(TTarget).FullName}."); + sourceCtor = sctors; + break; } - private Func? GetCtor(Type? sourceType, Type? targetType) + if (ctor is null || sourceCtor is null) { - if (sourceType is null || targetType is null) - { - return null; - } - if (_ctors.TryGetValue(sourceType, out var sourceCtor) && sourceCtor.TryGetValue(targetType, out var ctor)) - return ctor; - - // we *may* run this more than once but it does not matter + return null; + } - ctor = null; - foreach (var (stype, sctors) in _ctors) + _ctors.AddOrUpdate(sourceType, sourceCtor, (k, v) => + { + // Add missing constructors + foreach (KeyValuePair> c in sourceCtor) { - if (!stype.IsAssignableFrom(sourceType)) continue; - if (!sctors.TryGetValue(targetType, out ctor)) continue; - - sourceCtor = sctors; - break; + if (!v.ContainsKey(c.Key)) + { + v.Add(c.Key, c.Value); + } } - if (ctor is null || sourceCtor is null) return null; + return v; + }); - _ctors.AddOrUpdate(sourceType, sourceCtor, (k, v) => - { - // Add missing constructors - foreach (var c in sourceCtor) - { - if (!v.ContainsKey(c.Key)) - { - v.Add(c.Key, c.Value); - } - } - return v; - }); + return ctor; + } + private Action? GetMap(Type? sourceType, Type? targetType) + { + if (sourceType is null || targetType is null) + { + return null; + } - return ctor; + if (_maps.TryGetValue(sourceType, out Dictionary>? sourceMap) && + sourceMap.TryGetValue(targetType, out Action? map)) + { + return map; } - private Action? GetMap(Type? sourceType, Type? targetType) + // we *may* run this more than once but it does not matter + + map = null; + foreach ((Type stype, Dictionary> smap) in _maps) { - if (sourceType is null || targetType is null) + if (!stype.IsAssignableFrom(sourceType)) { - return null; + continue; } - if (_maps.TryGetValue(sourceType, out var sourceMap) && sourceMap.TryGetValue(targetType, out var map)) - return map; - - // we *may* run this more than once but it does not matter - map = null; - foreach (var (stype, smap) in _maps) + // TODO: consider looking for assignable types for target too? + if (!smap.TryGetValue(targetType, out map)) { - if (!stype.IsAssignableFrom(sourceType)) continue; - - // TODO: consider looking for assignable types for target too? - if (!smap.TryGetValue(targetType, out map)) continue; - - sourceMap = smap; - break; + continue; } - if (map is null || sourceMap is null) return null; + sourceMap = smap; + break; + } - if (_maps.ContainsKey(sourceType)) + if (map is null || sourceMap is null) + { + return null; + } + + if (_maps.ContainsKey(sourceType)) + { + foreach (KeyValuePair> m in sourceMap) { - foreach (var m in sourceMap) + if (!_maps[sourceType].TryGetValue(m.Key, out _)) { - if (!_maps[sourceType].TryGetValue(m.Key, out _)) - _maps[sourceType].Add(m.Key, m.Value); + _maps[sourceType].Add(m.Key, m.Value); } } - else - _maps[sourceType] = sourceMap; - - return map; } - - private static bool IsEnumerableOrArrayOfType(Type type) + else { - if (type.IsArray && type.GetArrayRank() == 1) return true; - if (type.IsGenericType && type.GenericTypeArguments.Length == 1) return true; - return false; + _maps[sourceType] = sourceMap; } - private static Type? GetEnumerableOrArrayTypeArgument(Type type) + return map; + } + + private static bool IsEnumerableOrArrayOfType(Type type) + { + if (type.IsArray && type.GetArrayRank() == 1) { - if (type.IsArray) return type.GetElementType(); - if (type.IsGenericType) return type.GenericTypeArguments[0]; - throw new PanicException($"Could not get enumerable or array type from {type}"); + return true; } - /// - /// Maps an enumerable of source objects to a new list of target objects. - /// - /// The type of the source objects. - /// The type of the target objects. - /// The source objects. - /// A list containing the target objects. - public List MapEnumerable(IEnumerable source) + if (type.IsGenericType && type.GenericTypeArguments.Length == 1) { - return source - .Select(Map) - .Where(x => x is not null) - .Select(x => x!) - .ToList(); + return true; } - /// - /// Maps an enumerable of source objects to a new list of target objects. - /// - /// The type of the source objects. - /// The type of the target objects. - /// The source objects. - /// A mapper context preparation method. - /// A list containing the target objects. - public List MapEnumerable(IEnumerable source, Action f) + return false; + } + + private static Type? GetEnumerableOrArrayTypeArgument(Type type) + { + if (type.IsArray) { - var context = new MapperContext(this); - f(context); - return source - .Select(x => Map(x, context)) - .Where(x => x is not null) - .Select(x => x!) - .ToList(); + return type.GetElementType(); } - /// - /// Maps an enumerable of source objects to a new list of target objects. - /// - /// The type of the source objects. - /// The type of the target objects. - /// The source objects. - /// A mapper context. - /// A list containing the target objects. - public List MapEnumerable(IEnumerable source, MapperContext context) + if (type.IsGenericType) { - return source - .Select(x => Map(x, context)) - .Where(x => x is not null) - .Select(x => x!) - .ToList(); + return type.GenericTypeArguments[0]; } - #endregion + throw new PanicException($"Could not get enumerable or array type from {type}"); + } + + /// + /// Maps an enumerable of source objects to a new list of target objects. + /// + /// The type of the source objects. + /// The type of the target objects. + /// The source objects. + /// A list containing the target objects. + public List MapEnumerable(IEnumerable source) => + source + .Select(Map) + .Where(x => x is not null) + .Select(x => x!) + .ToList(); + + /// + /// Maps an enumerable of source objects to a new list of target objects. + /// + /// The type of the source objects. + /// The type of the target objects. + /// The source objects. + /// A mapper context preparation method. + /// A list containing the target objects. + public List MapEnumerable(IEnumerable source, + Action f) + { + var context = new MapperContext(this); + f(context); + return source + .Select(x => Map(x, context)) + .Where(x => x is not null) + .Select(x => x!) + .ToList(); } + + /// + /// Maps an enumerable of source objects to a new list of target objects. + /// + /// The type of the source objects. + /// The type of the target objects. + /// The source objects. + /// A mapper context. + /// A list containing the target objects. + public List MapEnumerable(IEnumerable source, + MapperContext context) => + source + .Select(x => Map(x, context)) + .Where(x => x is not null) + .Select(x => x!) + .ToList(); + + #endregion } diff --git a/src/Umbraco.Infrastructure/Media/ImageSharpDimensionExtractor.cs b/src/Umbraco.Infrastructure/Media/ImageSharpDimensionExtractor.cs index 7881daa593c0..84aa89948db2 100644 --- a/src/Umbraco.Infrastructure/Media/ImageSharpDimensionExtractor.cs +++ b/src/Umbraco.Infrastructure/Media/ImageSharpDimensionExtractor.cs @@ -1,71 +1,66 @@ -using System; -using System.IO; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using Umbraco.Cms.Core.Media; using Size = System.Drawing.Size; -namespace Umbraco.Cms.Infrastructure.Media -{ - internal class ImageSharpDimensionExtractor : IImageDimensionExtractor - { - private readonly Configuration _configuration; +namespace Umbraco.Cms.Infrastructure.Media; - /// - /// Initializes a new instance of the class. - /// - /// The configuration. - public ImageSharpDimensionExtractor(Configuration configuration) - => _configuration = configuration; +internal class ImageSharpDimensionExtractor : IImageDimensionExtractor +{ + private readonly Configuration _configuration; - /// - /// Gets the dimensions of an image. - /// - /// A stream containing the image bytes. - /// - /// The dimension of the image. - /// - public Size? GetDimensions(Stream? stream) - { - Size? size = null; + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + public ImageSharpDimensionExtractor(Configuration configuration) + => _configuration = configuration; - IImageInfo imageInfo = Image.Identify(_configuration, stream); - if (imageInfo != null) - { - size = IsExifOrientationRotated(imageInfo) - ? new Size(imageInfo.Height, imageInfo.Width) - : new Size(imageInfo.Width, imageInfo.Height); - } + /// + /// Gets the dimensions of an image. + /// + /// A stream containing the image bytes. + /// + /// The dimension of the image. + /// + public Size? GetDimensions(Stream? stream) + { + Size? size = null; - return size; + IImageInfo imageInfo = Image.Identify(_configuration, stream); + if (imageInfo != null) + { + size = IsExifOrientationRotated(imageInfo) + ? new Size(imageInfo.Height, imageInfo.Width) + : new Size(imageInfo.Width, imageInfo.Height); } - private static bool IsExifOrientationRotated(IImageInfo imageInfo) - => GetExifOrientation(imageInfo) switch - { - ExifOrientationMode.LeftTop + return size; + } + + private static bool IsExifOrientationRotated(IImageInfo imageInfo) + => GetExifOrientation(imageInfo) switch + { + ExifOrientationMode.LeftTop or ExifOrientationMode.RightTop or ExifOrientationMode.RightBottom or ExifOrientationMode.LeftBottom => true, - _ => false, - }; + _ => false + }; - private static ushort GetExifOrientation(IImageInfo imageInfo) + private static ushort GetExifOrientation(IImageInfo imageInfo) + { + IExifValue? orientation = imageInfo.Metadata.ExifProfile?.GetValue(ExifTag.Orientation); + if (orientation is not null) { - IExifValue? orientation = imageInfo.Metadata.ExifProfile?.GetValue(ExifTag.Orientation); - if (orientation is not null) + if (orientation.DataType == ExifDataType.Short) { - if (orientation.DataType == ExifDataType.Short) - { - return orientation.Value; - } - else - { - return Convert.ToUInt16(orientation.Value); - } + return orientation.Value; } - return ExifOrientationMode.Unknown; + return Convert.ToUInt16(orientation.Value); } + + return ExifOrientationMode.Unknown; } } diff --git a/src/Umbraco.Infrastructure/Migrations/ExecutedMigrationPlan.cs b/src/Umbraco.Infrastructure/Migrations/ExecutedMigrationPlan.cs index 9979da1e4069..3c86f460ab00 100644 --- a/src/Umbraco.Infrastructure/Migrations/ExecutedMigrationPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/ExecutedMigrationPlan.cs @@ -1,18 +1,15 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Migrations; -namespace Umbraco.Cms.Infrastructure.Migrations +public class ExecutedMigrationPlan { - public class ExecutedMigrationPlan + public ExecutedMigrationPlan(MigrationPlan plan, string initialState, string finalState) { - public ExecutedMigrationPlan(MigrationPlan plan, string initialState, string finalState) - { - Plan = plan; - InitialState = initialState ?? throw new ArgumentNullException(nameof(initialState)); - FinalState = finalState ?? throw new ArgumentNullException(nameof(finalState)); - } - - public MigrationPlan Plan { get; } - public string InitialState { get; } - public string FinalState { get; } + Plan = plan; + InitialState = initialState ?? throw new ArgumentNullException(nameof(initialState)); + FinalState = finalState ?? throw new ArgumentNullException(nameof(finalState)); } + + public MigrationPlan Plan { get; } + public string InitialState { get; } + public string FinalState { get; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/AlterBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/AlterBuilder.cs index fec6b5d0c19d..ae4ac5c968ab 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/AlterBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/AlterBuilder.cs @@ -1,25 +1,21 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Expressions; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter; + +/// +/// Implements . +/// +public class AlterBuilder : IAlterBuilder { - /// - /// Implements . - /// - public class AlterBuilder : IAlterBuilder - { - private readonly IMigrationContext _context; + private readonly IMigrationContext _context; - public AlterBuilder(IMigrationContext context) - { - _context = context; - } + public AlterBuilder(IMigrationContext context) => _context = context; - /// - public IAlterTableBuilder Table(string tableName) - { - var expression = new AlterTableExpression(_context) { TableName = tableName }; - return new AlterTableBuilder(_context, expression); - } + /// + public IAlterTableBuilder Table(string tableName) + { + var expression = new AlterTableExpression(_context) {TableName = tableName}; + return new AlterTableBuilder(_context, expression); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Expressions/AlterColumnExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Expressions/AlterColumnExpression.cs index 6d1bfe4561c1..c3b922b292a8 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Expressions/AlterColumnExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Expressions/AlterColumnExpression.cs @@ -1,25 +1,19 @@ using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Expressions -{ - public class AlterColumnExpression : MigrationExpressionBase - { +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Expressions; - public AlterColumnExpression(IMigrationContext context) - : base(context) - { - Column = new ColumnDefinition { ModificationType = ModificationType.Alter }; - } +public class AlterColumnExpression : MigrationExpressionBase +{ + public AlterColumnExpression(IMigrationContext context) + : base(context) => + Column = new ColumnDefinition {ModificationType = ModificationType.Alter}; - public virtual string? SchemaName { get; set; } - public virtual string? TableName { get; set; } - public virtual ColumnDefinition Column { get; set; } + public virtual string? SchemaName { get; set; } + public virtual string? TableName { get; set; } + public virtual ColumnDefinition Column { get; set; } - protected override string GetSql() - { - return string.Format(SqlSyntax.AlterColumn, - SqlSyntax.GetQuotedTableName(TableName), - SqlSyntax.Format(Column)); - } - } + protected override string GetSql() => + string.Format(SqlSyntax.AlterColumn, + SqlSyntax.GetQuotedTableName(TableName), + SqlSyntax.Format(Column)); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Expressions/AlterDefaultConstraintExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Expressions/AlterDefaultConstraintExpression.cs index 18298c73786d..f63fece4b933 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Expressions/AlterDefaultConstraintExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Expressions/AlterDefaultConstraintExpression.cs @@ -1,26 +1,23 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Expressions; + +public class AlterDefaultConstraintExpression : MigrationExpressionBase { - public class AlterDefaultConstraintExpression : MigrationExpressionBase + public AlterDefaultConstraintExpression(IMigrationContext context) + : base(context) { - public AlterDefaultConstraintExpression(IMigrationContext context) - : base(context) - { } - - public virtual string? TableName { get; set; } + } - public virtual string? ColumnName { get; set; } + public virtual string? TableName { get; set; } - public virtual string? ConstraintName { get; set; } + public virtual string? ColumnName { get; set; } - public virtual object? DefaultValue { get; set; } + public virtual string? ConstraintName { get; set; } - protected override string GetSql() - { - //NOTE Should probably investigate if Deleting a Default Constraint is different from deleting a 'regular' constraint + public virtual object? DefaultValue { get; set; } - return string.Format(SqlSyntax.DeleteConstraint, - SqlSyntax.GetQuotedTableName(TableName), - SqlSyntax.GetQuotedName(ConstraintName)); - } - } + protected override string GetSql() => + //NOTE Should probably investigate if Deleting a Default Constraint is different from deleting a 'regular' constraint + string.Format(SqlSyntax.DeleteConstraint, + SqlSyntax.GetQuotedTableName(TableName), + SqlSyntax.GetQuotedName(ConstraintName)); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Expressions/AlterTableExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Expressions/AlterTableExpression.cs index 9be5354590d7..b28ebebbbf40 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Expressions/AlterTableExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Expressions/AlterTableExpression.cs @@ -1,16 +1,13 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Expressions; + +public class AlterTableExpression : MigrationExpressionBase { - public class AlterTableExpression : MigrationExpressionBase + public AlterTableExpression(IMigrationContext context) + : base(context) { - public AlterTableExpression(IMigrationContext context) - : base(context) - { } + } - public virtual string? TableName { get; set; } + public virtual string? TableName { get; set; } - protected override string GetSql() - { - return string.Empty; - } - } + protected override string GetSql() => string.Empty; } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/IAlterBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/IAlterBuilder.cs index 7f3bf080d4e0..f96237e39631 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/IAlterBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/IAlterBuilder.cs @@ -1,15 +1,14 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter; + +/// +/// Builds an Alter expression. +/// +public interface IAlterBuilder : IFluentBuilder { /// - /// Builds an Alter expression. + /// Specifies the table to alter. /// - public interface IAlterBuilder : IFluentBuilder - { - /// - /// Specifies the table to alter. - /// - IAlterTableBuilder Table(string tableName); - } + IAlterTableBuilder Table(string tableName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/AlterTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/AlterTableBuilder.cs index 199db3410244..e43e85344109 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/AlterTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/AlterTableBuilder.cs @@ -5,177 +5,147 @@ using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table -{ - public class AlterTableBuilder : ExpressionBuilderBase, - IAlterTableColumnTypeBuilder, - IAlterTableColumnOptionForeignKeyCascadeBuilder - { - private readonly IMigrationContext _context; +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table; - public AlterTableBuilder(IMigrationContext context, AlterTableExpression expression) - : base(expression) - { - _context = context; - } +public class AlterTableBuilder : ExpressionBuilderBase, + IAlterTableColumnTypeBuilder, + IAlterTableColumnOptionForeignKeyCascadeBuilder +{ + private readonly IMigrationContext _context; - public void Do() => Expression.Execute(); + public AlterTableBuilder(IMigrationContext context, AlterTableExpression expression) + : base(expression) => + _context = context; - public ColumnDefinition CurrentColumn { get; set; } = null!; + public ColumnDefinition CurrentColumn { get; set; } = null!; - public ForeignKeyDefinition CurrentForeignKey { get; set; } = null!; + public ForeignKeyDefinition CurrentForeignKey { get; set; } = null!; - public override ColumnDefinition GetColumnForType() - { - return CurrentColumn; - } + public void Do() => Expression.Execute(); - public IAlterTableColumnOptionBuilder WithDefault(SystemMethods method) - { - CurrentColumn.DefaultValue = method; - return this; - } + public IAlterTableColumnOptionBuilder WithDefault(SystemMethods method) + { + CurrentColumn.DefaultValue = method; + return this; + } - public IAlterTableColumnOptionBuilder WithDefaultValue(object value) + public IAlterTableColumnOptionBuilder WithDefaultValue(object value) + { + if (CurrentColumn.ModificationType == ModificationType.Alter) { - if (CurrentColumn.ModificationType == ModificationType.Alter) + var dc = new AlterDefaultConstraintExpression(_context) { - var dc = new AlterDefaultConstraintExpression(_context) - { - TableName = Expression.TableName, - ColumnName = CurrentColumn.Name, - DefaultValue = value - }; - - Expression.Expressions.Add(dc); - } + TableName = Expression.TableName, ColumnName = CurrentColumn.Name, DefaultValue = value + }; - CurrentColumn.DefaultValue = value; - return this; + Expression.Expressions.Add(dc); } - public IAlterTableColumnOptionBuilder Identity() - { - CurrentColumn.IsIdentity = true; - return this; - } + CurrentColumn.DefaultValue = value; + return this; + } - public IAlterTableColumnOptionBuilder Indexed() - { - return Indexed(null); - } + public IAlterTableColumnOptionBuilder Identity() + { + CurrentColumn.IsIdentity = true; + return this; + } - public IAlterTableColumnOptionBuilder Indexed(string? indexName) - { - CurrentColumn.IsIndexed = true; + public IAlterTableColumnOptionBuilder Indexed() => Indexed(null); - var index = new CreateIndexExpression(_context, new IndexDefinition - { - Name = indexName, - TableName = Expression.TableName - }); + public IAlterTableColumnOptionBuilder Indexed(string? indexName) + { + CurrentColumn.IsIndexed = true; - index.Index.Columns.Add(new IndexColumnDefinition - { - Name = CurrentColumn.Name - }); + var index = new CreateIndexExpression(_context, + new IndexDefinition {Name = indexName, TableName = Expression.TableName}); - Expression.Expressions.Add(index); + index.Index.Columns.Add(new IndexColumnDefinition {Name = CurrentColumn.Name}); - return this; - } + Expression.Expressions.Add(index); + + return this; + } - public IAlterTableColumnOptionBuilder PrimaryKey() + public IAlterTableColumnOptionBuilder PrimaryKey() + { + CurrentColumn.IsPrimaryKey = true; + + var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey) { - CurrentColumn.IsPrimaryKey = true; + Constraint = {TableName = Expression.TableName, Columns = new[] {CurrentColumn.Name}} + }; + Expression.Expressions.Add(expression); - var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey) - { - Constraint = - { - TableName = Expression.TableName, - Columns = new[] { CurrentColumn.Name } - } - }; - Expression.Expressions.Add(expression); + return this; + } - return this; - } + public IAlterTableColumnOptionBuilder PrimaryKey(string primaryKeyName) + { + CurrentColumn.IsPrimaryKey = true; + CurrentColumn.PrimaryKeyName = primaryKeyName; - public IAlterTableColumnOptionBuilder PrimaryKey(string primaryKeyName) + var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey) { - CurrentColumn.IsPrimaryKey = true; - CurrentColumn.PrimaryKeyName = primaryKeyName; - - var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey) + Constraint = { - Constraint = - { - ConstraintName = primaryKeyName, - TableName = Expression.TableName, - Columns = new[] { CurrentColumn.Name } - } - }; - Expression.Expressions.Add(expression); + ConstraintName = primaryKeyName, + TableName = Expression.TableName, + Columns = new[] {CurrentColumn.Name} + } + }; + Expression.Expressions.Add(expression); - return this; - } + return this; + } - public IAlterTableColumnOptionBuilder Nullable() - { - CurrentColumn.IsNullable = true; - return this; - } + public IAlterTableColumnOptionBuilder Nullable() + { + CurrentColumn.IsNullable = true; + return this; + } - public IAlterTableColumnOptionBuilder NotNullable() - { - CurrentColumn.IsNullable = false; - return this; - } + public IAlterTableColumnOptionBuilder NotNullable() + { + CurrentColumn.IsNullable = false; + return this; + } - public IAlterTableColumnOptionBuilder Unique() - { - return Unique(null); - } + public IAlterTableColumnOptionBuilder Unique() => Unique(null); - public IAlterTableColumnOptionBuilder Unique(string? indexName) - { - CurrentColumn.IsUnique = true; + public IAlterTableColumnOptionBuilder Unique(string? indexName) + { + CurrentColumn.IsUnique = true; - var index = new CreateIndexExpression(_context, new IndexDefinition + var index = new CreateIndexExpression(_context, + new IndexDefinition { - Name = indexName, - TableName = Expression.TableName, - IndexType = IndexTypes.UniqueNonClustered + Name = indexName, TableName = Expression.TableName, IndexType = IndexTypes.UniqueNonClustered }); - index.Index.Columns.Add(new IndexColumnDefinition - { - Name = CurrentColumn.Name - }); + index.Index.Columns.Add(new IndexColumnDefinition {Name = CurrentColumn.Name}); - Expression.Expressions.Add(index); + Expression.Expressions.Add(index); - return this; - } + return this; + } - public IAlterTableColumnOptionForeignKeyCascadeBuilder ForeignKey(string primaryTableName, string primaryColumnName) - { - return ForeignKey(null, null, primaryTableName, primaryColumnName); - } + public IAlterTableColumnOptionForeignKeyCascadeBuilder + ForeignKey(string primaryTableName, string primaryColumnName) => + ForeignKey(null, null, primaryTableName, primaryColumnName); - public IAlterTableColumnOptionForeignKeyCascadeBuilder ForeignKey(string foreignKeyName, string primaryTableName, - string primaryColumnName) - { - return ForeignKey(foreignKeyName, null, primaryTableName, primaryColumnName); - } + public IAlterTableColumnOptionForeignKeyCascadeBuilder ForeignKey(string foreignKeyName, string primaryTableName, + string primaryColumnName) => + ForeignKey(foreignKeyName, null, primaryTableName, primaryColumnName); - public IAlterTableColumnOptionForeignKeyCascadeBuilder ForeignKey(string? foreignKeyName, string? primaryTableSchema, - string primaryTableName, string primaryColumnName) - { - CurrentColumn.IsForeignKey = true; + public IAlterTableColumnOptionForeignKeyCascadeBuilder ForeignKey(string? foreignKeyName, + string? primaryTableSchema, + string primaryTableName, string primaryColumnName) + { + CurrentColumn.IsForeignKey = true; - var fk = new CreateForeignKeyExpression(_context, new ForeignKeyDefinition + var fk = new CreateForeignKeyExpression(_context, + new ForeignKeyDefinition { Name = foreignKeyName, PrimaryTable = primaryTableName, @@ -183,35 +153,33 @@ public IAlterTableColumnOptionForeignKeyCascadeBuilder ForeignKey(string? foreig ForeignTable = Expression.TableName }); - fk.ForeignKey.PrimaryColumns.Add(primaryColumnName); - fk.ForeignKey.ForeignColumns.Add(CurrentColumn.Name); + fk.ForeignKey.PrimaryColumns.Add(primaryColumnName); + fk.ForeignKey.ForeignColumns.Add(CurrentColumn.Name); - Expression.Expressions.Add(fk); - CurrentForeignKey = fk.ForeignKey; - return this; - } + Expression.Expressions.Add(fk); + CurrentForeignKey = fk.ForeignKey; + return this; + } - public IAlterTableColumnOptionForeignKeyCascadeBuilder ForeignKey() - { - CurrentColumn.IsForeignKey = true; - return this; - } + public IAlterTableColumnOptionForeignKeyCascadeBuilder ForeignKey() + { + CurrentColumn.IsForeignKey = true; + return this; + } - public IAlterTableColumnOptionForeignKeyCascadeBuilder ReferencedBy(string foreignTableName, string foreignColumnName) - { - return ReferencedBy(null, null, foreignTableName, foreignColumnName); - } + public IAlterTableColumnOptionForeignKeyCascadeBuilder ReferencedBy(string foreignTableName, + string foreignColumnName) => ReferencedBy(null, null, foreignTableName, foreignColumnName); - public IAlterTableColumnOptionForeignKeyCascadeBuilder ReferencedBy(string foreignKeyName, string foreignTableName, - string foreignColumnName) - { - return ReferencedBy(foreignKeyName, null, foreignTableName, foreignColumnName); - } + public IAlterTableColumnOptionForeignKeyCascadeBuilder ReferencedBy(string foreignKeyName, string foreignTableName, + string foreignColumnName) => + ReferencedBy(foreignKeyName, null, foreignTableName, foreignColumnName); - public IAlterTableColumnOptionForeignKeyCascadeBuilder ReferencedBy(string? foreignKeyName, string? foreignTableSchema, - string foreignTableName, string foreignColumnName) - { - var fk = new CreateForeignKeyExpression(_context, new ForeignKeyDefinition + public IAlterTableColumnOptionForeignKeyCascadeBuilder ReferencedBy(string? foreignKeyName, + string? foreignTableSchema, + string foreignTableName, string foreignColumnName) + { + var fk = new CreateForeignKeyExpression(_context, + new ForeignKeyDefinition { Name = foreignKeyName, PrimaryTable = Expression.TableName, @@ -219,61 +187,54 @@ public IAlterTableColumnOptionForeignKeyCascadeBuilder ReferencedBy(string? fore ForeignTableSchema = foreignTableSchema }); - fk.ForeignKey.PrimaryColumns.Add(CurrentColumn.Name); - fk.ForeignKey.ForeignColumns.Add(foreignColumnName); + fk.ForeignKey.PrimaryColumns.Add(CurrentColumn.Name); + fk.ForeignKey.ForeignColumns.Add(foreignColumnName); - Expression.Expressions.Add(fk); - CurrentForeignKey = fk.ForeignKey; - return this; - } + Expression.Expressions.Add(fk); + CurrentForeignKey = fk.ForeignKey; + return this; + } - public IAlterTableColumnTypeBuilder AddColumn(string name) - { - var column = new ColumnDefinition { Name = name, ModificationType = ModificationType.Create }; - var createColumn = new CreateColumnExpression(_context) - { - Column = column, - TableName = Expression.TableName - }; + public IAlterTableColumnTypeBuilder AddColumn(string name) + { + var column = new ColumnDefinition {Name = name, ModificationType = ModificationType.Create}; + var createColumn = new CreateColumnExpression(_context) {Column = column, TableName = Expression.TableName}; - CurrentColumn = column; + CurrentColumn = column; - Expression.Expressions.Add(createColumn); - return this; - } + Expression.Expressions.Add(createColumn); + return this; + } - public IAlterTableColumnTypeBuilder AlterColumn(string name) - { - var column = new ColumnDefinition { Name = name, ModificationType = ModificationType.Alter }; - var alterColumn = new AlterColumnExpression(_context) - { - Column = column, - TableName = Expression.TableName - }; + public IAlterTableColumnTypeBuilder AlterColumn(string name) + { + var column = new ColumnDefinition {Name = name, ModificationType = ModificationType.Alter}; + var alterColumn = new AlterColumnExpression(_context) {Column = column, TableName = Expression.TableName}; - CurrentColumn = column; + CurrentColumn = column; - Expression.Expressions.Add(alterColumn); - return this; - } + Expression.Expressions.Add(alterColumn); + return this; + } - public IAlterTableColumnOptionForeignKeyCascadeBuilder OnDelete(Rule rule) - { - CurrentForeignKey.OnDelete = rule; - return this; - } + public IAlterTableColumnOptionForeignKeyCascadeBuilder OnDelete(Rule rule) + { + CurrentForeignKey.OnDelete = rule; + return this; + } - public IAlterTableColumnOptionForeignKeyCascadeBuilder OnUpdate(Rule rule) - { - CurrentForeignKey.OnUpdate = rule; - return this; - } + public IAlterTableColumnOptionForeignKeyCascadeBuilder OnUpdate(Rule rule) + { + CurrentForeignKey.OnUpdate = rule; + return this; + } - public IAlterTableColumnOptionBuilder OnDeleteOrUpdate(Rule rule) - { - OnDelete(rule); - OnUpdate(rule); - return this; - } + public IAlterTableColumnOptionBuilder OnDeleteOrUpdate(Rule rule) + { + OnDelete(rule); + OnUpdate(rule); + return this; } + + public override ColumnDefinition GetColumnForType() => CurrentColumn; } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableBuilder.cs index 642e71757aa7..67ded6708165 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableBuilder.cs @@ -1,18 +1,17 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table; + +/// +/// Builds an Alter Table expression. +/// +public interface IAlterTableBuilder : IFluentBuilder { /// - /// Builds an Alter Table expression. + /// Specifies a column to add. /// - public interface IAlterTableBuilder : IFluentBuilder - { - /// - /// Specifies a column to add. - /// - IAlterTableColumnTypeBuilder AddColumn(string name); + IAlterTableColumnTypeBuilder AddColumn(string name); - /// - /// Specifies a column to alter. - /// - IAlterTableColumnTypeBuilder AlterColumn(string name); - } + /// + /// Specifies a column to alter. + /// + IAlterTableColumnTypeBuilder AlterColumn(string name); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnOptionBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnOptionBuilder.cs index 3ace421b7b2b..7bc2794f6360 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnOptionBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnOptionBuilder.cs @@ -1,8 +1,9 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table; + +public interface IAlterTableColumnOptionBuilder : IColumnOptionBuilder, + IAlterTableBuilder, IExecutableBuilder { - public interface IAlterTableColumnOptionBuilder : IColumnOptionBuilder, - IAlterTableBuilder, IExecutableBuilder - { } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnOptionForeignKeyCascadeBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnOptionForeignKeyCascadeBuilder.cs index e42fcb266da1..e97ac1d19deb 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnOptionForeignKeyCascadeBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnOptionForeignKeyCascadeBuilder.cs @@ -1,9 +1,9 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table; + +public interface IAlterTableColumnOptionForeignKeyCascadeBuilder : + IAlterTableColumnOptionBuilder, + IForeignKeyCascadeBuilder { - public interface IAlterTableColumnOptionForeignKeyCascadeBuilder : - IAlterTableColumnOptionBuilder, - IForeignKeyCascadeBuilder - { } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnTypeBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnTypeBuilder.cs index 4768b52e7fad..7c9be5e39e38 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnTypeBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnTypeBuilder.cs @@ -1,7 +1,7 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table; + +public interface IAlterTableColumnTypeBuilder : IColumnTypeBuilder { - public interface IAlterTableColumnTypeBuilder : IColumnTypeBuilder - { } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/ExecutableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/ExecutableBuilder.cs index 5ec8c200e0f3..073237fb78ab 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/ExecutableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/ExecutableBuilder.cs @@ -1,15 +1,11 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; + +public class ExecutableBuilder : IExecutableBuilder { - public class ExecutableBuilder : IExecutableBuilder - { - private readonly IMigrationExpression _expression; + private readonly IMigrationExpression _expression; - public ExecutableBuilder(IMigrationExpression expression) - { - _expression = expression; - } + public ExecutableBuilder(IMigrationExpression expression) => _expression = expression; - /// - public void Do() => _expression.Execute(); - } + /// + public void Do() => _expression.Execute(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/Expressions/CreateColumnExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/Expressions/CreateColumnExpression.cs index 8e701f845ece..8bc2648befd4 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/Expressions/CreateColumnExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/Expressions/CreateColumnExpression.cs @@ -1,26 +1,25 @@ using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common.Expressions; + +public class CreateColumnExpression : MigrationExpressionBase { - public class CreateColumnExpression : MigrationExpressionBase - { - public CreateColumnExpression(IMigrationContext context) - : base(context) - { - Column = new ColumnDefinition { ModificationType = ModificationType.Create }; - } + public CreateColumnExpression(IMigrationContext context) + : base(context) => + Column = new ColumnDefinition {ModificationType = ModificationType.Create}; - public string? TableName { get; set; } - public ColumnDefinition Column { get; set; } + public string? TableName { get; set; } + public ColumnDefinition Column { get; set; } - protected override string GetSql() + protected override string GetSql() + { + if (string.IsNullOrEmpty(Column.TableName)) { - if (string.IsNullOrEmpty(Column.TableName)) - Column.TableName = TableName; - - return string.Format(SqlSyntax.AddColumn, - SqlSyntax.GetQuotedTableName(Column.TableName), - SqlSyntax.Format(Column)); + Column.TableName = TableName; } + + return string.Format(SqlSyntax.AddColumn, + SqlSyntax.GetQuotedTableName(Column.TableName), + SqlSyntax.Format(Column)); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/Expressions/CreateForeignKeyExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/Expressions/CreateForeignKeyExpression.cs index 511f4ac634ad..9d1b9b1a7652 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/Expressions/CreateForeignKeyExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/Expressions/CreateForeignKeyExpression.cs @@ -1,26 +1,18 @@ using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common.Expressions; + +public class CreateForeignKeyExpression : MigrationExpressionBase { - public class CreateForeignKeyExpression : MigrationExpressionBase - { - public CreateForeignKeyExpression(IMigrationContext context, ForeignKeyDefinition fkDef) - : base(context) - { - ForeignKey = fkDef; - } + public CreateForeignKeyExpression(IMigrationContext context, ForeignKeyDefinition fkDef) + : base(context) => + ForeignKey = fkDef; - public CreateForeignKeyExpression(IMigrationContext context) - : base(context) - { - ForeignKey = new ForeignKeyDefinition(); - } + public CreateForeignKeyExpression(IMigrationContext context) + : base(context) => + ForeignKey = new ForeignKeyDefinition(); - public ForeignKeyDefinition ForeignKey { get; set; } + public ForeignKeyDefinition ForeignKey { get; set; } - protected override string GetSql() - { - return SqlSyntax.Format(ForeignKey); - } - } + protected override string GetSql() => SqlSyntax.Format(ForeignKey); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/Expressions/CreateIndexExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/Expressions/CreateIndexExpression.cs index cef5a8387a62..75120390e62e 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/Expressions/CreateIndexExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/Expressions/CreateIndexExpression.cs @@ -1,27 +1,18 @@ using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common.Expressions -{ - public class CreateIndexExpression : MigrationExpressionBase - { +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common.Expressions; - public CreateIndexExpression(IMigrationContext context, IndexDefinition index) - : base(context) - { - Index = index; - } +public class CreateIndexExpression : MigrationExpressionBase +{ + public CreateIndexExpression(IMigrationContext context, IndexDefinition index) + : base(context) => + Index = index; - public CreateIndexExpression(IMigrationContext context) - : base(context) - { - Index = new IndexDefinition(); - } + public CreateIndexExpression(IMigrationContext context) + : base(context) => + Index = new IndexDefinition(); - public IndexDefinition Index { get; set; } + public IndexDefinition Index { get; set; } - protected override string GetSql() - { - return SqlSyntax.Format(Index); - } - } + protected override string GetSql() => SqlSyntax.Format(Index); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IColumnOptionBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IColumnOptionBuilder.cs index 10057c0f6f89..9cdb27bf1e83 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IColumnOptionBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IColumnOptionBuilder.cs @@ -1,31 +1,35 @@ using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; + +public interface IColumnOptionBuilder : IFluentBuilder + where TNext : IFluentBuilder + where TNextFk : IFluentBuilder { - public interface IColumnOptionBuilder : IFluentBuilder - where TNext : IFluentBuilder - where TNextFk : IFluentBuilder - { - TNext WithDefault(SystemMethods method); - TNext WithDefaultValue(object value); - TNext Identity(); - TNext Indexed(); - TNext Indexed(string indexName); - - TNext PrimaryKey(); - TNext PrimaryKey(string primaryKeyName); - TNext Nullable(); - TNext NotNullable(); - TNext Unique(); - TNext Unique(string indexName); - - TNextFk ForeignKey(string primaryTableName, string primaryColumnName); - TNextFk ForeignKey(string foreignKeyName, string primaryTableName, string primaryColumnName); - TNextFk ForeignKey(string foreignKeyName, string primaryTableSchema, string primaryTableName, string primaryColumnName); - TNextFk ForeignKey(); - - TNextFk ReferencedBy(string foreignTableName, string foreignColumnName); - TNextFk ReferencedBy(string foreignKeyName, string foreignTableName, string foreignColumnName); - TNextFk ReferencedBy(string foreignKeyName, string foreignTableSchema, string foreignTableName, string foreignColumnName); - } + TNext WithDefault(SystemMethods method); + TNext WithDefaultValue(object value); + TNext Identity(); + TNext Indexed(); + TNext Indexed(string indexName); + + TNext PrimaryKey(); + TNext PrimaryKey(string primaryKeyName); + TNext Nullable(); + TNext NotNullable(); + TNext Unique(); + TNext Unique(string indexName); + + TNextFk ForeignKey(string primaryTableName, string primaryColumnName); + TNextFk ForeignKey(string foreignKeyName, string primaryTableName, string primaryColumnName); + + TNextFk ForeignKey(string foreignKeyName, string primaryTableSchema, string primaryTableName, + string primaryColumnName); + + TNextFk ForeignKey(); + + TNextFk ReferencedBy(string foreignTableName, string foreignColumnName); + TNextFk ReferencedBy(string foreignKeyName, string foreignTableName, string foreignColumnName); + + TNextFk ReferencedBy(string foreignKeyName, string foreignTableSchema, string foreignTableName, + string foreignColumnName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IColumnTypeBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IColumnTypeBuilder.cs index 75d5512ceee4..2de16cabdde3 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IColumnTypeBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IColumnTypeBuilder.cs @@ -1,35 +1,34 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; + +/// +/// Builds a column type expression. +/// +public interface IColumnTypeBuilder : IFluentBuilder + where TNext : IFluentBuilder { - /// - /// Builds a column type expression. - /// - public interface IColumnTypeBuilder : IFluentBuilder - where TNext : IFluentBuilder - { - TNext AsAnsiString(); - TNext AsAnsiString(int size); - TNext AsBinary(); - TNext AsBinary(int size); - TNext AsBoolean(); - TNext AsByte(); - TNext AsCurrency(); - TNext AsDate(); - TNext AsDateTime(); - TNext AsDecimal(); - TNext AsDecimal(int size, int precision); - TNext AsDouble(); - TNext AsGuid(); - TNext AsFixedLengthString(int size); - TNext AsFixedLengthAnsiString(int size); - TNext AsFloat(); - TNext AsInt16(); - TNext AsInt32(); - TNext AsInt64(); - TNext AsString(); - TNext AsString(int size); - TNext AsTime(); - TNext AsXml(); - TNext AsXml(int size); - TNext AsCustom(string customType); - } + TNext AsAnsiString(); + TNext AsAnsiString(int size); + TNext AsBinary(); + TNext AsBinary(int size); + TNext AsBoolean(); + TNext AsByte(); + TNext AsCurrency(); + TNext AsDate(); + TNext AsDateTime(); + TNext AsDecimal(); + TNext AsDecimal(int size, int precision); + TNext AsDouble(); + TNext AsGuid(); + TNext AsFixedLengthString(int size); + TNext AsFixedLengthAnsiString(int size); + TNext AsFloat(); + TNext AsInt16(); + TNext AsInt32(); + TNext AsInt64(); + TNext AsString(); + TNext AsString(int size); + TNext AsTime(); + TNext AsXml(); + TNext AsXml(int size); + TNext AsCustom(string customType); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IExecutableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IExecutableBuilder.cs index b5a29d801b45..e7595706ecc8 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IExecutableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IExecutableBuilder.cs @@ -1,10 +1,9 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; + +public interface IExecutableBuilder { - public interface IExecutableBuilder - { - /// - /// Executes. - /// - void Do(); - } + /// + /// Executes. + /// + void Do(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IForeignKeyCascadeBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IForeignKeyCascadeBuilder.cs index f566e5c4bb5b..f04cb22c82cf 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IForeignKeyCascadeBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IForeignKeyCascadeBuilder.cs @@ -1,24 +1,23 @@ using System.Data; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; + +public interface IForeignKeyCascadeBuilder : IFluentBuilder + where TNext : IFluentBuilder + where TNextFk : IFluentBuilder { - public interface IForeignKeyCascadeBuilder : IFluentBuilder - where TNext : IFluentBuilder - where TNextFk : IFluentBuilder - { - /// - /// Specifies a rule on deletes. - /// - TNextFk OnDelete(Rule rule); + /// + /// Specifies a rule on deletes. + /// + TNextFk OnDelete(Rule rule); - /// - /// Specifies a rule on updates. - /// - TNextFk OnUpdate(Rule rule); + /// + /// Specifies a rule on updates. + /// + TNextFk OnUpdate(Rule rule); - /// - /// Specifies a rule on deletes and updates. - /// - TNext OnDeleteOrUpdate(Rule rule); - } + /// + /// Specifies a rule on deletes and updates. + /// + TNext OnDeleteOrUpdate(Rule rule); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/CreateColumnBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/CreateColumnBuilder.cs index 07c11c57cdad..56be473fce88 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/CreateColumnBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/CreateColumnBuilder.cs @@ -3,146 +3,117 @@ using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column; + +public class CreateColumnBuilder : ExpressionBuilderBase, + ICreateColumnOnTableBuilder, + ICreateColumnTypeBuilder, + ICreateColumnOptionForeignKeyCascadeBuilder { - public class CreateColumnBuilder : ExpressionBuilderBase, - ICreateColumnOnTableBuilder, - ICreateColumnTypeBuilder, - ICreateColumnOptionForeignKeyCascadeBuilder - { - private readonly IMigrationContext _context; + private readonly IMigrationContext _context; - public CreateColumnBuilder(IMigrationContext context, CreateColumnExpression expression) - : base(expression) - { - _context = context; - } + public CreateColumnBuilder(IMigrationContext context, CreateColumnExpression expression) + : base(expression) => + _context = context; - public void Do() => Expression.Execute(); + public ForeignKeyDefinition? CurrentForeignKey { get; set; } - public ForeignKeyDefinition? CurrentForeignKey { get; set; } + public ICreateColumnTypeBuilder OnTable(string name) + { + Expression.TableName = name; + return this; + } - public override ColumnDefinition GetColumnForType() - { - return Expression.Column; - } + public void Do() => Expression.Execute(); - public ICreateColumnTypeBuilder OnTable(string name) - { - Expression.TableName = name; - return this; - } + public ICreateColumnOptionBuilder WithDefault(SystemMethods method) + { + Expression.Column.DefaultValue = method; + return this; + } - public ICreateColumnOptionBuilder WithDefault(SystemMethods method) - { - Expression.Column.DefaultValue = method; - return this; - } + public ICreateColumnOptionBuilder WithDefaultValue(object value) + { + Expression.Column.DefaultValue = value; + return this; + } - public ICreateColumnOptionBuilder WithDefaultValue(object value) - { - Expression.Column.DefaultValue = value; - return this; - } + public ICreateColumnOptionBuilder Identity() => Indexed(null); - public ICreateColumnOptionBuilder Identity() - { - return Indexed(null); - } + public ICreateColumnOptionBuilder Indexed() => Indexed(null); - public ICreateColumnOptionBuilder Indexed() - { - return Indexed(null); - } - - public ICreateColumnOptionBuilder Indexed(string? indexName) - { - Expression.Column.IsIndexed = true; + public ICreateColumnOptionBuilder Indexed(string? indexName) + { + Expression.Column.IsIndexed = true; - var index = new CreateIndexExpression(_context, new IndexDefinition - { - Name = indexName, - TableName = Expression.TableName - }); + var index = new CreateIndexExpression(_context, + new IndexDefinition {Name = indexName, TableName = Expression.TableName}); - index.Index.Columns.Add(new IndexColumnDefinition - { - Name = Expression.Column.Name - }); + index.Index.Columns.Add(new IndexColumnDefinition {Name = Expression.Column.Name}); - Expression.Expressions.Add(index); + Expression.Expressions.Add(index); - return this; - } + return this; + } - public ICreateColumnOptionBuilder PrimaryKey() - { - Expression.Column.IsPrimaryKey = true; - return this; - } + public ICreateColumnOptionBuilder PrimaryKey() + { + Expression.Column.IsPrimaryKey = true; + return this; + } - public ICreateColumnOptionBuilder PrimaryKey(string primaryKeyName) - { - Expression.Column.IsPrimaryKey = true; - Expression.Column.PrimaryKeyName = primaryKeyName; - return this; - } + public ICreateColumnOptionBuilder PrimaryKey(string primaryKeyName) + { + Expression.Column.IsPrimaryKey = true; + Expression.Column.PrimaryKeyName = primaryKeyName; + return this; + } - public ICreateColumnOptionBuilder Nullable() - { - Expression.Column.IsNullable = true; - return this; - } + public ICreateColumnOptionBuilder Nullable() + { + Expression.Column.IsNullable = true; + return this; + } - public ICreateColumnOptionBuilder NotNullable() - { - Expression.Column.IsNullable = false; - return this; - } + public ICreateColumnOptionBuilder NotNullable() + { + Expression.Column.IsNullable = false; + return this; + } - public ICreateColumnOptionBuilder Unique() - { - return Unique(null); - } + public ICreateColumnOptionBuilder Unique() => Unique(null); - public ICreateColumnOptionBuilder Unique(string? indexName) - { - Expression.Column.IsUnique = true; + public ICreateColumnOptionBuilder Unique(string? indexName) + { + Expression.Column.IsUnique = true; - var index = new CreateIndexExpression(_context, new IndexDefinition + var index = new CreateIndexExpression(_context, + new IndexDefinition { - Name = indexName, - TableName = Expression.TableName, - IndexType = IndexTypes.UniqueNonClustered + Name = indexName, TableName = Expression.TableName, IndexType = IndexTypes.UniqueNonClustered }); - index.Index.Columns.Add(new IndexColumnDefinition - { - Name = Expression.Column.Name - }); + index.Index.Columns.Add(new IndexColumnDefinition {Name = Expression.Column.Name}); - Expression.Expressions.Add(index); + Expression.Expressions.Add(index); - return this; - } + return this; + } - public ICreateColumnOptionForeignKeyCascadeBuilder ForeignKey(string primaryTableName, string primaryColumnName) - { - return ForeignKey(null, null, primaryTableName, primaryColumnName); - } + public ICreateColumnOptionForeignKeyCascadeBuilder ForeignKey(string primaryTableName, string primaryColumnName) => + ForeignKey(null, null, primaryTableName, primaryColumnName); - public ICreateColumnOptionForeignKeyCascadeBuilder ForeignKey(string foreignKeyName, string primaryTableName, - string primaryColumnName) - { - return ForeignKey(foreignKeyName, null, primaryTableName, primaryColumnName); - } + public ICreateColumnOptionForeignKeyCascadeBuilder ForeignKey(string foreignKeyName, string primaryTableName, + string primaryColumnName) => + ForeignKey(foreignKeyName, null, primaryTableName, primaryColumnName); - public ICreateColumnOptionForeignKeyCascadeBuilder ForeignKey(string? foreignKeyName, string? primaryTableSchema, - string primaryTableName, string primaryColumnName) - { - Expression.Column.IsForeignKey = true; + public ICreateColumnOptionForeignKeyCascadeBuilder ForeignKey(string? foreignKeyName, string? primaryTableSchema, + string primaryTableName, string primaryColumnName) + { + Expression.Column.IsForeignKey = true; - var fk = new CreateForeignKeyExpression(_context, new ForeignKeyDefinition + var fk = new CreateForeignKeyExpression(_context, + new ForeignKeyDefinition { Name = foreignKeyName, PrimaryTable = primaryTableName, @@ -150,35 +121,33 @@ public ICreateColumnOptionForeignKeyCascadeBuilder ForeignKey(string? foreignKey ForeignTable = Expression.TableName }); - fk.ForeignKey.PrimaryColumns.Add(primaryColumnName); - fk.ForeignKey.ForeignColumns.Add(Expression.Column.Name); + fk.ForeignKey.PrimaryColumns.Add(primaryColumnName); + fk.ForeignKey.ForeignColumns.Add(Expression.Column.Name); - Expression.Expressions.Add(fk); - CurrentForeignKey = fk.ForeignKey; - return this; - } + Expression.Expressions.Add(fk); + CurrentForeignKey = fk.ForeignKey; + return this; + } - public ICreateColumnOptionForeignKeyCascadeBuilder ForeignKey() - { - Expression.Column.IsForeignKey = true; - return this; - } + public ICreateColumnOptionForeignKeyCascadeBuilder ForeignKey() + { + Expression.Column.IsForeignKey = true; + return this; + } - public ICreateColumnOptionForeignKeyCascadeBuilder ReferencedBy(string foreignTableName, string foreignColumnName) - { - return ReferencedBy(null, null, foreignTableName, foreignColumnName); - } + public ICreateColumnOptionForeignKeyCascadeBuilder + ReferencedBy(string foreignTableName, string foreignColumnName) => + ReferencedBy(null, null, foreignTableName, foreignColumnName); - public ICreateColumnOptionForeignKeyCascadeBuilder ReferencedBy(string foreignKeyName, string foreignTableName, - string foreignColumnName) - { - return ReferencedBy(foreignKeyName, null, foreignTableName, foreignColumnName); - } + public ICreateColumnOptionForeignKeyCascadeBuilder ReferencedBy(string foreignKeyName, string foreignTableName, + string foreignColumnName) => + ReferencedBy(foreignKeyName, null, foreignTableName, foreignColumnName); - public ICreateColumnOptionForeignKeyCascadeBuilder ReferencedBy(string? foreignKeyName, string? foreignTableSchema, - string foreignTableName, string foreignColumnName) - { - var fk = new CreateForeignKeyExpression(_context, new ForeignKeyDefinition + public ICreateColumnOptionForeignKeyCascadeBuilder ReferencedBy(string? foreignKeyName, string? foreignTableSchema, + string foreignTableName, string foreignColumnName) + { + var fk = new CreateForeignKeyExpression(_context, + new ForeignKeyDefinition { Name = foreignKeyName, PrimaryTable = Expression.TableName, @@ -186,39 +155,40 @@ public ICreateColumnOptionForeignKeyCascadeBuilder ReferencedBy(string? foreignK ForeignTableSchema = foreignTableSchema }); - fk.ForeignKey.PrimaryColumns.Add(Expression.Column.Name); - fk.ForeignKey.ForeignColumns.Add(foreignColumnName); + fk.ForeignKey.PrimaryColumns.Add(Expression.Column.Name); + fk.ForeignKey.ForeignColumns.Add(foreignColumnName); - Expression.Expressions.Add(fk); - CurrentForeignKey = fk.ForeignKey; - return this; - } + Expression.Expressions.Add(fk); + CurrentForeignKey = fk.ForeignKey; + return this; + } - public ICreateColumnOptionForeignKeyCascadeBuilder OnDelete(Rule rule) + public ICreateColumnOptionForeignKeyCascadeBuilder OnDelete(Rule rule) + { + if (CurrentForeignKey is not null) { - if (CurrentForeignKey is not null) - { - CurrentForeignKey.OnDelete = rule; - } - - return this; + CurrentForeignKey.OnDelete = rule; } - public ICreateColumnOptionForeignKeyCascadeBuilder OnUpdate(Rule rule) - { - if (CurrentForeignKey is not null) - { - CurrentForeignKey.OnUpdate = rule; - } - - return this; - } + return this; + } - public ICreateColumnOptionBuilder OnDeleteOrUpdate(Rule rule) + public ICreateColumnOptionForeignKeyCascadeBuilder OnUpdate(Rule rule) + { + if (CurrentForeignKey is not null) { - OnDelete(rule); - OnUpdate(rule); - return this; + CurrentForeignKey.OnUpdate = rule; } + + return this; } + + public ICreateColumnOptionBuilder OnDeleteOrUpdate(Rule rule) + { + OnDelete(rule); + OnUpdate(rule); + return this; + } + + public override ColumnDefinition GetColumnForType() => Expression.Column; } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnOnTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnOnTableBuilder.cs index 982b495ac863..837aaa3bf85a 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnOnTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnOnTableBuilder.cs @@ -1,12 +1,11 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column; + +public interface ICreateColumnOnTableBuilder : IColumnTypeBuilder { - public interface ICreateColumnOnTableBuilder : IColumnTypeBuilder - { - /// - /// Specifies the name of the table. - /// - ICreateColumnTypeBuilder OnTable(string name); - } + /// + /// Specifies the name of the table. + /// + ICreateColumnTypeBuilder OnTable(string name); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnOptionBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnOptionBuilder.cs index 9a4c2c647ea0..b3a7d58a70d8 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnOptionBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnOptionBuilder.cs @@ -1,8 +1,9 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column; + +public interface ICreateColumnOptionBuilder : + IColumnOptionBuilder + , IExecutableBuilder { - public interface ICreateColumnOptionBuilder : IColumnOptionBuilder - , IExecutableBuilder - { } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnOptionForeignKeyCascadeBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnOptionForeignKeyCascadeBuilder.cs index 25e0d792c40c..79cb0651d0f4 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnOptionForeignKeyCascadeBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnOptionForeignKeyCascadeBuilder.cs @@ -1,8 +1,8 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column; + +public interface ICreateColumnOptionForeignKeyCascadeBuilder : ICreateColumnOptionBuilder, + IForeignKeyCascadeBuilder { - public interface ICreateColumnOptionForeignKeyCascadeBuilder : ICreateColumnOptionBuilder, - IForeignKeyCascadeBuilder - { } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnTypeBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnTypeBuilder.cs index f1177efad38b..d7ba53992695 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnTypeBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnTypeBuilder.cs @@ -1,7 +1,7 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column; + +public interface ICreateColumnTypeBuilder : IColumnTypeBuilder { - public interface ICreateColumnTypeBuilder : IColumnTypeBuilder - { } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Constraint/CreateConstraintBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Constraint/CreateConstraintBuilder.cs index f61d99f2370b..460e4007c995 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Constraint/CreateConstraintBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Constraint/CreateConstraintBuilder.cs @@ -1,36 +1,39 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Constraint +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Constraint; + +public class CreateConstraintBuilder : ExpressionBuilderBase, + ICreateConstraintOnTableBuilder, + ICreateConstraintColumnsBuilder { - public class CreateConstraintBuilder : ExpressionBuilderBase, - ICreateConstraintOnTableBuilder, - ICreateConstraintColumnsBuilder + public CreateConstraintBuilder(CreateConstraintExpression expression) + : base(expression) { - public CreateConstraintBuilder(CreateConstraintExpression expression) - : base(expression) - { } + } - /// - public ICreateConstraintColumnsBuilder OnTable(string tableName) - { - Expression.Constraint.TableName = tableName; - return this; - } + /// + public IExecutableBuilder Column(string columnName) + { + Expression.Constraint.Columns.Add(columnName); + return new ExecutableBuilder(Expression); + } - /// - public IExecutableBuilder Column(string columnName) + /// + public IExecutableBuilder Columns(string[] columnNames) + { + foreach (var columnName in columnNames) { Expression.Constraint.Columns.Add(columnName); - return new ExecutableBuilder(Expression); } - /// - public IExecutableBuilder Columns(string[] columnNames) - { - foreach (var columnName in columnNames) - Expression.Constraint.Columns.Add(columnName); - return new ExecutableBuilder(Expression); - } + return new ExecutableBuilder(Expression); + } + + /// + public ICreateConstraintColumnsBuilder OnTable(string tableName) + { + Expression.Constraint.TableName = tableName; + return this; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Constraint/ICreateConstraintColumnsBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Constraint/ICreateConstraintColumnsBuilder.cs index cfc756868638..a90a07ac2ae1 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Constraint/ICreateConstraintColumnsBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Constraint/ICreateConstraintColumnsBuilder.cs @@ -1,17 +1,16 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Constraint +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Constraint; + +public interface ICreateConstraintColumnsBuilder : IFluentBuilder { - public interface ICreateConstraintColumnsBuilder : IFluentBuilder - { - /// - /// Specifies the constraint column. - /// - IExecutableBuilder Column(string columnName); + /// + /// Specifies the constraint column. + /// + IExecutableBuilder Column(string columnName); - /// - /// Specifies the constraint columns. - /// - IExecutableBuilder Columns(string[] columnNames); - } + /// + /// Specifies the constraint columns. + /// + IExecutableBuilder Columns(string[] columnNames); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Constraint/ICreateConstraintOnTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Constraint/ICreateConstraintOnTableBuilder.cs index 01d2da0cd199..2ba8fcc749db 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Constraint/ICreateConstraintOnTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Constraint/ICreateConstraintOnTableBuilder.cs @@ -1,10 +1,9 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Constraint +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Constraint; + +public interface ICreateConstraintOnTableBuilder : IFluentBuilder { - public interface ICreateConstraintOnTableBuilder : IFluentBuilder - { - /// - /// Specifies the table name. - /// - ICreateConstraintColumnsBuilder OnTable(string tableName); - } + /// + /// Specifies the table name. + /// + ICreateConstraintColumnsBuilder OnTable(string tableName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/CreateBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/CreateBuilder.cs index b672b1e5d48f..0901129ea09f 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/CreateBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/CreateBuilder.cs @@ -1,5 +1,4 @@ -using System; -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common.Expressions; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Constraint; @@ -10,121 +9,112 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create; + +public class CreateBuilder : ICreateBuilder { - public class CreateBuilder : ICreateBuilder + private readonly IMigrationContext _context; + + public CreateBuilder(IMigrationContext context) => + _context = context ?? throw new ArgumentNullException(nameof(context)); + + /// + public IExecutableBuilder Table(bool withoutKeysAndIndexes = false) => + new CreateTableOfDtoBuilder(_context) {TypeOfDto = typeof(TDto), WithoutKeysAndIndexes = withoutKeysAndIndexes}; + + /// + public IExecutableBuilder KeysAndIndexes() => + new CreateKeysAndIndexesBuilder(_context) {TypeOfDto = typeof(TDto)}; + + /// + public IExecutableBuilder KeysAndIndexes(Type typeOfDto) => + new CreateKeysAndIndexesBuilder(_context) {TypeOfDto = typeOfDto}; + + /// + public ICreateTableWithColumnBuilder Table(string tableName) + { + var expression = new CreateTableExpression(_context) {TableName = tableName}; + return new CreateTableBuilder(_context, expression); + } + + /// + public ICreateColumnOnTableBuilder Column(string columnName) + { + var expression = new CreateColumnExpression(_context) {Column = {Name = columnName}}; + return new CreateColumnBuilder(_context, expression); + } + + /// + public ICreateForeignKeyFromTableBuilder ForeignKey() + { + var expression = new CreateForeignKeyExpression(_context); + return new CreateForeignKeyBuilder(expression); + } + + /// + public ICreateForeignKeyFromTableBuilder ForeignKey(string foreignKeyName) + { + var expression = new CreateForeignKeyExpression(_context) {ForeignKey = {Name = foreignKeyName}}; + return new CreateForeignKeyBuilder(expression); + } + + /// + public ICreateIndexForTableBuilder Index() + { + var expression = new CreateIndexExpression(_context); + return new CreateIndexBuilder(expression); + } + + /// + public ICreateIndexForTableBuilder Index(string indexName) + { + var expression = new CreateIndexExpression(_context) {Index = {Name = indexName}}; + return new CreateIndexBuilder(expression); + } + + /// + public ICreateConstraintOnTableBuilder PrimaryKey() => PrimaryKey(true); + + /// + public ICreateConstraintOnTableBuilder PrimaryKey(bool clustered) + { + var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey); + expression.Constraint.IsPrimaryKeyClustered = clustered; + return new CreateConstraintBuilder(expression); + } + + /// + public ICreateConstraintOnTableBuilder PrimaryKey(string primaryKeyName) => PrimaryKey(primaryKeyName, true); + + /// + public ICreateConstraintOnTableBuilder PrimaryKey(string primaryKeyName, bool clustered) + { + var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey); + expression.Constraint.ConstraintName = primaryKeyName; + expression.Constraint.IsPrimaryKeyClustered = clustered; + return new CreateConstraintBuilder(expression); + } + + /// + public ICreateConstraintOnTableBuilder UniqueConstraint() + { + var expression = new CreateConstraintExpression(_context, ConstraintType.Unique); + return new CreateConstraintBuilder(expression); + } + + /// + public ICreateConstraintOnTableBuilder UniqueConstraint(string constraintName) + { + var expression = new CreateConstraintExpression(_context, ConstraintType.Unique); + expression.Constraint.ConstraintName = constraintName; + return new CreateConstraintBuilder(expression); + } + + /// + public ICreateConstraintOnTableBuilder Constraint(string constraintName) { - private readonly IMigrationContext _context; - - public CreateBuilder(IMigrationContext context) - { - _context = context ?? throw new ArgumentNullException(nameof(context)); - } - - /// - public IExecutableBuilder Table(bool withoutKeysAndIndexes = false) - { - return new CreateTableOfDtoBuilder(_context) { TypeOfDto = typeof(TDto), WithoutKeysAndIndexes = withoutKeysAndIndexes }; - } - - /// - public IExecutableBuilder KeysAndIndexes() - { - return new CreateKeysAndIndexesBuilder(_context) { TypeOfDto = typeof(TDto) }; - } - - /// - public IExecutableBuilder KeysAndIndexes(Type typeOfDto) - { - return new CreateKeysAndIndexesBuilder(_context) { TypeOfDto = typeOfDto }; - } - - /// - public ICreateTableWithColumnBuilder Table(string tableName) - { - var expression = new CreateTableExpression(_context) { TableName = tableName }; - return new CreateTableBuilder(_context, expression); - } - - /// - public ICreateColumnOnTableBuilder Column(string columnName) - { - var expression = new CreateColumnExpression(_context) { Column = { Name = columnName } }; - return new CreateColumnBuilder(_context, expression); - } - - /// - public ICreateForeignKeyFromTableBuilder ForeignKey() - { - var expression = new CreateForeignKeyExpression(_context); - return new CreateForeignKeyBuilder(expression); - } - - /// - public ICreateForeignKeyFromTableBuilder ForeignKey(string foreignKeyName) - { - var expression = new CreateForeignKeyExpression(_context) { ForeignKey = { Name = foreignKeyName } }; - return new CreateForeignKeyBuilder(expression); - } - - /// - public ICreateIndexForTableBuilder Index() - { - var expression = new CreateIndexExpression(_context); - return new CreateIndexBuilder(expression); - } - - /// - public ICreateIndexForTableBuilder Index(string indexName) - { - var expression = new CreateIndexExpression(_context) { Index = { Name = indexName } }; - return new CreateIndexBuilder(expression); - } - - /// - public ICreateConstraintOnTableBuilder PrimaryKey() => PrimaryKey(true); - - /// - public ICreateConstraintOnTableBuilder PrimaryKey(bool clustered) - { - var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey); - expression.Constraint.IsPrimaryKeyClustered = clustered; - return new CreateConstraintBuilder(expression); - } - - /// - public ICreateConstraintOnTableBuilder PrimaryKey(string primaryKeyName) => PrimaryKey(primaryKeyName, true); - - /// - public ICreateConstraintOnTableBuilder PrimaryKey(string primaryKeyName, bool clustered) - { - var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey); - expression.Constraint.ConstraintName = primaryKeyName; - expression.Constraint.IsPrimaryKeyClustered = clustered; - return new CreateConstraintBuilder(expression); - } - - /// - public ICreateConstraintOnTableBuilder UniqueConstraint() - { - var expression = new CreateConstraintExpression(_context, ConstraintType.Unique); - return new CreateConstraintBuilder(expression); - } - - /// - public ICreateConstraintOnTableBuilder UniqueConstraint(string constraintName) - { - var expression = new CreateConstraintExpression(_context, ConstraintType.Unique); - expression.Constraint.ConstraintName = constraintName; - return new CreateConstraintBuilder(expression); - } - - /// - public ICreateConstraintOnTableBuilder Constraint(string constraintName) - { - var expression = new CreateConstraintExpression(_context, ConstraintType.NonUnique); - expression.Constraint.ConstraintName = constraintName; - return new CreateConstraintBuilder(expression); - } + var expression = new CreateConstraintExpression(_context, ConstraintType.NonUnique); + expression.Constraint.ConstraintName = constraintName; + return new CreateConstraintBuilder(expression); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Expressions/CreateConstraintExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Expressions/CreateConstraintExpression.cs index 7440d6c837fc..f5c756d2ef57 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Expressions/CreateConstraintExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Expressions/CreateConstraintExpression.cs @@ -1,40 +1,40 @@ -using System.Linq; -using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Expressions; + +public class CreateConstraintExpression : MigrationExpressionBase { - public class CreateConstraintExpression : MigrationExpressionBase + public CreateConstraintExpression(IMigrationContext context, ConstraintType constraint) + : base(context) => + Constraint = new ConstraintDefinition(constraint); + + public ConstraintDefinition Constraint { get; } + + protected override string GetSql() { - public CreateConstraintExpression(IMigrationContext context, ConstraintType constraint) - : base(context) + var constraintType = Constraint.IsPrimaryKeyConstraint ? "PRIMARY KEY" : "UNIQUE"; + + if (Constraint.IsPrimaryKeyConstraint && SqlSyntax.SupportsClustered()) { - Constraint = new ConstraintDefinition(constraint); + constraintType += Constraint.IsPrimaryKeyClustered ? " CLUSTERED" : " NONCLUSTERED"; } - public ConstraintDefinition Constraint { get; } - - protected override string GetSql() + if (Constraint.IsNonUniqueConstraint) { - var constraintType = (Constraint.IsPrimaryKeyConstraint) ? "PRIMARY KEY" : "UNIQUE"; - - if (Constraint.IsPrimaryKeyConstraint && SqlSyntax.SupportsClustered()) - constraintType += Constraint.IsPrimaryKeyClustered ? " CLUSTERED" : " NONCLUSTERED"; - - if (Constraint.IsNonUniqueConstraint) - constraintType = string.Empty; - - var columns = new string[Constraint.Columns.Count]; + constraintType = string.Empty; + } - for (var i = 0; i < Constraint.Columns.Count; i++) - { - columns[i] = SqlSyntax.GetQuotedColumnName(Constraint.Columns.ElementAt(i)); - } + var columns = new string[Constraint.Columns.Count]; - return string.Format(SqlSyntax.CreateConstraint, - SqlSyntax.GetQuotedTableName(Constraint.TableName), - SqlSyntax.GetQuotedName(Constraint.ConstraintName), - constraintType, - string.Join(", ", columns)); + for (var i = 0; i < Constraint.Columns.Count; i++) + { + columns[i] = SqlSyntax.GetQuotedColumnName(Constraint.Columns.ElementAt(i)); } + + return string.Format(SqlSyntax.CreateConstraint, + SqlSyntax.GetQuotedTableName(Constraint.TableName), + SqlSyntax.GetQuotedName(Constraint.ConstraintName), + constraintType, + string.Join(", ", columns)); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Expressions/CreateTableExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Expressions/CreateTableExpression.cs index e7ed2faf53d0..3be140e5617a 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Expressions/CreateTableExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Expressions/CreateTableExpression.cs @@ -1,25 +1,21 @@ -using System.Collections.Generic; -using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Expressions; + +public class CreateTableExpression : MigrationExpressionBase { - public class CreateTableExpression : MigrationExpressionBase - { - public CreateTableExpression(IMigrationContext context) - : base(context) - { - Columns = new List(); - } + public CreateTableExpression(IMigrationContext context) + : base(context) => + Columns = new List(); - public virtual string SchemaName { get; set; } = null!; - public virtual string TableName { get; set; } = null!; - public virtual IList Columns { get; set; } + public virtual string SchemaName { get; set; } = null!; + public virtual string TableName { get; set; } = null!; + public virtual IList Columns { get; set; } - protected override string GetSql() - { - var table = new TableDefinition { Name = TableName, SchemaName = SchemaName, Columns = Columns }; + protected override string GetSql() + { + var table = new TableDefinition {Name = TableName, SchemaName = SchemaName, Columns = Columns}; - return string.Format(SqlSyntax.Format(table)); - } + return string.Format(SqlSyntax.Format(table)); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/CreateForeignKeyBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/CreateForeignKeyBuilder.cs index 4a30a815a257..4d305e04ddf5 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/CreateForeignKeyBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/CreateForeignKeyBuilder.cs @@ -2,86 +2,92 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey; + +public class CreateForeignKeyBuilder : ExpressionBuilderBase, + ICreateForeignKeyFromTableBuilder, + ICreateForeignKeyForeignColumnBuilder, + ICreateForeignKeyToTableBuilder, + ICreateForeignKeyPrimaryColumnBuilder, + ICreateForeignKeyCascadeBuilder { - public class CreateForeignKeyBuilder : ExpressionBuilderBase, - ICreateForeignKeyFromTableBuilder, - ICreateForeignKeyForeignColumnBuilder, - ICreateForeignKeyToTableBuilder, - ICreateForeignKeyPrimaryColumnBuilder, - ICreateForeignKeyCascadeBuilder + public CreateForeignKeyBuilder(CreateForeignKeyExpression expression) + : base(expression) { - public CreateForeignKeyBuilder(CreateForeignKeyExpression expression) - : base(expression) - { } + } - /// - public void Do() => Expression.Execute(); + /// + public void Do() => Expression.Execute(); - /// - public ICreateForeignKeyForeignColumnBuilder FromTable(string table) - { - Expression.ForeignKey.ForeignTable = table; - return this; - } + /// + public ICreateForeignKeyCascadeBuilder OnDelete(Rule rule) + { + Expression.ForeignKey.OnDelete = rule; + return this; + } - /// - public ICreateForeignKeyToTableBuilder ForeignColumn(string column) - { - Expression.ForeignKey.ForeignColumns.Add(column); - return this; - } + /// + public ICreateForeignKeyCascadeBuilder OnUpdate(Rule rule) + { + Expression.ForeignKey.OnUpdate = rule; + return this; + } - /// - public ICreateForeignKeyToTableBuilder ForeignColumns(params string[] columns) - { - foreach (var column in columns) - Expression.ForeignKey.ForeignColumns.Add(column); - return this; - } + /// + public IExecutableBuilder OnDeleteOrUpdate(Rule rule) + { + Expression.ForeignKey.OnDelete = rule; + Expression.ForeignKey.OnUpdate = rule; + return new ExecutableBuilder(Expression); + } - /// - public ICreateForeignKeyPrimaryColumnBuilder ToTable(string table) - { - Expression.ForeignKey.PrimaryTable = table; - return this; - } + /// + public ICreateForeignKeyToTableBuilder ForeignColumn(string column) + { + Expression.ForeignKey.ForeignColumns.Add(column); + return this; + } - /// - public ICreateForeignKeyCascadeBuilder PrimaryColumn(string column) + /// + public ICreateForeignKeyToTableBuilder ForeignColumns(params string[] columns) + { + foreach (var column in columns) { - Expression.ForeignKey.PrimaryColumns.Add(column); - return this; + Expression.ForeignKey.ForeignColumns.Add(column); } - /// - public ICreateForeignKeyCascadeBuilder PrimaryColumns(params string[] columns) - { - foreach (var column in columns) - Expression.ForeignKey.PrimaryColumns.Add(column); - return this; - } + return this; + } - /// - public ICreateForeignKeyCascadeBuilder OnDelete(Rule rule) - { - Expression.ForeignKey.OnDelete = rule; - return this; - } + /// + public ICreateForeignKeyForeignColumnBuilder FromTable(string table) + { + Expression.ForeignKey.ForeignTable = table; + return this; + } - /// - public ICreateForeignKeyCascadeBuilder OnUpdate(Rule rule) - { - Expression.ForeignKey.OnUpdate = rule; - return this; - } + /// + public ICreateForeignKeyCascadeBuilder PrimaryColumn(string column) + { + Expression.ForeignKey.PrimaryColumns.Add(column); + return this; + } - /// - public IExecutableBuilder OnDeleteOrUpdate(Rule rule) + /// + public ICreateForeignKeyCascadeBuilder PrimaryColumns(params string[] columns) + { + foreach (var column in columns) { - Expression.ForeignKey.OnDelete = rule; - Expression.ForeignKey.OnUpdate = rule; - return new ExecutableBuilder(Expression); + Expression.ForeignKey.PrimaryColumns.Add(column); } + + return this; + } + + /// + public ICreateForeignKeyPrimaryColumnBuilder ToTable(string table) + { + Expression.ForeignKey.PrimaryTable = table; + return this; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyCascadeBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyCascadeBuilder.cs index 3b45404b8506..8645c2065906 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyCascadeBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyCascadeBuilder.cs @@ -1,12 +1,11 @@ using System.Data; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey; + +public interface ICreateForeignKeyCascadeBuilder : IFluentBuilder, IExecutableBuilder { - public interface ICreateForeignKeyCascadeBuilder : IFluentBuilder, IExecutableBuilder - { - ICreateForeignKeyCascadeBuilder OnDelete(Rule rule); - ICreateForeignKeyCascadeBuilder OnUpdate(Rule rule); - IExecutableBuilder OnDeleteOrUpdate(Rule rule); - } + ICreateForeignKeyCascadeBuilder OnDelete(Rule rule); + ICreateForeignKeyCascadeBuilder OnUpdate(Rule rule); + IExecutableBuilder OnDeleteOrUpdate(Rule rule); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyForeignColumnBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyForeignColumnBuilder.cs index 8f37b40487f3..51543e1a6b8a 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyForeignColumnBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyForeignColumnBuilder.cs @@ -1,8 +1,7 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey; + +public interface ICreateForeignKeyForeignColumnBuilder : IFluentBuilder { - public interface ICreateForeignKeyForeignColumnBuilder : IFluentBuilder - { - ICreateForeignKeyToTableBuilder ForeignColumn(string column); - ICreateForeignKeyToTableBuilder ForeignColumns(params string[] columns); - } + ICreateForeignKeyToTableBuilder ForeignColumn(string column); + ICreateForeignKeyToTableBuilder ForeignColumns(params string[] columns); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyFromTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyFromTableBuilder.cs index 941647e27d94..72dd1b2476be 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyFromTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyFromTableBuilder.cs @@ -1,7 +1,6 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey; + +public interface ICreateForeignKeyFromTableBuilder : IFluentBuilder { - public interface ICreateForeignKeyFromTableBuilder : IFluentBuilder - { - ICreateForeignKeyForeignColumnBuilder FromTable(string table); - } + ICreateForeignKeyForeignColumnBuilder FromTable(string table); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyPrimaryColumnBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyPrimaryColumnBuilder.cs index 95d1346d0f49..9c93ee3d4925 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyPrimaryColumnBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyPrimaryColumnBuilder.cs @@ -1,8 +1,7 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey; + +public interface ICreateForeignKeyPrimaryColumnBuilder : IFluentBuilder { - public interface ICreateForeignKeyPrimaryColumnBuilder : IFluentBuilder - { - ICreateForeignKeyCascadeBuilder PrimaryColumn(string column); - ICreateForeignKeyCascadeBuilder PrimaryColumns(params string[] columns); - } + ICreateForeignKeyCascadeBuilder PrimaryColumn(string column); + ICreateForeignKeyCascadeBuilder PrimaryColumns(params string[] columns); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyToTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyToTableBuilder.cs index 1ea49b136968..e4b56eadb071 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyToTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyToTableBuilder.cs @@ -1,7 +1,6 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey; + +public interface ICreateForeignKeyToTableBuilder : IFluentBuilder { - public interface ICreateForeignKeyToTableBuilder : IFluentBuilder - { - ICreateForeignKeyPrimaryColumnBuilder ToTable(string table); - } + ICreateForeignKeyPrimaryColumnBuilder ToTable(string table); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ICreateBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ICreateBuilder.cs index d01326eb0dd4..3a4ec3dfeda4 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ICreateBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ICreateBuilder.cs @@ -1,96 +1,94 @@ -using System; -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Constraint; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Index; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create; + +/// +/// Builds a Create expression. +/// +public interface ICreateBuilder : IFluentBuilder { /// - /// Builds a Create expression. - /// - public interface ICreateBuilder : IFluentBuilder - { - /// - /// Builds a Create Table expression, and executes. - /// - IExecutableBuilder Table(bool withoutKeysAndIndexes = false); - - /// - /// Builds a Create Keys and Indexes expression, and executes. - /// - IExecutableBuilder KeysAndIndexes(); - - /// - /// Builds a Create Keys and Indexes expression, and executes. - /// - IExecutableBuilder KeysAndIndexes(Type typeOfDto); - - /// - /// Builds a Create Table expression. - /// - ICreateTableWithColumnBuilder Table(string tableName); - - /// - /// Builds a Create Column expression. - /// - ICreateColumnOnTableBuilder Column(string columnName); - - /// - /// Builds a Create Foreign Key expression. - /// - ICreateForeignKeyFromTableBuilder ForeignKey(); - - /// - /// Builds a Create Foreign Key expression. - /// - ICreateForeignKeyFromTableBuilder ForeignKey(string foreignKeyName); - - /// - /// Builds a Create Index expression. - /// - ICreateIndexForTableBuilder Index(); - - /// - /// Builds a Create Index expression. - /// - ICreateIndexForTableBuilder Index(string indexName); - - /// - /// Builds a Create Primary Key expression. - /// - ICreateConstraintOnTableBuilder PrimaryKey(); - - /// - /// Builds a Create Primary Key expression. - /// - ICreateConstraintOnTableBuilder PrimaryKey(string primaryKeyName); - - /// - /// Builds a Create Primary Key expression. - /// - ICreateConstraintOnTableBuilder PrimaryKey(bool clustered); - - /// - /// Builds a Create Primary Key expression. - /// - ICreateConstraintOnTableBuilder PrimaryKey(string primaryKeyName, bool clustered); - - /// - /// Builds a Create Unique Constraint expression. - /// - ICreateConstraintOnTableBuilder UniqueConstraint(); - - /// - /// Builds a Create Unique Constraint expression. - /// - ICreateConstraintOnTableBuilder UniqueConstraint(string constraintName); - - /// - /// Builds a Create Constraint expression. - /// - ICreateConstraintOnTableBuilder Constraint(string constraintName); - } + /// Builds a Create Table expression, and executes. + /// + IExecutableBuilder Table(bool withoutKeysAndIndexes = false); + + /// + /// Builds a Create Keys and Indexes expression, and executes. + /// + IExecutableBuilder KeysAndIndexes(); + + /// + /// Builds a Create Keys and Indexes expression, and executes. + /// + IExecutableBuilder KeysAndIndexes(Type typeOfDto); + + /// + /// Builds a Create Table expression. + /// + ICreateTableWithColumnBuilder Table(string tableName); + + /// + /// Builds a Create Column expression. + /// + ICreateColumnOnTableBuilder Column(string columnName); + + /// + /// Builds a Create Foreign Key expression. + /// + ICreateForeignKeyFromTableBuilder ForeignKey(); + + /// + /// Builds a Create Foreign Key expression. + /// + ICreateForeignKeyFromTableBuilder ForeignKey(string foreignKeyName); + + /// + /// Builds a Create Index expression. + /// + ICreateIndexForTableBuilder Index(); + + /// + /// Builds a Create Index expression. + /// + ICreateIndexForTableBuilder Index(string indexName); + + /// + /// Builds a Create Primary Key expression. + /// + ICreateConstraintOnTableBuilder PrimaryKey(); + + /// + /// Builds a Create Primary Key expression. + /// + ICreateConstraintOnTableBuilder PrimaryKey(string primaryKeyName); + + /// + /// Builds a Create Primary Key expression. + /// + ICreateConstraintOnTableBuilder PrimaryKey(bool clustered); + + /// + /// Builds a Create Primary Key expression. + /// + ICreateConstraintOnTableBuilder PrimaryKey(string primaryKeyName, bool clustered); + + /// + /// Builds a Create Unique Constraint expression. + /// + ICreateConstraintOnTableBuilder UniqueConstraint(); + + /// + /// Builds a Create Unique Constraint expression. + /// + ICreateConstraintOnTableBuilder UniqueConstraint(string constraintName); + + /// + /// Builds a Create Constraint expression. + /// + ICreateConstraintOnTableBuilder Constraint(string constraintName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/CreateIndexBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/CreateIndexBuilder.cs index a6024c19db5d..2752af4a8d66 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/CreateIndexBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/CreateIndexBuilder.cs @@ -3,92 +3,89 @@ using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Index +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Index; + +public class CreateIndexBuilder : ExpressionBuilderBase, + ICreateIndexForTableBuilder, + ICreateIndexOnColumnBuilder, + ICreateIndexColumnOptionsBuilder, + ICreateIndexOptionsBuilder { - public class CreateIndexBuilder : ExpressionBuilderBase, - ICreateIndexForTableBuilder, - ICreateIndexOnColumnBuilder, - ICreateIndexColumnOptionsBuilder, - ICreateIndexOptionsBuilder + public CreateIndexBuilder(CreateIndexExpression expression) + : base(expression) { - public CreateIndexBuilder(CreateIndexExpression expression) - : base(expression) - { } - - /// - public void Do() => Expression.Execute(); + } - public IndexColumnDefinition? CurrentColumn { get; set; } + public IndexColumnDefinition? CurrentColumn { get; set; } - /// - public ICreateIndexOnColumnBuilder OnTable(string tableName) + /// + public ICreateIndexOnColumnBuilder Ascending() + { + if (CurrentColumn is not null) { - Expression.Index.TableName = tableName; - return this; + CurrentColumn.Direction = Direction.Ascending; } - /// - public ICreateIndexColumnOptionsBuilder OnColumn(string columnName) - { - CurrentColumn = new IndexColumnDefinition { Name = columnName }; - Expression.Index.Columns.Add(CurrentColumn); - return this; - } + return this; + } - /// - public ICreateIndexOptionsBuilder WithOptions() + /// + public ICreateIndexOnColumnBuilder Descending() + { + if (CurrentColumn is not null) { - return this; + CurrentColumn.Direction = Direction.Descending; } - /// - public ICreateIndexOnColumnBuilder Ascending() - { - if (CurrentColumn is not null) - { - CurrentColumn.Direction = Direction.Ascending; - } + return this; + } - return this; - } + /// + ICreateIndexOnColumnBuilder ICreateIndexColumnOptionsBuilder.Unique() + { + Expression.Index.IndexType = IndexTypes.UniqueNonClustered; + return this; + } - /// - public ICreateIndexOnColumnBuilder Descending() - { - if (CurrentColumn is not null) - { - CurrentColumn.Direction = Direction.Descending; - } + /// + public ICreateIndexOnColumnBuilder OnTable(string tableName) + { + Expression.Index.TableName = tableName; + return this; + } - return this; - } + /// + public void Do() => Expression.Execute(); - /// - ICreateIndexOnColumnBuilder ICreateIndexColumnOptionsBuilder.Unique() - { - Expression.Index.IndexType = IndexTypes.UniqueNonClustered; - return this; - } + /// + public ICreateIndexColumnOptionsBuilder OnColumn(string columnName) + { + CurrentColumn = new IndexColumnDefinition {Name = columnName}; + Expression.Index.Columns.Add(CurrentColumn); + return this; + } - /// - public ICreateIndexOnColumnBuilder NonClustered() - { - Expression.Index.IndexType = IndexTypes.NonClustered; - return this; - } + /// + public ICreateIndexOptionsBuilder WithOptions() => this; - /// - public ICreateIndexOnColumnBuilder Clustered() - { - Expression.Index.IndexType = IndexTypes.Clustered; - return this; - } + /// + public ICreateIndexOnColumnBuilder NonClustered() + { + Expression.Index.IndexType = IndexTypes.NonClustered; + return this; + } - /// - ICreateIndexOnColumnBuilder ICreateIndexOptionsBuilder.Unique() - { - Expression.Index.IndexType = IndexTypes.UniqueNonClustered; - return this; - } + /// + public ICreateIndexOnColumnBuilder Clustered() + { + Expression.Index.IndexType = IndexTypes.Clustered; + return this; + } + + /// + ICreateIndexOnColumnBuilder ICreateIndexOptionsBuilder.Unique() + { + Expression.Index.IndexType = IndexTypes.UniqueNonClustered; + return this; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexColumnOptionsBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexColumnOptionsBuilder.cs index 037e9e71f59c..21b03a9c0b6f 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexColumnOptionsBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexColumnOptionsBuilder.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Index +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Index; + +public interface ICreateIndexColumnOptionsBuilder : IFluentBuilder { - public interface ICreateIndexColumnOptionsBuilder : IFluentBuilder - { - ICreateIndexOnColumnBuilder Ascending(); - ICreateIndexOnColumnBuilder Descending(); - ICreateIndexOnColumnBuilder Unique(); - } + ICreateIndexOnColumnBuilder Ascending(); + ICreateIndexOnColumnBuilder Descending(); + ICreateIndexOnColumnBuilder Unique(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexForTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexForTableBuilder.cs index c74c5b546e12..1ff61e0d87ca 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexForTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexForTableBuilder.cs @@ -1,7 +1,6 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Index +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Index; + +public interface ICreateIndexForTableBuilder : IFluentBuilder { - public interface ICreateIndexForTableBuilder : IFluentBuilder - { - ICreateIndexOnColumnBuilder OnTable(string tableName); - } + ICreateIndexOnColumnBuilder OnTable(string tableName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexOnColumnBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexOnColumnBuilder.cs index 4981186fa35b..5a101dbda6d8 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexOnColumnBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexOnColumnBuilder.cs @@ -1,17 +1,16 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Index +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Index; + +public interface ICreateIndexOnColumnBuilder : IFluentBuilder, IExecutableBuilder { - public interface ICreateIndexOnColumnBuilder : IFluentBuilder, IExecutableBuilder - { - /// - /// Specifies the index column. - /// - ICreateIndexColumnOptionsBuilder OnColumn(string columnName); + /// + /// Specifies the index column. + /// + ICreateIndexColumnOptionsBuilder OnColumn(string columnName); - /// - /// Specifies options. - /// - ICreateIndexOptionsBuilder WithOptions(); - } + /// + /// Specifies options. + /// + ICreateIndexOptionsBuilder WithOptions(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexOptionsBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexOptionsBuilder.cs index fc2e4f2a5385..cb7c9ad599a2 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexOptionsBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexOptionsBuilder.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Index +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Index; + +public interface ICreateIndexOptionsBuilder : IFluentBuilder { - public interface ICreateIndexOptionsBuilder : IFluentBuilder - { - ICreateIndexOnColumnBuilder Unique(); - ICreateIndexOnColumnBuilder NonClustered(); - ICreateIndexOnColumnBuilder Clustered(); - } + ICreateIndexOnColumnBuilder Unique(); + ICreateIndexOnColumnBuilder NonClustered(); + ICreateIndexOnColumnBuilder Clustered(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/KeysAndIndexes/CreateKeysAndIndexesBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/KeysAndIndexes/CreateKeysAndIndexesBuilder.cs index 86c3dc537a06..b78c2065af2c 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/KeysAndIndexes/CreateKeysAndIndexesBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/KeysAndIndexes/CreateKeysAndIndexesBuilder.cs @@ -1,59 +1,62 @@ -using System; -using NPoco; +using NPoco; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Execute.Expressions; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; +using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.KeysAndIndexes +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.KeysAndIndexes; + +public class CreateKeysAndIndexesBuilder : IExecutableBuilder { - public class CreateKeysAndIndexesBuilder : IExecutableBuilder + private readonly IMigrationContext _context; + private readonly DatabaseType[] _supportedDatabaseTypes; + + public CreateKeysAndIndexesBuilder(IMigrationContext context, params DatabaseType[] supportedDatabaseTypes) { - private readonly IMigrationContext _context; - private readonly DatabaseType[] _supportedDatabaseTypes; + _context = context; + _supportedDatabaseTypes = supportedDatabaseTypes; + } - public CreateKeysAndIndexesBuilder(IMigrationContext context, params DatabaseType[] supportedDatabaseTypes) + public Type? TypeOfDto { get; set; } + + /// + public void Do() + { + ISqlSyntaxProvider syntax = _context.SqlContext.SqlSyntax; + if (TypeOfDto is null) { - _context = context; - _supportedDatabaseTypes = supportedDatabaseTypes; + return; } - public Type? TypeOfDto { get; set; } + TableDefinition tableDefinition = DefinitionFactory.GetTableDefinition(TypeOfDto, syntax); + + // note: of course we are creating the keys and indexes as per the DTO, so + // changing the DTO may break old migrations - or, better, these migrations + // should capture a copy of the DTO class that will not change - /// - public void Do() + ExecuteSql(syntax.FormatPrimaryKey(tableDefinition)); + foreach (var sql in syntax.Format(tableDefinition.Indexes)) { - var syntax = _context.SqlContext.SqlSyntax; - if (TypeOfDto is null) - { - return; - } - var tableDefinition = DefinitionFactory.GetTableDefinition(TypeOfDto, syntax); - - // note: of course we are creating the keys and indexes as per the DTO, so - // changing the DTO may break old migrations - or, better, these migrations - // should capture a copy of the DTO class that will not change - - ExecuteSql(syntax.FormatPrimaryKey(tableDefinition)); - foreach (var sql in syntax.Format(tableDefinition.Indexes)) - ExecuteSql(sql); - foreach (var sql in syntax.Format(tableDefinition.ForeignKeys)) - ExecuteSql(sql); + ExecuteSql(sql); + } - // note: we do *not* create the DF_ default constraints - /* - foreach (var column in tableDefinition.Columns) - { - var sql = syntax.FormatDefaultConstraint(column); - if (!sql.IsNullOrWhiteSpace()) - ExecuteSql(sql); - } - */ + foreach (var sql in syntax.Format(tableDefinition.ForeignKeys)) + { + ExecuteSql(sql); } - private void ExecuteSql(string sql) + // note: we do *not* create the DF_ default constraints + /* + foreach (var column in tableDefinition.Columns) { - new ExecuteSqlStatementExpression(_context) { SqlStatement = sql } - .Execute(); + var sql = syntax.FormatDefaultConstraint(column); + if (!sql.IsNullOrWhiteSpace()) + ExecuteSql(sql); } + */ } + + private void ExecuteSql(string sql) => + new ExecuteSqlStatementExpression(_context) {SqlStatement = sql} + .Execute(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableBuilder.cs index 81e3a702ce38..02d1294fc084 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableBuilder.cs @@ -4,153 +4,136 @@ using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table; + +public class CreateTableBuilder : ExpressionBuilderBase, + ICreateTableColumnAsTypeBuilder, + ICreateTableColumnOptionForeignKeyCascadeBuilder { - public class CreateTableBuilder : ExpressionBuilderBase, - ICreateTableColumnAsTypeBuilder, - ICreateTableColumnOptionForeignKeyCascadeBuilder - { - private readonly IMigrationContext _context; + private readonly IMigrationContext _context; - public CreateTableBuilder(IMigrationContext context, CreateTableExpression expression) - : base(expression) - { - _context = context; - } + public CreateTableBuilder(IMigrationContext context, CreateTableExpression expression) + : base(expression) => + _context = context; - /// - public void Do() => Expression.Execute(); + public ColumnDefinition CurrentColumn { get; set; } = null!; - public ColumnDefinition CurrentColumn { get; set; } = null!; + public ForeignKeyDefinition CurrentForeignKey { get; set; } = null!; - public ForeignKeyDefinition CurrentForeignKey { get; set; } = null!; + /// + public void Do() => Expression.Execute(); - public override ColumnDefinition GetColumnForType() + /// + public ICreateTableColumnAsTypeBuilder WithColumn(string name) + { + var column = new ColumnDefinition { - return CurrentColumn; - } + Name = name, TableName = Expression.TableName, ModificationType = ModificationType.Create + }; + Expression.Columns.Add(column); + CurrentColumn = column; + return this; + } - /// - public ICreateTableColumnAsTypeBuilder WithColumn(string name) - { - var column = new ColumnDefinition { Name = name, TableName = Expression.TableName, ModificationType = ModificationType.Create }; - Expression.Columns.Add(column); - CurrentColumn = column; - return this; - } - - /// - public ICreateTableColumnOptionBuilder WithDefault(SystemMethods method) - { - CurrentColumn.DefaultValue = method; - return this; - } + /// + public ICreateTableColumnOptionBuilder WithDefault(SystemMethods method) + { + CurrentColumn.DefaultValue = method; + return this; + } - public ICreateTableColumnOptionBuilder WithDefaultValue(object value) - { - CurrentColumn.DefaultValue = value; - return this; - } + public ICreateTableColumnOptionBuilder WithDefaultValue(object value) + { + CurrentColumn.DefaultValue = value; + return this; + } - /// - public ICreateTableColumnOptionBuilder Identity() - { - CurrentColumn.IsIdentity = true; - return this; - } + /// + public ICreateTableColumnOptionBuilder Identity() + { + CurrentColumn.IsIdentity = true; + return this; + } - /// - public ICreateTableColumnOptionBuilder Indexed() - { - return Indexed(null); - } + /// + public ICreateTableColumnOptionBuilder Indexed() => Indexed(null); - /// - public ICreateTableColumnOptionBuilder Indexed(string? indexName) - { - CurrentColumn.IsIndexed = true; + /// + public ICreateTableColumnOptionBuilder Indexed(string? indexName) + { + CurrentColumn.IsIndexed = true; - var index = new CreateIndexExpression(_context, new IndexDefinition + var index = new CreateIndexExpression(_context, + new IndexDefinition { - Name = indexName, - SchemaName = Expression.SchemaName, - TableName = Expression.TableName + Name = indexName, SchemaName = Expression.SchemaName, TableName = Expression.TableName }); - index.Index.Columns.Add(new IndexColumnDefinition - { - Name = CurrentColumn.Name - }); + index.Index.Columns.Add(new IndexColumnDefinition {Name = CurrentColumn.Name}); - Expression.Expressions.Add(index); + Expression.Expressions.Add(index); - return this; - } + return this; + } - /// - public ICreateTableColumnOptionBuilder PrimaryKey() - { - CurrentColumn.IsPrimaryKey = true; + /// + public ICreateTableColumnOptionBuilder PrimaryKey() + { + CurrentColumn.IsPrimaryKey = true; - var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey) - { - Constraint = - { - TableName = CurrentColumn.TableName, - Columns = new[] { CurrentColumn.Name } - } - }; - Expression.Expressions.Add(expression); - - return this; - } - - /// - public ICreateTableColumnOptionBuilder PrimaryKey(string primaryKeyName) + var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey) { - CurrentColumn.IsPrimaryKey = true; - CurrentColumn.PrimaryKeyName = primaryKeyName; + Constraint = {TableName = CurrentColumn.TableName, Columns = new[] {CurrentColumn.Name}} + }; + Expression.Expressions.Add(expression); - var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey) - { - Constraint = - { - ConstraintName = primaryKeyName, - TableName = CurrentColumn.TableName, - Columns = new[] { CurrentColumn.Name } - } - }; - Expression.Expressions.Add(expression); - - return this; - } - - /// - public ICreateTableColumnOptionBuilder Nullable() - { - CurrentColumn.IsNullable = true; - return this; - } + return this; + } - /// - public ICreateTableColumnOptionBuilder NotNullable() - { - CurrentColumn.IsNullable = false; - return this; - } + /// + public ICreateTableColumnOptionBuilder PrimaryKey(string primaryKeyName) + { + CurrentColumn.IsPrimaryKey = true; + CurrentColumn.PrimaryKeyName = primaryKeyName; - /// - public ICreateTableColumnOptionBuilder Unique() + var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey) { - return Unique(null); - } + Constraint = + { + ConstraintName = primaryKeyName, + TableName = CurrentColumn.TableName, + Columns = new[] {CurrentColumn.Name} + } + }; + Expression.Expressions.Add(expression); + + return this; + } - /// - public ICreateTableColumnOptionBuilder Unique(string? indexName) - { - CurrentColumn.IsUnique = true; + /// + public ICreateTableColumnOptionBuilder Nullable() + { + CurrentColumn.IsNullable = true; + return this; + } + + /// + public ICreateTableColumnOptionBuilder NotNullable() + { + CurrentColumn.IsNullable = false; + return this; + } - var index = new CreateIndexExpression(_context, new IndexDefinition + /// + public ICreateTableColumnOptionBuilder Unique() => Unique(null); + + /// + public ICreateTableColumnOptionBuilder Unique(string? indexName) + { + CurrentColumn.IsUnique = true; + + var index = new CreateIndexExpression(_context, + new IndexDefinition { Name = indexName, SchemaName = Expression.SchemaName, @@ -158,36 +141,31 @@ public ICreateTableColumnOptionBuilder Unique(string? indexName) IndexType = IndexTypes.UniqueNonClustered }); - index.Index.Columns.Add(new IndexColumnDefinition - { - Name = CurrentColumn.Name - }); + index.Index.Columns.Add(new IndexColumnDefinition {Name = CurrentColumn.Name}); - Expression.Expressions.Add(index); + Expression.Expressions.Add(index); - return this; - } + return this; + } - /// - public ICreateTableColumnOptionForeignKeyCascadeBuilder ForeignKey(string primaryTableName, string primaryColumnName) - { - return ForeignKey(null, null, primaryTableName, primaryColumnName); - } + /// + public ICreateTableColumnOptionForeignKeyCascadeBuilder ForeignKey(string primaryTableName, + string primaryColumnName) => ForeignKey(null, null, primaryTableName, primaryColumnName); - /// - public ICreateTableColumnOptionForeignKeyCascadeBuilder ForeignKey(string foreignKeyName, string primaryTableName, - string primaryColumnName) - { - return ForeignKey(foreignKeyName, null, primaryTableName, primaryColumnName); - } + /// + public ICreateTableColumnOptionForeignKeyCascadeBuilder ForeignKey(string foreignKeyName, string primaryTableName, + string primaryColumnName) => + ForeignKey(foreignKeyName, null, primaryTableName, primaryColumnName); - /// - public ICreateTableColumnOptionForeignKeyCascadeBuilder ForeignKey(string? foreignKeyName, string? primaryTableSchema, - string primaryTableName, string primaryColumnName) - { - CurrentColumn.IsForeignKey = true; + /// + public ICreateTableColumnOptionForeignKeyCascadeBuilder ForeignKey(string? foreignKeyName, + string? primaryTableSchema, + string primaryTableName, string primaryColumnName) + { + CurrentColumn.IsForeignKey = true; - var fk = new CreateForeignKeyExpression(_context, new ForeignKeyDefinition + var fk = new CreateForeignKeyExpression(_context, + new ForeignKeyDefinition { Name = foreignKeyName, PrimaryTable = primaryTableName, @@ -196,39 +174,37 @@ public ICreateTableColumnOptionForeignKeyCascadeBuilder ForeignKey(string? forei ForeignTableSchema = Expression.SchemaName }); - fk.ForeignKey.PrimaryColumns.Add(primaryColumnName); - fk.ForeignKey.ForeignColumns.Add(CurrentColumn.Name); + fk.ForeignKey.PrimaryColumns.Add(primaryColumnName); + fk.ForeignKey.ForeignColumns.Add(CurrentColumn.Name); - Expression.Expressions.Add(fk); - CurrentForeignKey = fk.ForeignKey; - return this; - } + Expression.Expressions.Add(fk); + CurrentForeignKey = fk.ForeignKey; + return this; + } - /// - public ICreateTableColumnOptionForeignKeyCascadeBuilder ForeignKey() - { - CurrentColumn.IsForeignKey = true; - return this; - } + /// + public ICreateTableColumnOptionForeignKeyCascadeBuilder ForeignKey() + { + CurrentColumn.IsForeignKey = true; + return this; + } - /// - public ICreateTableColumnOptionForeignKeyCascadeBuilder ReferencedBy(string foreignTableName, string foreignColumnName) - { - return ReferencedBy(null, null, foreignTableName, foreignColumnName); - } + /// + public ICreateTableColumnOptionForeignKeyCascadeBuilder ReferencedBy(string foreignTableName, + string foreignColumnName) => ReferencedBy(null, null, foreignTableName, foreignColumnName); - /// - public ICreateTableColumnOptionForeignKeyCascadeBuilder ReferencedBy(string foreignKeyName, string foreignTableName, - string foreignColumnName) - { - return ReferencedBy(foreignKeyName, null, foreignTableName, foreignColumnName); - } + /// + public ICreateTableColumnOptionForeignKeyCascadeBuilder ReferencedBy(string foreignKeyName, string foreignTableName, + string foreignColumnName) => + ReferencedBy(foreignKeyName, null, foreignTableName, foreignColumnName); - /// - public ICreateTableColumnOptionForeignKeyCascadeBuilder ReferencedBy(string? foreignKeyName, string? foreignTableSchema, - string foreignTableName, string foreignColumnName) - { - var fk = new CreateForeignKeyExpression(_context, new ForeignKeyDefinition + /// + public ICreateTableColumnOptionForeignKeyCascadeBuilder ReferencedBy(string? foreignKeyName, + string? foreignTableSchema, + string foreignTableName, string foreignColumnName) + { + var fk = new CreateForeignKeyExpression(_context, + new ForeignKeyDefinition { Name = foreignKeyName, PrimaryTable = Expression.TableName, @@ -237,34 +213,35 @@ public ICreateTableColumnOptionForeignKeyCascadeBuilder ReferencedBy(string? for ForeignTableSchema = foreignTableSchema }); - fk.ForeignKey.PrimaryColumns.Add(CurrentColumn.Name); - fk.ForeignKey.ForeignColumns.Add(foreignColumnName); + fk.ForeignKey.PrimaryColumns.Add(CurrentColumn.Name); + fk.ForeignKey.ForeignColumns.Add(foreignColumnName); - Expression.Expressions.Add(fk); - CurrentForeignKey = fk.ForeignKey; - return this; - } + Expression.Expressions.Add(fk); + CurrentForeignKey = fk.ForeignKey; + return this; + } - /// - public ICreateTableColumnOptionForeignKeyCascadeBuilder OnDelete(Rule rule) - { - CurrentForeignKey.OnDelete = rule; - return this; - } + /// + public ICreateTableColumnOptionForeignKeyCascadeBuilder OnDelete(Rule rule) + { + CurrentForeignKey.OnDelete = rule; + return this; + } - /// - public ICreateTableColumnOptionForeignKeyCascadeBuilder OnUpdate(Rule rule) - { - CurrentForeignKey.OnUpdate = rule; - return this; - } + /// + public ICreateTableColumnOptionForeignKeyCascadeBuilder OnUpdate(Rule rule) + { + CurrentForeignKey.OnUpdate = rule; + return this; + } - /// - public ICreateTableColumnOptionBuilder OnDeleteOrUpdate(Rule rule) - { - OnDelete(rule); - OnUpdate(rule); - return this; - } + /// + public ICreateTableColumnOptionBuilder OnDeleteOrUpdate(Rule rule) + { + OnDelete(rule); + OnUpdate(rule); + return this; } + + public override ColumnDefinition GetColumnForType() => CurrentColumn; } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableOfDtoBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableOfDtoBuilder.cs index 5aac9e90f772..f345a4cb1560 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableOfDtoBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableOfDtoBuilder.cs @@ -1,46 +1,44 @@ -using System; -using NPoco; +using NPoco; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Execute.Expressions; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; +using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table; + +public class CreateTableOfDtoBuilder : IExecutableBuilder { - public class CreateTableOfDtoBuilder : IExecutableBuilder - { - private readonly IMigrationContext _context; + private readonly IMigrationContext _context; - // TODO: This doesn't do anything. - private readonly DatabaseType[] _supportedDatabaseTypes; + // TODO: This doesn't do anything. + private readonly DatabaseType[] _supportedDatabaseTypes; - public CreateTableOfDtoBuilder(IMigrationContext context, params DatabaseType[] supportedDatabaseTypes) - { - _context = context; - _supportedDatabaseTypes = supportedDatabaseTypes; - } + public CreateTableOfDtoBuilder(IMigrationContext context, params DatabaseType[] supportedDatabaseTypes) + { + _context = context; + _supportedDatabaseTypes = supportedDatabaseTypes; + } - public Type? TypeOfDto { get; set; } + public Type? TypeOfDto { get; set; } - public bool WithoutKeysAndIndexes { get; set; } + public bool WithoutKeysAndIndexes { get; set; } - /// - public void Do() + /// + public void Do() + { + ISqlSyntaxProvider syntax = _context.SqlContext.SqlSyntax; + if (TypeOfDto is null) { - var syntax = _context.SqlContext.SqlSyntax; - if (TypeOfDto is null) - { - return; - } - var tableDefinition = DefinitionFactory.GetTableDefinition(TypeOfDto, syntax); - - syntax.HandleCreateTable(_context.Database, tableDefinition, WithoutKeysAndIndexes); - _context.BuildingExpression = false; + return; } - private void ExecuteSql(string sql) - { - new ExecuteSqlStatementExpression(_context) { SqlStatement = sql } - .Execute(); - } + TableDefinition tableDefinition = DefinitionFactory.GetTableDefinition(TypeOfDto, syntax); + + syntax.HandleCreateTable(_context.Database, tableDefinition, WithoutKeysAndIndexes); + _context.BuildingExpression = false; } + + private void ExecuteSql(string sql) => + new ExecuteSqlStatementExpression(_context) {SqlStatement = sql} + .Execute(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableColumnAsTypeBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableColumnAsTypeBuilder.cs index dfbeacde3507..22c717004f20 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableColumnAsTypeBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableColumnAsTypeBuilder.cs @@ -1,7 +1,7 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table; + +public interface ICreateTableColumnAsTypeBuilder : IColumnTypeBuilder { - public interface ICreateTableColumnAsTypeBuilder : IColumnTypeBuilder - { } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableColumnOptionBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableColumnOptionBuilder.cs index 9c3d87727726..cefb813e0a1d 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableColumnOptionBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableColumnOptionBuilder.cs @@ -1,9 +1,9 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table; + +public interface ICreateTableColumnOptionBuilder : + IColumnOptionBuilder, + ICreateTableWithColumnBuilder { - public interface ICreateTableColumnOptionBuilder : - IColumnOptionBuilder, - ICreateTableWithColumnBuilder - { } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableColumnOptionForeignKeyCascadeBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableColumnOptionForeignKeyCascadeBuilder.cs index 14d9369cfc6d..0b914ad0f6f7 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableColumnOptionForeignKeyCascadeBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableColumnOptionForeignKeyCascadeBuilder.cs @@ -1,9 +1,9 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table; + +public interface ICreateTableColumnOptionForeignKeyCascadeBuilder : + ICreateTableColumnOptionBuilder, + IForeignKeyCascadeBuilder { - public interface ICreateTableColumnOptionForeignKeyCascadeBuilder : - ICreateTableColumnOptionBuilder, - IForeignKeyCascadeBuilder - { } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableWithColumnBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableWithColumnBuilder.cs index d91340638710..2b8396b22cb2 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableWithColumnBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableWithColumnBuilder.cs @@ -1,9 +1,8 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table; + +public interface ICreateTableWithColumnBuilder : IFluentBuilder, IExecutableBuilder { - public interface ICreateTableWithColumnBuilder : IFluentBuilder, IExecutableBuilder - { - ICreateTableColumnAsTypeBuilder WithColumn(string name); - } + ICreateTableColumnAsTypeBuilder WithColumn(string name); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Column/DeleteColumnBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Column/DeleteColumnBuilder.cs index 50101f46a115..779ebf538659 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Column/DeleteColumnBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Column/DeleteColumnBuilder.cs @@ -1,27 +1,27 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Column +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Column; + +public class DeleteColumnBuilder : ExpressionBuilderBase, + IDeleteColumnBuilder { - public class DeleteColumnBuilder : ExpressionBuilderBase, - IDeleteColumnBuilder + public DeleteColumnBuilder(DeleteColumnExpression expression) + : base(expression) { - public DeleteColumnBuilder(DeleteColumnExpression expression) - : base(expression) - { } + } - /// - public IExecutableBuilder FromTable(string tableName) - { - Expression.TableName = tableName; - return new ExecutableBuilder(Expression); - } + /// + public IExecutableBuilder FromTable(string tableName) + { + Expression.TableName = tableName; + return new ExecutableBuilder(Expression); + } - /// - public IDeleteColumnBuilder Column(string columnName) - { - Expression.ColumnNames.Add(columnName); - return this; - } + /// + public IDeleteColumnBuilder Column(string columnName) + { + Expression.ColumnNames.Add(columnName); + return this; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Column/IDeleteColumnBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Column/IDeleteColumnBuilder.cs index 80755635eed5..c0a99e677b28 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Column/IDeleteColumnBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Column/IDeleteColumnBuilder.cs @@ -1,20 +1,19 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Column +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Column; + +/// +/// Builds a Delete Column expression. +/// +public interface IDeleteColumnBuilder : IFluentBuilder { /// - /// Builds a Delete Column expression. + /// Specifies the table of the column to delete. /// - public interface IDeleteColumnBuilder : IFluentBuilder - { - /// - /// Specifies the table of the column to delete. - /// - IExecutableBuilder FromTable(string tableName); + IExecutableBuilder FromTable(string tableName); - /// - /// Specifies the column to delete. - /// - IDeleteColumnBuilder Column(string columnName); - } + /// + /// Specifies the column to delete. + /// + IDeleteColumnBuilder Column(string columnName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Constraint/DeleteConstraintBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Constraint/DeleteConstraintBuilder.cs index 84e5393549a6..e98460e5d113 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Constraint/DeleteConstraintBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Constraint/DeleteConstraintBuilder.cs @@ -1,20 +1,20 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Constraint +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Constraint; + +public class DeleteConstraintBuilder : ExpressionBuilderBase, + IDeleteConstraintBuilder { - public class DeleteConstraintBuilder : ExpressionBuilderBase, - IDeleteConstraintBuilder + public DeleteConstraintBuilder(DeleteConstraintExpression expression) + : base(expression) { - public DeleteConstraintBuilder(DeleteConstraintExpression expression) - : base(expression) - { } + } - /// - public IExecutableBuilder FromTable(string tableName) - { - Expression.Constraint.TableName = tableName; - return new ExecutableBuilder(Expression); - } + /// + public IExecutableBuilder FromTable(string tableName) + { + Expression.Constraint.TableName = tableName; + return new ExecutableBuilder(Expression); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Constraint/IDeleteConstraintBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Constraint/IDeleteConstraintBuilder.cs index a3304f552d99..da038d5b24ba 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Constraint/IDeleteConstraintBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Constraint/IDeleteConstraintBuilder.cs @@ -1,15 +1,14 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Constraint +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Constraint; + +/// +/// Builds a Delete Constraint expression. +/// +public interface IDeleteConstraintBuilder : IFluentBuilder { /// - /// Builds a Delete Constraint expression. + /// Specifies the table of the constraint to delete. /// - public interface IDeleteConstraintBuilder : IFluentBuilder - { - /// - /// Specifies the table of the constraint to delete. - /// - IExecutableBuilder FromTable(string tableName); - } + IExecutableBuilder FromTable(string tableName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Data/DeleteDataBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Data/DeleteDataBuilder.cs index 77d00b29f3d4..e91352fc8aee 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Data/DeleteDataBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Data/DeleteDataBuilder.cs @@ -1,53 +1,52 @@ -using System.Collections.Generic; -using System.ComponentModel; +using System.ComponentModel; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Data +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Data; + +public class DeleteDataBuilder : ExpressionBuilderBase, + IDeleteDataBuilder { - public class DeleteDataBuilder : ExpressionBuilderBase, - IDeleteDataBuilder + public DeleteDataBuilder(DeleteDataExpression expression) + : base(expression) { - public DeleteDataBuilder(DeleteDataExpression expression) - : base(expression) - { } + } - /// - public IExecutableBuilder IsNull(string columnName) - { - Expression.Rows.Add(new DeletionDataDefinition { new KeyValuePair(columnName, null) }); - return this; - } + /// + public IExecutableBuilder IsNull(string columnName) + { + Expression.Rows.Add(new DeletionDataDefinition {new(columnName, null)}); + return this; + } - /// - public IDeleteDataBuilder Row(object dataAsAnonymousType) - { - Expression.Rows.Add(GetData(dataAsAnonymousType)); - return this; - } + /// + public IDeleteDataBuilder Row(object dataAsAnonymousType) + { + Expression.Rows.Add(GetData(dataAsAnonymousType)); + return this; + } - /// - public IExecutableBuilder AllRows() - { - Expression.IsAllRows = true; - return this; - } + /// + public IExecutableBuilder AllRows() + { + Expression.IsAllRows = true; + return this; + } - /// - public void Do() - { - Expression.Execute(); - } + /// + public void Do() => Expression.Execute(); - private static DeletionDataDefinition GetData(object dataAsAnonymousType) - { - var properties = TypeDescriptor.GetProperties(dataAsAnonymousType); + private static DeletionDataDefinition GetData(object dataAsAnonymousType) + { + PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(dataAsAnonymousType); - var data = new DeletionDataDefinition(); - foreach (PropertyDescriptor property in properties) - data.Add(new KeyValuePair(property.Name, property.GetValue(dataAsAnonymousType))); - return data; + var data = new DeletionDataDefinition(); + foreach (PropertyDescriptor property in properties) + { + data.Add(new KeyValuePair(property.Name, property.GetValue(dataAsAnonymousType))); } + + return data; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Data/IDeleteDataBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Data/IDeleteDataBuilder.cs index 701d526d7d37..ce6885464291 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Data/IDeleteDataBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Data/IDeleteDataBuilder.cs @@ -1,25 +1,24 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Data +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Data; + +/// +/// Builds a Delete expression. +/// +public interface IDeleteDataBuilder : IFluentBuilder, IExecutableBuilder { /// - /// Builds a Delete expression. + /// Specifies a row to be deleted. /// - public interface IDeleteDataBuilder : IFluentBuilder, IExecutableBuilder - { - /// - /// Specifies a row to be deleted. - /// - IDeleteDataBuilder Row(object dataAsAnonymousType); + IDeleteDataBuilder Row(object dataAsAnonymousType); - /// - /// Specifies that all rows must be deleted. - /// - IExecutableBuilder AllRows(); + /// + /// Specifies that all rows must be deleted. + /// + IExecutableBuilder AllRows(); - /// - /// Specifies that rows with a specified column being null must be deleted. - /// - IExecutableBuilder IsNull(string columnName); - } + /// + /// Specifies that rows with a specified column being null must be deleted. + /// + IExecutableBuilder IsNull(string columnName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DefaultConstraint/DeleteDefaultConstraintBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DefaultConstraint/DeleteDefaultConstraintBuilder.cs index 7093256c5f97..35e62e9e43c7 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DefaultConstraint/DeleteDefaultConstraintBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DefaultConstraint/DeleteDefaultConstraintBuilder.cs @@ -1,38 +1,37 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.DefaultConstraint +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.DefaultConstraint; + +/// +/// Implements , +/// . +/// +public class DeleteDefaultConstraintBuilder : ExpressionBuilderBase, + IDeleteDefaultConstraintOnTableBuilder, + IDeleteDefaultConstraintOnColumnBuilder { - /// - /// Implements , . - /// - public class DeleteDefaultConstraintBuilder : ExpressionBuilderBase, - IDeleteDefaultConstraintOnTableBuilder, - IDeleteDefaultConstraintOnColumnBuilder - { - private readonly IMigrationContext _context; + private readonly IMigrationContext _context; - public DeleteDefaultConstraintBuilder(IMigrationContext context, DeleteDefaultConstraintExpression expression) - : base(expression) - { - _context = context; - } + public DeleteDefaultConstraintBuilder(IMigrationContext context, DeleteDefaultConstraintExpression expression) + : base(expression) => + _context = context; - /// - public IDeleteDefaultConstraintOnColumnBuilder OnTable(string tableName) - { - Expression.TableName = tableName; - return this; - } + /// + public IExecutableBuilder OnColumn(string columnName) + { + Expression.ColumnName = columnName; + Expression.HasDefaultConstraint = _context.SqlContext.SqlSyntax.TryGetDefaultConstraint(_context.Database, + Expression.TableName, columnName, out var constraintName); + Expression.ConstraintName = constraintName ?? string.Empty; - /// - public IExecutableBuilder OnColumn(string columnName) - { - Expression.ColumnName = columnName; - Expression.HasDefaultConstraint = _context.SqlContext.SqlSyntax.TryGetDefaultConstraint(_context.Database, Expression.TableName, columnName, out var constraintName); - Expression.ConstraintName = constraintName ?? string.Empty; + return new ExecutableBuilder(Expression); + } - return new ExecutableBuilder(Expression); - } + /// + public IDeleteDefaultConstraintOnColumnBuilder OnTable(string tableName) + { + Expression.TableName = tableName; + return this; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DefaultConstraint/IDeleteDefaultConstraintOnColumnBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DefaultConstraint/IDeleteDefaultConstraintOnColumnBuilder.cs index dcc613e0fb9d..c95e4e2c4684 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DefaultConstraint/IDeleteDefaultConstraintOnColumnBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DefaultConstraint/IDeleteDefaultConstraintOnColumnBuilder.cs @@ -1,15 +1,14 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.DefaultConstraint +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.DefaultConstraint; + +/// +/// Builds a Delete expression. +/// +public interface IDeleteDefaultConstraintOnColumnBuilder : IFluentBuilder { /// - /// Builds a Delete expression. + /// Specifies the column of the constraint to delete. /// - public interface IDeleteDefaultConstraintOnColumnBuilder : IFluentBuilder - { - /// - /// Specifies the column of the constraint to delete. - /// - IExecutableBuilder OnColumn(string columnName); - } + IExecutableBuilder OnColumn(string columnName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DefaultConstraint/IDeleteDefaultConstraintOnTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DefaultConstraint/IDeleteDefaultConstraintOnTableBuilder.cs index 4b8be9f3ee58..9c7dc6812c01 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DefaultConstraint/IDeleteDefaultConstraintOnTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DefaultConstraint/IDeleteDefaultConstraintOnTableBuilder.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.DefaultConstraint +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.DefaultConstraint; + +/// +/// Builds a Delete expression. +/// +public interface IDeleteDefaultConstraintOnTableBuilder : IFluentBuilder { /// - /// Builds a Delete expression. + /// Specifies the table of the constraint to delete. /// - public interface IDeleteDefaultConstraintOnTableBuilder : IFluentBuilder - { - /// - /// Specifies the table of the constraint to delete. - /// - IDeleteDefaultConstraintOnColumnBuilder OnTable(string tableName); - } + IDeleteDefaultConstraintOnColumnBuilder OnTable(string tableName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DeleteBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DeleteBuilder.cs index c8a3ed5d28dc..caff259af435 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DeleteBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DeleteBuilder.cs @@ -1,5 +1,4 @@ -using System; -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Column; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Constraint; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Data; @@ -9,109 +8,117 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Index; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.KeysAndIndexes; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; +using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete; + +public class DeleteBuilder : IDeleteBuilder { - public class DeleteBuilder : IDeleteBuilder + private readonly IMigrationContext _context; + + public DeleteBuilder(IMigrationContext context) => _context = context; + + /// + public IExecutableBuilder Table(string tableName) { - private readonly IMigrationContext _context; + var expression = new DeleteTableExpression(_context) {TableName = tableName}; + return new ExecutableBuilder(expression); + } - public DeleteBuilder(IMigrationContext context) - { - _context = context; - } + /// + public IExecutableBuilder KeysAndIndexes(bool local = true, bool foreign = true) + { + ISqlSyntaxProvider syntax = _context.SqlContext.SqlSyntax; + TableDefinition tableDefinition = DefinitionFactory.GetTableDefinition(typeof(TDto), syntax); + return KeysAndIndexes(tableDefinition.Name, local, foreign); + } - /// - public IExecutableBuilder Table(string tableName) + /// + public IExecutableBuilder KeysAndIndexes(string? tableName, bool local = true, bool foreign = true) + { + if (tableName == null) { - var expression = new DeleteTableExpression(_context) { TableName = tableName }; - return new ExecutableBuilder(expression); + throw new ArgumentNullException(nameof(tableName)); } - /// - public IExecutableBuilder KeysAndIndexes(bool local = true, bool foreign = true) + if (string.IsNullOrWhiteSpace(tableName)) { - var syntax = _context.SqlContext.SqlSyntax; - var tableDefinition = DefinitionFactory.GetTableDefinition(typeof(TDto), syntax); - return KeysAndIndexes(tableDefinition.Name, local, foreign); + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", + nameof(tableName)); } - /// - public IExecutableBuilder KeysAndIndexes(string? tableName, bool local = true, bool foreign = true) + return new DeleteKeysAndIndexesBuilder(_context) { - if (tableName == null) throw new ArgumentNullException(nameof(tableName)); - if (string.IsNullOrWhiteSpace(tableName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(tableName)); - - return new DeleteKeysAndIndexesBuilder(_context) { TableName = tableName, DeleteLocal = local, DeleteForeign = foreign }; - } + TableName = tableName, DeleteLocal = local, DeleteForeign = foreign + }; + } - /// - public IDeleteColumnBuilder Column(string columnName) - { - var expression = new DeleteColumnExpression(_context) {ColumnNames = {columnName}}; - return new DeleteColumnBuilder(expression); - } + /// + public IDeleteColumnBuilder Column(string columnName) + { + var expression = new DeleteColumnExpression(_context) {ColumnNames = {columnName}}; + return new DeleteColumnBuilder(expression); + } - /// - public IDeleteForeignKeyFromTableBuilder ForeignKey() - { - var expression = new DeleteForeignKeyExpression(_context); - return new DeleteForeignKeyBuilder(expression); - } + /// + public IDeleteForeignKeyFromTableBuilder ForeignKey() + { + var expression = new DeleteForeignKeyExpression(_context); + return new DeleteForeignKeyBuilder(expression); + } - /// - public IDeleteForeignKeyOnTableBuilder ForeignKey(string foreignKeyName) - { - var expression = new DeleteForeignKeyExpression(_context) {ForeignKey = {Name = foreignKeyName}}; - return new DeleteForeignKeyBuilder(expression); - } + /// + public IDeleteForeignKeyOnTableBuilder ForeignKey(string foreignKeyName) + { + var expression = new DeleteForeignKeyExpression(_context) {ForeignKey = {Name = foreignKeyName}}; + return new DeleteForeignKeyBuilder(expression); + } - /// - public IDeleteDataBuilder FromTable(string tableName) - { - var expression = new DeleteDataExpression(_context) { TableName = tableName }; - return new DeleteDataBuilder(expression); - } + /// + public IDeleteDataBuilder FromTable(string tableName) + { + var expression = new DeleteDataExpression(_context) {TableName = tableName}; + return new DeleteDataBuilder(expression); + } - /// - public IDeleteIndexForTableBuilder Index() - { - var expression = new DeleteIndexExpression(_context); - return new DeleteIndexBuilder(expression); - } + /// + public IDeleteIndexForTableBuilder Index() + { + var expression = new DeleteIndexExpression(_context); + return new DeleteIndexBuilder(expression); + } - /// - public IDeleteIndexForTableBuilder Index(string indexName) - { - var expression = new DeleteIndexExpression(_context) { Index = { Name = indexName } }; - return new DeleteIndexBuilder(expression); - } + /// + public IDeleteIndexForTableBuilder Index(string indexName) + { + var expression = new DeleteIndexExpression(_context) {Index = {Name = indexName}}; + return new DeleteIndexBuilder(expression); + } - /// - public IDeleteConstraintBuilder PrimaryKey(string primaryKeyName) + /// + public IDeleteConstraintBuilder PrimaryKey(string primaryKeyName) + { + var expression = new DeleteConstraintExpression(_context, ConstraintType.PrimaryKey) { - var expression = new DeleteConstraintExpression(_context, ConstraintType.PrimaryKey) - { - Constraint = { ConstraintName = primaryKeyName } - }; - return new DeleteConstraintBuilder(expression); - } + Constraint = {ConstraintName = primaryKeyName} + }; + return new DeleteConstraintBuilder(expression); + } - /// - public IDeleteConstraintBuilder UniqueConstraint(string constraintName) + /// + public IDeleteConstraintBuilder UniqueConstraint(string constraintName) + { + var expression = new DeleteConstraintExpression(_context, ConstraintType.Unique) { - var expression = new DeleteConstraintExpression(_context, ConstraintType.Unique) - { - Constraint = { ConstraintName = constraintName } - }; - return new DeleteConstraintBuilder(expression); - } + Constraint = {ConstraintName = constraintName} + }; + return new DeleteConstraintBuilder(expression); + } - /// - public IDeleteDefaultConstraintOnTableBuilder DefaultConstraint() - { - var expression = new DeleteDefaultConstraintExpression(_context); - return new DeleteDefaultConstraintBuilder(_context, expression); - } + /// + public IDeleteDefaultConstraintOnTableBuilder DefaultConstraint() + { + var expression = new DeleteDefaultConstraintExpression(_context); + return new DeleteDefaultConstraintBuilder(_context, expression); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteColumnExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteColumnExpression.cs index 7cd93133e710..35c05968ad1f 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteColumnExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteColumnExpression.cs @@ -1,28 +1,26 @@ -using System.Collections.Generic; -using System.Text; +using System.Text; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; + +public class DeleteColumnExpression : MigrationExpressionBase { - public class DeleteColumnExpression : MigrationExpressionBase - { - public DeleteColumnExpression(IMigrationContext context) - : base(context) - { - ColumnNames = new List(); - } + public DeleteColumnExpression(IMigrationContext context) + : base(context) => + ColumnNames = new List(); - public virtual string? TableName { get; set; } - public ICollection ColumnNames { get; set; } + public virtual string? TableName { get; set; } + public ICollection ColumnNames { get; set; } - protected override string GetSql() + protected override string GetSql() + { + var stmts = new StringBuilder(); + foreach (var columnName in ColumnNames) { - var stmts = new StringBuilder(); - foreach (var columnName in ColumnNames) - { - stmts.AppendFormat(SqlSyntax.DropColumn, SqlSyntax.GetQuotedTableName(TableName), SqlSyntax.GetQuotedColumnName(columnName)); - AppendStatementSeparator(stmts); - } - return stmts.ToString(); + stmts.AppendFormat(SqlSyntax.DropColumn, SqlSyntax.GetQuotedTableName(TableName), + SqlSyntax.GetQuotedColumnName(columnName)); + AppendStatementSeparator(stmts); } + + return stmts.ToString(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteConstraintExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteConstraintExpression.cs index 73e17ba124a7..b60be78201d1 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteConstraintExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteConstraintExpression.cs @@ -1,22 +1,17 @@ using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; + +public class DeleteConstraintExpression : MigrationExpressionBase { - public class DeleteConstraintExpression : MigrationExpressionBase - { - public DeleteConstraintExpression(IMigrationContext context, ConstraintType type) - : base(context) - { - Constraint = new ConstraintDefinition(type); - } + public DeleteConstraintExpression(IMigrationContext context, ConstraintType type) + : base(context) => + Constraint = new ConstraintDefinition(type); - public ConstraintDefinition Constraint { get; } + public ConstraintDefinition Constraint { get; } - protected override string GetSql() - { - return string.Format(SqlSyntax.DeleteConstraint, - SqlSyntax.GetQuotedTableName(Constraint.TableName), - SqlSyntax.GetQuotedName(Constraint.ConstraintName)); - } - } + protected override string GetSql() => + string.Format(SqlSyntax.DeleteConstraint, + SqlSyntax.GetQuotedTableName(Constraint.TableName), + SqlSyntax.GetQuotedName(Constraint.ConstraintName)); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteDataExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteDataExpression.cs index 3d57a77dc0a5..9792b1cd4e96 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteDataExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteDataExpression.cs @@ -1,38 +1,40 @@ -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Text; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; + +public class DeleteDataExpression : MigrationExpressionBase { - public class DeleteDataExpression : MigrationExpressionBase + public DeleteDataExpression(IMigrationContext context) + : base(context) { - public DeleteDataExpression(IMigrationContext context) - : base(context) - { } + } - public string? TableName { get; set; } - public virtual bool IsAllRows { get; set; } + public string? TableName { get; set; } + public virtual bool IsAllRows { get; set; } - public List Rows { get; } = new List(); + public List Rows { get; } = new(); - protected override string GetSql() + protected override string GetSql() + { + if (IsAllRows) { - if (IsAllRows) - return string.Format(SqlSyntax.DeleteData, SqlSyntax.GetQuotedTableName(TableName), "(1=1)"); + return string.Format(SqlSyntax.DeleteData, SqlSyntax.GetQuotedTableName(TableName), "(1=1)"); + } - var stmts = new StringBuilder(); - foreach (var row in Rows) - { - var whereClauses = row.Select(kvp => $"{SqlSyntax.GetQuotedColumnName(kvp.Key)} {(kvp.Value == null ? "IS" : "=")} {GetQuotedValue(kvp.Value)}"); + var stmts = new StringBuilder(); + foreach (DeletionDataDefinition row in Rows) + { + IEnumerable whereClauses = row.Select(kvp => + $"{SqlSyntax.GetQuotedColumnName(kvp.Key)} {(kvp.Value == null ? "IS" : "=")} {GetQuotedValue(kvp.Value)}"); - stmts.Append(string.Format(SqlSyntax.DeleteData, - SqlSyntax.GetQuotedTableName(TableName), - string.Join(" AND ", whereClauses))); + stmts.Append(string.Format(SqlSyntax.DeleteData, + SqlSyntax.GetQuotedTableName(TableName), + string.Join(" AND ", whereClauses))); - AppendStatementSeparator(stmts); - } - return stmts.ToString(); + AppendStatementSeparator(stmts); } + + return stmts.ToString(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteDefaultConstraintExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteDefaultConstraintExpression.cs index e653d0f6bfd0..8e365e3a3e16 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteDefaultConstraintExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteDefaultConstraintExpression.cs @@ -1,24 +1,22 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; + +public class DeleteDefaultConstraintExpression : MigrationExpressionBase { - public class DeleteDefaultConstraintExpression : MigrationExpressionBase + public DeleteDefaultConstraintExpression(IMigrationContext context) + : base(context) { - public DeleteDefaultConstraintExpression(IMigrationContext context) - : base(context) - { } + } - public virtual string? TableName { get; set; } - public virtual string? ColumnName { get; set; } - public virtual string? ConstraintName { get; set; } - public virtual bool HasDefaultConstraint { get; set; } + public virtual string? TableName { get; set; } + public virtual string? ColumnName { get; set; } + public virtual string? ConstraintName { get; set; } + public virtual bool HasDefaultConstraint { get; set; } - protected override string GetSql() - { - return HasDefaultConstraint - ? string.Format(SqlSyntax.DeleteDefaultConstraint, - SqlSyntax.GetQuotedTableName(TableName), - SqlSyntax.GetQuotedColumnName(ColumnName), - SqlSyntax.GetQuotedName(ConstraintName)) - : string.Empty; - } - } + protected override string GetSql() => + HasDefaultConstraint + ? string.Format(SqlSyntax.DeleteDefaultConstraint, + SqlSyntax.GetQuotedTableName(TableName), + SqlSyntax.GetQuotedColumnName(ColumnName), + SqlSyntax.GetQuotedName(ConstraintName)) + : string.Empty; } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteForeignKeyExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteForeignKeyExpression.cs index b7f670006ed1..c0fcd5a9703c 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteForeignKeyExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteForeignKeyExpression.cs @@ -1,32 +1,31 @@ -using System; -using System.Linq; -using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; + +public class DeleteForeignKeyExpression : MigrationExpressionBase { - public class DeleteForeignKeyExpression : MigrationExpressionBase + public DeleteForeignKeyExpression(IMigrationContext context) + : base(context) => + ForeignKey = new ForeignKeyDefinition(); + + public ForeignKeyDefinition ForeignKey { get; set; } + + protected override string GetSql() { - public DeleteForeignKeyExpression(IMigrationContext context) - : base(context) + if (ForeignKey.ForeignTable == null) { - ForeignKey = new ForeignKeyDefinition(); + throw new ArgumentNullException( + "Table name not specified, ensure you have appended the OnTable extension. Format should be Delete.ForeignKey(KeyName).OnTable(TableName)"); } - public ForeignKeyDefinition ForeignKey { get; set; } - - protected override string GetSql() + if (string.IsNullOrEmpty(ForeignKey.Name)) { - if (ForeignKey.ForeignTable == null) - throw new ArgumentNullException("Table name not specified, ensure you have appended the OnTable extension. Format should be Delete.ForeignKey(KeyName).OnTable(TableName)"); - - if (string.IsNullOrEmpty(ForeignKey.Name)) - { - ForeignKey.Name = $"FK_{ForeignKey.ForeignTable}_{ForeignKey.PrimaryTable}_{ForeignKey.PrimaryColumns.First()}"; - } - - return string.Format(SqlSyntax.DeleteConstraint, - SqlSyntax.GetQuotedTableName(ForeignKey.ForeignTable), - SqlSyntax.GetQuotedName(ForeignKey.Name)); + ForeignKey.Name = + $"FK_{ForeignKey.ForeignTable}_{ForeignKey.PrimaryTable}_{ForeignKey.PrimaryColumns.First()}"; } + + return string.Format(SqlSyntax.DeleteConstraint, + SqlSyntax.GetQuotedTableName(ForeignKey.ForeignTable), + SqlSyntax.GetQuotedName(ForeignKey.Name)); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteIndexExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteIndexExpression.cs index dd3c41dd2c24..f77b0bbd2168 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteIndexExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteIndexExpression.cs @@ -1,28 +1,21 @@ using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; + +public class DeleteIndexExpression : MigrationExpressionBase { - public class DeleteIndexExpression : MigrationExpressionBase - { - public DeleteIndexExpression(IMigrationContext context) - : base(context) - { - Index = new IndexDefinition(); - } + public DeleteIndexExpression(IMigrationContext context) + : base(context) => + Index = new IndexDefinition(); - public DeleteIndexExpression(IMigrationContext context, IndexDefinition index) - : base(context) - { - Index = index; - } + public DeleteIndexExpression(IMigrationContext context, IndexDefinition index) + : base(context) => + Index = index; - public IndexDefinition Index { get; } + public IndexDefinition Index { get; } - protected override string GetSql() - { - return string.Format(SqlSyntax.DropIndex, - SqlSyntax.GetQuotedName(Index.Name), - SqlSyntax.GetQuotedTableName(Index.TableName)); - } - } + protected override string GetSql() => + string.Format(SqlSyntax.DropIndex, + SqlSyntax.GetQuotedName(Index.Name), + SqlSyntax.GetQuotedTableName(Index.TableName)); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteTableExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteTableExpression.cs index 75d453eb88d3..57ba6767fc8c 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteTableExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteTableExpression.cs @@ -1,17 +1,15 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; + +public class DeleteTableExpression : MigrationExpressionBase { - public class DeleteTableExpression : MigrationExpressionBase + public DeleteTableExpression(IMigrationContext context) + : base(context) { - public DeleteTableExpression(IMigrationContext context) - : base(context) - { } + } - public virtual string? TableName { get; set; } + public virtual string? TableName { get; set; } - protected override string GetSql() - { - return string.Format(SqlSyntax.DropTable, - SqlSyntax.GetQuotedTableName(TableName)); - } - } + protected override string GetSql() => + string.Format(SqlSyntax.DropTable, + SqlSyntax.GetQuotedTableName(TableName)); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/DeleteForeignKeyBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/DeleteForeignKeyBuilder.cs index 74bee1a4406f..6b84a88f1ba0 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/DeleteForeignKeyBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/DeleteForeignKeyBuilder.cs @@ -1,72 +1,77 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey; + +/// +/// Implements IDeleteForeignKey... +/// +public class DeleteForeignKeyBuilder : ExpressionBuilderBase, + IDeleteForeignKeyFromTableBuilder, + IDeleteForeignKeyForeignColumnBuilder, + IDeleteForeignKeyToTableBuilder, + IDeleteForeignKeyPrimaryColumnBuilder, + IDeleteForeignKeyOnTableBuilder { - /// - /// Implements IDeleteForeignKey... - /// - public class DeleteForeignKeyBuilder : ExpressionBuilderBase, - IDeleteForeignKeyFromTableBuilder, - IDeleteForeignKeyForeignColumnBuilder, - IDeleteForeignKeyToTableBuilder, - IDeleteForeignKeyPrimaryColumnBuilder, - IDeleteForeignKeyOnTableBuilder + public DeleteForeignKeyBuilder(DeleteForeignKeyExpression expression) + : base(expression) { - public DeleteForeignKeyBuilder(DeleteForeignKeyExpression expression) - : base(expression) - { } + } - /// - public IDeleteForeignKeyForeignColumnBuilder FromTable(string foreignTableName) - { - Expression.ForeignKey.ForeignTable = foreignTableName; - return this; - } + /// + public IDeleteForeignKeyToTableBuilder ForeignColumn(string column) + { + Expression.ForeignKey.ForeignColumns.Add(column); + return this; + } - /// - public IDeleteForeignKeyToTableBuilder ForeignColumn(string column) + /// + public IDeleteForeignKeyToTableBuilder ForeignColumns(params string[] columns) + { + foreach (var column in columns) { Expression.ForeignKey.ForeignColumns.Add(column); - return this; } - /// - public IDeleteForeignKeyToTableBuilder ForeignColumns(params string[] columns) - { - foreach (var column in columns) - Expression.ForeignKey.ForeignColumns.Add(column); + return this; + } - return this; - } + /// + public IDeleteForeignKeyForeignColumnBuilder FromTable(string foreignTableName) + { + Expression.ForeignKey.ForeignTable = foreignTableName; + return this; + } - /// - public IDeleteForeignKeyPrimaryColumnBuilder ToTable(string table) - { - Expression.ForeignKey.PrimaryTable = table; - return this; - } + /// + public IExecutableBuilder OnTable(string foreignTableName) + { + Expression.ForeignKey.ForeignTable = foreignTableName; + return new ExecutableBuilder(Expression); + } - /// - public IExecutableBuilder PrimaryColumn(string column) + /// + public IExecutableBuilder PrimaryColumn(string column) + { + Expression.ForeignKey.PrimaryColumns.Add(column); + return new ExecutableBuilder(Expression); + } + + /// + public IExecutableBuilder PrimaryColumns(params string[] columns) + { + foreach (var column in columns) { Expression.ForeignKey.PrimaryColumns.Add(column); - return new ExecutableBuilder(Expression); } - /// - public IExecutableBuilder PrimaryColumns(params string[] columns) - { - foreach (var column in columns) - Expression.ForeignKey.PrimaryColumns.Add(column); - return new ExecutableBuilder(Expression); - } + return new ExecutableBuilder(Expression); + } - /// - public IExecutableBuilder OnTable(string foreignTableName) - { - Expression.ForeignKey.ForeignTable = foreignTableName; - return new ExecutableBuilder(Expression); - } + /// + public IDeleteForeignKeyPrimaryColumnBuilder ToTable(string table) + { + Expression.ForeignKey.PrimaryTable = table; + return this; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyForeignColumnBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyForeignColumnBuilder.cs index 3b17700218d0..b53d91fc7046 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyForeignColumnBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyForeignColumnBuilder.cs @@ -1,18 +1,17 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey; + +/// +/// Builds a Delete expression. +/// +public interface IDeleteForeignKeyForeignColumnBuilder : IFluentBuilder { /// - /// Builds a Delete expression. + /// Specifies the foreign column. /// - public interface IDeleteForeignKeyForeignColumnBuilder : IFluentBuilder - { - /// - /// Specifies the foreign column. - /// - IDeleteForeignKeyToTableBuilder ForeignColumn(string column); + IDeleteForeignKeyToTableBuilder ForeignColumn(string column); - /// - /// Specifies the foreign columns. - /// - IDeleteForeignKeyToTableBuilder ForeignColumns(params string[] columns); - } + /// + /// Specifies the foreign columns. + /// + IDeleteForeignKeyToTableBuilder ForeignColumns(params string[] columns); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyFromTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyFromTableBuilder.cs index 6d422ad535fe..6b6e03c67ff2 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyFromTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyFromTableBuilder.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey; + +/// +/// Builds a Delete expression. +/// +public interface IDeleteForeignKeyFromTableBuilder : IFluentBuilder { /// - /// Builds a Delete expression. + /// Specifies the source table of the foreign key. /// - public interface IDeleteForeignKeyFromTableBuilder : IFluentBuilder - { - /// - /// Specifies the source table of the foreign key. - /// - IDeleteForeignKeyForeignColumnBuilder FromTable(string foreignTableName); - } + IDeleteForeignKeyForeignColumnBuilder FromTable(string foreignTableName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyOnTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyOnTableBuilder.cs index 19dd14f36e75..8c0f8fe083af 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyOnTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyOnTableBuilder.cs @@ -1,15 +1,14 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey; + +/// +/// Builds a Delete expression. +/// +public interface IDeleteForeignKeyOnTableBuilder : IFluentBuilder { /// - /// Builds a Delete expression. + /// Specifies the table of the foreign key. /// - public interface IDeleteForeignKeyOnTableBuilder : IFluentBuilder - { - /// - /// Specifies the table of the foreign key. - /// - IExecutableBuilder OnTable(string foreignTableName); - } + IExecutableBuilder OnTable(string foreignTableName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyPrimaryColumnBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyPrimaryColumnBuilder.cs index c44696b45d03..ba062a531f6b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyPrimaryColumnBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyPrimaryColumnBuilder.cs @@ -1,20 +1,19 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey; + +/// +/// Builds a Delete expression. +/// +public interface IDeleteForeignKeyPrimaryColumnBuilder : IFluentBuilder { /// - /// Builds a Delete expression. + /// Specifies the target primary column. /// - public interface IDeleteForeignKeyPrimaryColumnBuilder : IFluentBuilder - { - /// - /// Specifies the target primary column. - /// - IExecutableBuilder PrimaryColumn(string column); + IExecutableBuilder PrimaryColumn(string column); - /// - /// Specifies the target primary columns. - /// - IExecutableBuilder PrimaryColumns(params string[] columns); - } + /// + /// Specifies the target primary columns. + /// + IExecutableBuilder PrimaryColumns(params string[] columns); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyToTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyToTableBuilder.cs index 6588b7a18a2f..537552cadf86 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyToTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyToTableBuilder.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey; + +/// +/// Builds a Delete expression. +/// +public interface IDeleteForeignKeyToTableBuilder : IFluentBuilder { /// - /// Builds a Delete expression. + /// Specifies the target table of the foreign key. /// - public interface IDeleteForeignKeyToTableBuilder : IFluentBuilder - { - /// - /// Specifies the target table of the foreign key. - /// - IDeleteForeignKeyPrimaryColumnBuilder ToTable(string table); - } + IDeleteForeignKeyPrimaryColumnBuilder ToTable(string table); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/IDeleteBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/IDeleteBuilder.cs index 0b8da10097c2..6758a41fdf51 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/IDeleteBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/IDeleteBuilder.cs @@ -6,73 +6,72 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Index; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete; + +/// +/// Builds a Delete expression. +/// +public interface IDeleteBuilder : IFluentBuilder { /// - /// Builds a Delete expression. + /// Specifies the table to delete. /// - public interface IDeleteBuilder : IFluentBuilder - { - /// - /// Specifies the table to delete. - /// - IExecutableBuilder Table(string tableName); + IExecutableBuilder Table(string tableName); - /// - /// Builds a Delete Keys and Indexes expression, and executes. - /// - IExecutableBuilder KeysAndIndexes(bool local = true, bool foreign = true); + /// + /// Builds a Delete Keys and Indexes expression, and executes. + /// + IExecutableBuilder KeysAndIndexes(bool local = true, bool foreign = true); - /// - /// Builds a Delete Keys and Indexes expression, and executes. - /// - IExecutableBuilder KeysAndIndexes(string tableName, bool local = true, bool foreign = true); + /// + /// Builds a Delete Keys and Indexes expression, and executes. + /// + IExecutableBuilder KeysAndIndexes(string tableName, bool local = true, bool foreign = true); - /// - /// Specifies the column to delete. - /// - IDeleteColumnBuilder Column(string columnName); + /// + /// Specifies the column to delete. + /// + IDeleteColumnBuilder Column(string columnName); - /// - /// Specifies the foreign key to delete. - /// - IDeleteForeignKeyFromTableBuilder ForeignKey(); + /// + /// Specifies the foreign key to delete. + /// + IDeleteForeignKeyFromTableBuilder ForeignKey(); - /// - /// Specifies the foreign key to delete. - /// - IDeleteForeignKeyOnTableBuilder ForeignKey(string foreignKeyName); + /// + /// Specifies the foreign key to delete. + /// + IDeleteForeignKeyOnTableBuilder ForeignKey(string foreignKeyName); - /// - /// Specifies the table to delete data from. - /// - /// - /// - IDeleteDataBuilder FromTable(string tableName); + /// + /// Specifies the table to delete data from. + /// + /// + /// + IDeleteDataBuilder FromTable(string tableName); - /// - /// Specifies the index to delete. - /// - IDeleteIndexForTableBuilder Index(); + /// + /// Specifies the index to delete. + /// + IDeleteIndexForTableBuilder Index(); - /// - /// Specifies the index to delete. - /// - IDeleteIndexForTableBuilder Index(string indexName); + /// + /// Specifies the index to delete. + /// + IDeleteIndexForTableBuilder Index(string indexName); - /// - /// Specifies the primary key to delete. - /// - IDeleteConstraintBuilder PrimaryKey(string primaryKeyName); + /// + /// Specifies the primary key to delete. + /// + IDeleteConstraintBuilder PrimaryKey(string primaryKeyName); - /// - /// Specifies the unique constraint to delete. - /// - IDeleteConstraintBuilder UniqueConstraint(string constraintName); + /// + /// Specifies the unique constraint to delete. + /// + IDeleteConstraintBuilder UniqueConstraint(string constraintName); - /// - /// Specifies the default constraint to delete. - /// - IDeleteDefaultConstraintOnTableBuilder DefaultConstraint(); - } + /// + /// Specifies the default constraint to delete. + /// + IDeleteDefaultConstraintOnTableBuilder DefaultConstraint(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Index/DeleteIndexBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Index/DeleteIndexBuilder.cs index e55b1e3d8fcc..7bc89f04f358 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Index/DeleteIndexBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Index/DeleteIndexBuilder.cs @@ -1,22 +1,22 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Index +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Index; + +public class DeleteIndexBuilder : ExpressionBuilderBase, + IDeleteIndexForTableBuilder, IExecutableBuilder { - public class DeleteIndexBuilder : ExpressionBuilderBase, - IDeleteIndexForTableBuilder, IExecutableBuilder + public DeleteIndexBuilder(DeleteIndexExpression expression) + : base(expression) { - public DeleteIndexBuilder(DeleteIndexExpression expression) - : base(expression) - { } - - /// - public void Do() => Expression.Execute(); + } - public IExecutableBuilder OnTable(string tableName) - { - Expression.Index.TableName = tableName; - return this; - } + public IExecutableBuilder OnTable(string tableName) + { + Expression.Index.TableName = tableName; + return this; } + + /// + public void Do() => Expression.Execute(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Index/IDeleteIndexForTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Index/IDeleteIndexForTableBuilder.cs index f99e0d1ea01a..a6e18b640b59 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Index/IDeleteIndexForTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Index/IDeleteIndexForTableBuilder.cs @@ -1,15 +1,14 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Index +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Index; + +/// +/// Builds a Delete expression. +/// +public interface IDeleteIndexForTableBuilder : IFluentBuilder { /// - /// Builds a Delete expression. + /// Specifies the table of the index to delete. /// - public interface IDeleteIndexForTableBuilder : IFluentBuilder - { - /// - /// Specifies the table of the index to delete. - /// - IExecutableBuilder OnTable(string tableName); - } + IExecutableBuilder OnTable(string tableName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs index 90ade43d6d14..ac627a81ea37 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs @@ -1,102 +1,112 @@ -using System.Linq; using NPoco; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.KeysAndIndexes +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.KeysAndIndexes; + +/// +/// +/// Assuming we stick with the current migrations setup this will need to be altered to +/// delegate to SQL syntax provider (we can drop indexes but not PK/FK). +/// +/// +/// 1. For SQLite, rename table.
+/// 2. Create new table with expected keys.
+/// 3. Insert into new from renamed
+/// 4. Drop renamed.
+///
+/// +/// Read more SQL Features That SQLite Does Not Implement +/// +///
+public class DeleteKeysAndIndexesBuilder : IExecutableBuilder { - /// - /// - /// Assuming we stick with the current migrations setup this will need to be altered to - /// delegate to SQL syntax provider (we can drop indexes but not PK/FK). - /// - /// - /// 1. For SQLite, rename table.
- /// 2. Create new table with expected keys.
- /// 3. Insert into new from renamed
- /// 4. Drop renamed.
- ///
- /// - /// Read more SQL Features That SQLite Does Not Implement - /// - ///
- public class DeleteKeysAndIndexesBuilder : IExecutableBuilder + private readonly IMigrationContext _context; + private readonly DatabaseType[] _supportedDatabaseTypes; + + public DeleteKeysAndIndexesBuilder(IMigrationContext context, params DatabaseType[] supportedDatabaseTypes) { - private readonly IMigrationContext _context; - private readonly DatabaseType[] _supportedDatabaseTypes; + _context = context; + _supportedDatabaseTypes = supportedDatabaseTypes; + } - public DeleteKeysAndIndexesBuilder(IMigrationContext context, params DatabaseType[] supportedDatabaseTypes) - { - _context = context; - _supportedDatabaseTypes = supportedDatabaseTypes; - } + public string? TableName { get; set; } - public string? TableName { get; set; } + public bool DeleteLocal { get; set; } - public bool DeleteLocal { get; set; } + public bool DeleteForeign { get; set; } - public bool DeleteForeign { get; set; } + private IDeleteBuilder Delete => new DeleteBuilder(_context); - /// - public void Do() - { - _context.BuildingExpression = false; + /// + public void Do() + { + _context.BuildingExpression = false; - //get a list of all constraints - this will include all PK, FK and unique constraints - var tableConstraints = _context.SqlContext.SqlSyntax.GetConstraintsPerTable(_context.Database).DistinctBy(x => x.Item2).ToList(); + //get a list of all constraints - this will include all PK, FK and unique constraints + var tableConstraints = _context.SqlContext.SqlSyntax.GetConstraintsPerTable(_context.Database) + .DistinctBy(x => x.Item2).ToList(); - //get a list of defined indexes - this will include all indexes, unique indexes and unique constraint indexes - var indexes = _context.SqlContext.SqlSyntax.GetDefinedIndexesDefinitions(_context.Database).DistinctBy(x => x.IndexName).ToList(); + //get a list of defined indexes - this will include all indexes, unique indexes and unique constraint indexes + var indexes = _context.SqlContext.SqlSyntax.GetDefinedIndexesDefinitions(_context.Database) + .DistinctBy(x => x.IndexName).ToList(); - var uniqueConstraintNames = tableConstraints.Where(x => !x.Item2.InvariantStartsWith("PK_") && !x.Item2.InvariantStartsWith("FK_")).Select(x => x.Item2); - var indexNames = indexes.Select(x => x.IndexName).ToList(); + IEnumerable uniqueConstraintNames = tableConstraints + .Where(x => !x.Item2.InvariantStartsWith("PK_") && !x.Item2.InvariantStartsWith("FK_")) + .Select(x => x.Item2); + var indexNames = indexes.Select(x => x.IndexName).ToList(); - // drop keys - if (DeleteLocal || DeleteForeign) + // drop keys + if (DeleteLocal || DeleteForeign) + { + // table, constraint + + if (DeleteForeign) { - // table, constraint + //In some cases not all FK's are prefixed with "FK" :/ mostly with old upgraded databases so we need to check if it's either: + // * starts with FK OR + // * doesn't start with PK_ and doesn't exist in the list of indexes - if (DeleteForeign) + foreach (Tuple key in tableConstraints.Where(x => x.Item1 == TableName + && (x.Item2.InvariantStartsWith("FK_") || (!x.Item2.InvariantStartsWith("PK_") && + !indexNames.InvariantContains(x.Item2))))) { - //In some cases not all FK's are prefixed with "FK" :/ mostly with old upgraded databases so we need to check if it's either: - // * starts with FK OR - // * doesn't start with PK_ and doesn't exist in the list of indexes - - foreach (var key in tableConstraints.Where(x => x.Item1 == TableName - && (x.Item2.InvariantStartsWith("FK_") || (!x.Item2.InvariantStartsWith("PK_") && !indexNames.InvariantContains(x.Item2))))) - { - Delete.ForeignKey(key.Item2).OnTable(key.Item1).Do(); - } - - } - if (DeleteLocal) - { - foreach (var key in tableConstraints.Where(x => x.Item1 == TableName && x.Item2.InvariantStartsWith("PK_"))) - Delete.PrimaryKey(key.Item2).FromTable(key.Item1).Do(); - - // note: we do *not* delete the DEFAULT constraints and if we wanted to we'd have to deal with that in interesting ways - // since SQL server has a specific way to handle that, see SqlServerSyntaxProvider.GetDefaultConstraintsPerColumn + Delete.ForeignKey(key.Item2).OnTable(key.Item1).Do(); } } - // drop indexes if (DeleteLocal) { - foreach (var index in indexes.Where(x => x.TableName == TableName)) + foreach (Tuple key in tableConstraints.Where(x => + x.Item1 == TableName && x.Item2.InvariantStartsWith("PK_"))) { - //if this is a unique constraint we need to drop the constraint, else drop the index - //to figure this out, the index must be tagged as unique and it must exist in the tableConstraints - - if (index.IsUnique && uniqueConstraintNames.InvariantContains(index.IndexName)) - Delete.UniqueConstraint(index.IndexName).FromTable(index.TableName).Do(); - else - Delete.Index(index.IndexName).OnTable(index.TableName).Do(); + Delete.PrimaryKey(key.Item2).FromTable(key.Item1).Do(); } + // note: we do *not* delete the DEFAULT constraints and if we wanted to we'd have to deal with that in interesting ways + // since SQL server has a specific way to handle that, see SqlServerSyntaxProvider.GetDefaultConstraintsPerColumn } } - private IDeleteBuilder Delete => new DeleteBuilder(_context); + // drop indexes + if (DeleteLocal) + { + foreach (DbIndexDefinition index in indexes.Where(x => x.TableName == TableName)) + { + //if this is a unique constraint we need to drop the constraint, else drop the index + //to figure this out, the index must be tagged as unique and it must exist in the tableConstraints + + if (index.IsUnique && uniqueConstraintNames.InvariantContains(index.IndexName)) + { + Delete.UniqueConstraint(index.IndexName).FromTable(index.TableName).Do(); + } + else + { + Delete.Index(index.IndexName).OnTable(index.TableName).Do(); + } + } + } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Execute/ExecuteBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Execute/ExecuteBuilder.cs index f483ec640243..74d24d658d57 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Execute/ExecuteBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Execute/ExecuteBuilder.cs @@ -3,39 +3,43 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Execute.Expressions; using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Execute +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Execute; + +public class ExecuteBuilder : ExpressionBuilderBase, + IExecuteBuilder, IExecutableBuilder { - public class ExecuteBuilder : ExpressionBuilderBase, - IExecuteBuilder, IExecutableBuilder + public ExecuteBuilder(IMigrationContext context) + : base(new ExecuteSqlStatementExpression(context)) { - public ExecuteBuilder(IMigrationContext context) - : base(new ExecuteSqlStatementExpression(context)) - { } - - /// - public void Do() - { - // slightly awkward, but doing it right would mean a *lot* - // of changes for MigrationExpressionBase + } - if (Expression.SqlObject == null) - Expression.Execute(); - else - Expression.ExecuteSqlObject(); - } + /// + public void Do() + { + // slightly awkward, but doing it right would mean a *lot* + // of changes for MigrationExpressionBase - /// - public IExecutableBuilder Sql(string sqlStatement) + if (Expression.SqlObject == null) { - Expression.SqlStatement = sqlStatement; - return this; + Expression.Execute(); } - - /// - public IExecutableBuilder Sql(Sql sql) + else { - Expression.SqlObject = sql; - return this; + Expression.ExecuteSqlObject(); } } + + /// + public IExecutableBuilder Sql(string sqlStatement) + { + Expression.SqlStatement = sqlStatement; + return this; + } + + /// + public IExecutableBuilder Sql(Sql sql) + { + Expression.SqlObject = sql; + return this; + } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Execute/Expressions/ExecuteSqlStatementExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Execute/Expressions/ExecuteSqlStatementExpression.cs index 4e9186ace907..61cb70e4c9e5 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Execute/Expressions/ExecuteSqlStatementExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Execute/Expressions/ExecuteSqlStatementExpression.cs @@ -1,26 +1,20 @@ using NPoco; using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Execute.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Execute.Expressions; + +public class ExecuteSqlStatementExpression : MigrationExpressionBase { - public class ExecuteSqlStatementExpression : MigrationExpressionBase + public ExecuteSqlStatementExpression(IMigrationContext context) + : base(context) { - public ExecuteSqlStatementExpression(IMigrationContext context) - : base(context) - { } + } - public virtual string? SqlStatement { get; set; } + public virtual string? SqlStatement { get; set; } - public virtual Sql? SqlObject { get; set; } + public virtual Sql? SqlObject { get; set; } - public void ExecuteSqlObject() - { - Execute(SqlObject); - } + public void ExecuteSqlObject() => Execute(SqlObject); - protected override string? GetSql() - { - return SqlStatement; - } - } + protected override string? GetSql() => SqlStatement; } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Execute/IExecuteBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Execute/IExecuteBuilder.cs index 54a1f6a768b1..fe696411f287 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Execute/IExecuteBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Execute/IExecuteBuilder.cs @@ -2,22 +2,21 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Execute +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Execute; + +/// +/// Builds and executes an Sql statement. +/// +/// Deals with multi-statements Sql. +public interface IExecuteBuilder : IFluentBuilder { /// - /// Builds and executes an Sql statement. + /// Specifies the Sql statement to execute. /// - /// Deals with multi-statements Sql. - public interface IExecuteBuilder : IFluentBuilder - { - /// - /// Specifies the Sql statement to execute. - /// - IExecutableBuilder Sql(string sqlStatement); + IExecutableBuilder Sql(string sqlStatement); - /// - /// Specifies the Sql statement to execute. - /// - IExecutableBuilder Sql(Sql sql); - } + /// + /// Specifies the Sql statement to execute. + /// + IExecutableBuilder Sql(Sql sql); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/ExpressionBuilderBase.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/ExpressionBuilderBase.cs index a3bed8b5d895..d64ae160a38f 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/ExpressionBuilderBase.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/ExpressionBuilderBase.cs @@ -1,22 +1,18 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions; + +/// +/// Provides a base class for expression builders. +/// +public abstract class ExpressionBuilderBase + where TExpression : IMigrationExpression { /// - /// Provides a base class for expression builders. + /// Initializes a new instance of the class. /// - public abstract class ExpressionBuilderBase - where TExpression : IMigrationExpression - { - /// - /// Initializes a new instance of the class. - /// - protected ExpressionBuilderBase(TExpression expression) - { - Expression = expression; - } + protected ExpressionBuilderBase(TExpression expression) => Expression = expression; - /// - /// Gets the expression. - /// - public TExpression Expression { get; } - } + /// + /// Gets the expression. + /// + public TExpression Expression { get; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/ExpressionBuilderBaseOfNext.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/ExpressionBuilderBaseOfNext.cs index 74f1676defa4..39fa97284d07 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/ExpressionBuilderBaseOfNext.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/ExpressionBuilderBaseOfNext.cs @@ -1,284 +1,283 @@ using System.Data; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions; + +/// +/// Provides a base class for expression builders. +/// +public abstract class ExpressionBuilderBase : ExpressionBuilderBase + where TExpression : IMigrationExpression + where TNext : IFluentBuilder { /// - /// Provides a base class for expression builders. + /// Initializes a new instance of the class. /// - public abstract class ExpressionBuilderBase : ExpressionBuilderBase - where TExpression : IMigrationExpression - where TNext : IFluentBuilder + protected ExpressionBuilderBase(TExpression expression) + : base(expression) { - /// - /// Initializes a new instance of the class. - /// - protected ExpressionBuilderBase(TExpression expression) - : base(expression) - { - } + } - public abstract ColumnDefinition? GetColumnForType(); + private ColumnDefinition? Column => GetColumnForType(); - private ColumnDefinition? Column => GetColumnForType(); + public abstract ColumnDefinition? GetColumnForType(); - public TNext AsAnsiString() + public TNext AsAnsiString() + { + if (Column is not null) { - if (Column is not null) - { - Column.Type = DbType.AnsiString; - } - - return (TNext)(object)this; + Column.Type = DbType.AnsiString; } - public TNext AsAnsiString(int size) - { - if (Column is not null) - { - Column.Type = DbType.AnsiString; - Column.Size = size; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsAnsiString(int size) + { + if (Column is not null) + { + Column.Type = DbType.AnsiString; + Column.Size = size; } - public TNext AsBinary() - { - if (Column is not null) - { - Column.Type = DbType.Binary; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsBinary() + { + if (Column is not null) + { + Column.Type = DbType.Binary; } - public TNext AsBinary(int size) - { - if (Column is not null) - { - Column.Type = DbType.Binary; - Column.Size = size; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsBinary(int size) + { + if (Column is not null) + { + Column.Type = DbType.Binary; + Column.Size = size; } - public TNext AsBoolean() - { - if (Column is not null) - { - Column.Type = DbType.Boolean; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsBoolean() + { + if (Column is not null) + { + Column.Type = DbType.Boolean; } - public TNext AsByte() - { - if (Column is not null) - { - Column.Type = DbType.Byte; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsByte() + { + if (Column is not null) + { + Column.Type = DbType.Byte; } - public TNext AsCurrency() - { - if (Column is not null) - { - Column.Type = DbType.Currency; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsCurrency() + { + if (Column is not null) + { + Column.Type = DbType.Currency; } - public TNext AsDate() - { - if (Column is not null) - { - Column.Type = DbType.Date; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsDate() + { + if (Column is not null) + { + Column.Type = DbType.Date; } - public TNext AsDateTime() - { - if (Column is not null) - { - Column.Type = DbType.DateTime; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsDateTime() + { + if (Column is not null) + { + Column.Type = DbType.DateTime; } - public TNext AsDecimal() - { - if (Column is not null) - { - Column.Type = DbType.Decimal; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsDecimal() + { + if (Column is not null) + { + Column.Type = DbType.Decimal; } - public TNext AsDecimal(int size, int precision) + return (TNext)(object)this; + } + + public TNext AsDecimal(int size, int precision) + { + if (Column is not null) { - if (Column is not null) - { - Column.Type = DbType.Decimal; - Column.Size = size; - Column.Precision = precision; - } - - return (TNext)(object)this; + Column.Type = DbType.Decimal; + Column.Size = size; + Column.Precision = precision; } - public TNext AsDouble() - { - if (Column is not null) - { - Column.Type = DbType.Double; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsDouble() + { + if (Column is not null) + { + Column.Type = DbType.Double; } - public TNext AsFixedLengthString(int size) - { - if (Column is not null) - { - Column.Type = DbType.StringFixedLength; - Column.Size = size; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsFixedLengthString(int size) + { + if (Column is not null) + { + Column.Type = DbType.StringFixedLength; + Column.Size = size; } - public TNext AsFixedLengthAnsiString(int size) - { - if (Column is not null) - { - Column.Type = DbType.AnsiStringFixedLength; - Column.Size = size; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsFixedLengthAnsiString(int size) + { + if (Column is not null) + { + Column.Type = DbType.AnsiStringFixedLength; + Column.Size = size; } - public TNext AsFloat() - { - if (Column is not null) - { - Column.Type = DbType.Single; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsFloat() + { + if (Column is not null) + { + Column.Type = DbType.Single; } - public TNext AsGuid() - { - if (Column is not null) - { - Column.Type = DbType.Guid; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsGuid() + { + if (Column is not null) + { + Column.Type = DbType.Guid; } - public TNext AsInt16() - { - if (Column is not null) - { - Column.Type = DbType.Int16; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsInt16() + { + if (Column is not null) + { + Column.Type = DbType.Int16; } - public TNext AsInt32() - { - if (Column is not null) - { - Column.Type = DbType.Int32; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsInt32() + { + if (Column is not null) + { + Column.Type = DbType.Int32; } - public TNext AsInt64() - { - if (Column is not null) - { - Column.Type = DbType.Int64; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsInt64() + { + if (Column is not null) + { + Column.Type = DbType.Int64; } - public TNext AsString() - { - if (Column is not null) - { - Column.Type = DbType.String; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsString() + { + if (Column is not null) + { + Column.Type = DbType.String; } - public TNext AsString(int size) - { - if (Column is not null) - { - Column.Type = DbType.String; - Column.Size = size; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsString(int size) + { + if (Column is not null) + { + Column.Type = DbType.String; + Column.Size = size; } - public TNext AsTime() - { - if (Column is not null) - { - Column.Type = DbType.Time; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsTime() + { + if (Column is not null) + { + Column.Type = DbType.Time; } - public TNext AsXml() - { - if (Column is not null) - { - Column.Type = DbType.Xml; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsXml() + { + if (Column is not null) + { + Column.Type = DbType.Xml; } - public TNext AsXml(int size) - { - if (Column is not null) - { - Column.Type = DbType.Xml; - Column.Size = size; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsXml(int size) + { + if (Column is not null) + { + Column.Type = DbType.Xml; + Column.Size = size; } - public TNext AsCustom(string customType) - { - if (Column is not null) - { - Column.Type = null; - Column.CustomType = customType; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsCustom(string customType) + { + if (Column is not null) + { + Column.Type = null; + Column.CustomType = customType; } + + return (TNext)(object)this; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/IFluentBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/IFluentBuilder.cs index 8ad08b57330e..adb4a379ae17 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/IFluentBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/IFluentBuilder.cs @@ -1,5 +1,5 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions; + +public interface IFluentBuilder { - public interface IFluentBuilder - { } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/Expressions/InsertDataExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/Expressions/InsertDataExpression.cs index 75664b701e33..47fdb436f6d2 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/Expressions/InsertDataExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/Expressions/InsertDataExpression.cs @@ -1,68 +1,68 @@ -using System.Collections.Generic; -using System.Text; +using System.Text; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert.Expressions; + +public class InsertDataExpression : MigrationExpressionBase { - public class InsertDataExpression : MigrationExpressionBase + public InsertDataExpression(IMigrationContext context) + : base(context) { - public InsertDataExpression(IMigrationContext context) - : base(context) - { } + } - public string? TableName { get; set; } - public bool EnabledIdentityInsert { get; set; } + public string? TableName { get; set; } + public bool EnabledIdentityInsert { get; set; } - public List Rows { get; } = new List(); + public List Rows { get; } = new(); - protected override string GetSql() - { - var stmts = new StringBuilder(); + protected override string GetSql() + { + var stmts = new StringBuilder(); - if (EnabledIdentityInsert && SqlSyntax.SupportsIdentityInsert()) - { - stmts.AppendLine($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(TableName)} ON"); - AppendStatementSeparator(stmts); - } + if (EnabledIdentityInsert && SqlSyntax.SupportsIdentityInsert()) + { + stmts.AppendLine($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(TableName)} ON"); + AppendStatementSeparator(stmts); + } - try + try + { + foreach (InsertionDataDefinition item in Rows) { - foreach (var item in Rows) + var cols = new StringBuilder(); + var vals = new StringBuilder(); + var first = true; + foreach (KeyValuePair keyVal in item) { - var cols = new StringBuilder(); - var vals = new StringBuilder(); - var first = true; - foreach (var keyVal in item) + if (first) { - if (first) - { - first = false; - } - else - { - cols.Append(","); - vals.Append(","); - } - cols.Append(SqlSyntax.GetQuotedColumnName(keyVal.Key)); - vals.Append(GetQuotedValue(keyVal.Value)); + first = false; + } + else + { + cols.Append(","); + vals.Append(","); } - var sql = string.Format(SqlSyntax.InsertData, SqlSyntax.GetQuotedTableName(TableName), cols, vals); - - stmts.Append(sql); - AppendStatementSeparator(stmts); + cols.Append(SqlSyntax.GetQuotedColumnName(keyVal.Key)); + vals.Append(GetQuotedValue(keyVal.Value)); } + + var sql = string.Format(SqlSyntax.InsertData, SqlSyntax.GetQuotedTableName(TableName), cols, vals); + + stmts.Append(sql); + AppendStatementSeparator(stmts); } - finally + } + finally + { + if (EnabledIdentityInsert && SqlSyntax.SupportsIdentityInsert()) { - if (EnabledIdentityInsert && SqlSyntax.SupportsIdentityInsert()) - { - stmts.AppendLine($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(TableName)} OFF"); - AppendStatementSeparator(stmts); - } + stmts.AppendLine($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(TableName)} OFF"); + AppendStatementSeparator(stmts); } - - return stmts.ToString(); } + + return stmts.ToString(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/IInsertBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/IInsertBuilder.cs index 407a7a02f15a..42c5efa3d053 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/IInsertBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/IInsertBuilder.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert; + +/// +/// Builds an Insert expression. +/// +public interface IInsertBuilder : IFluentBuilder { /// - /// Builds an Insert expression. + /// Specifies the table to insert into. /// - public interface IInsertBuilder : IFluentBuilder - { - /// - /// Specifies the table to insert into. - /// - IInsertIntoBuilder IntoTable(string tableName); - } + IInsertIntoBuilder IntoTable(string tableName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/IInsertIntoBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/IInsertIntoBuilder.cs index dfe4ba7909ba..7fd907181cd8 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/IInsertIntoBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/IInsertIntoBuilder.cs @@ -1,20 +1,19 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert; + +/// +/// Builds an Insert Into expression. +/// +public interface IInsertIntoBuilder : IFluentBuilder, IExecutableBuilder { /// - /// Builds an Insert Into expression. + /// Enables identity insert. /// - public interface IInsertIntoBuilder : IFluentBuilder, IExecutableBuilder - { - /// - /// Enables identity insert. - /// - IInsertIntoBuilder EnableIdentityInsert(); + IInsertIntoBuilder EnableIdentityInsert(); - /// - /// Specifies a row to be inserted. - /// - IInsertIntoBuilder Row(object dataAsAnonymousType); - } + /// + /// Specifies a row to be inserted. + /// + IInsertIntoBuilder Row(object dataAsAnonymousType); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/InsertBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/InsertBuilder.cs index ddae2d532543..78cc503324d3 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/InsertBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/InsertBuilder.cs @@ -1,24 +1,20 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert; + +/// +/// Implements . +/// +public class InsertBuilder : IInsertBuilder { - /// - /// Implements . - /// - public class InsertBuilder : IInsertBuilder - { - private readonly IMigrationContext _context; + private readonly IMigrationContext _context; - public InsertBuilder(IMigrationContext context) - { - _context = context; - } + public InsertBuilder(IMigrationContext context) => _context = context; - /// - public IInsertIntoBuilder IntoTable(string tableName) - { - var expression = new InsertDataExpression(_context) { TableName = tableName }; - return new InsertIntoBuilder(expression); - } + /// + public IInsertIntoBuilder IntoTable(string tableName) + { + var expression = new InsertDataExpression(_context) {TableName = tableName}; + return new InsertIntoBuilder(expression); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/InsertIntoBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/InsertIntoBuilder.cs index 8d2787723069..56b3b451364e 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/InsertIntoBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/InsertIntoBuilder.cs @@ -1,45 +1,47 @@ -using System.Collections.Generic; -using System.ComponentModel; +using System.ComponentModel; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert.Expressions; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert; + +/// +/// Implements . +/// +public class InsertIntoBuilder : ExpressionBuilderBase, + IInsertIntoBuilder { - /// - /// Implements . - /// - public class InsertIntoBuilder : ExpressionBuilderBase, - IInsertIntoBuilder + public InsertIntoBuilder(InsertDataExpression expression) + : base(expression) { - public InsertIntoBuilder(InsertDataExpression expression) - : base(expression) - { } + } - /// - public void Do() => Expression.Execute(); + /// + public void Do() => Expression.Execute(); - /// - public IInsertIntoBuilder EnableIdentityInsert() - { - Expression.EnabledIdentityInsert = true; - return this; - } + /// + public IInsertIntoBuilder EnableIdentityInsert() + { + Expression.EnabledIdentityInsert = true; + return this; + } - /// - public IInsertIntoBuilder Row(object dataAsAnonymousType) - { - Expression.Rows.Add(GetData(dataAsAnonymousType)); - return this; - } + /// + public IInsertIntoBuilder Row(object dataAsAnonymousType) + { + Expression.Rows.Add(GetData(dataAsAnonymousType)); + return this; + } - private static InsertionDataDefinition GetData(object dataAsAnonymousType) - { - var properties = TypeDescriptor.GetProperties(dataAsAnonymousType); + private static InsertionDataDefinition GetData(object dataAsAnonymousType) + { + PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(dataAsAnonymousType); - var data = new InsertionDataDefinition(); - foreach (PropertyDescriptor property in properties) - data.Add(new KeyValuePair(property.Name, property.GetValue(dataAsAnonymousType))); - return data; + var data = new InsertionDataDefinition(); + foreach (PropertyDescriptor property in properties) + { + data.Add(new KeyValuePair(property.Name, property.GetValue(dataAsAnonymousType))); } + + return data; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Column/IRenameColumnBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Column/IRenameColumnBuilder.cs index 76a3c069466d..9a57c7037e09 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Column/IRenameColumnBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Column/IRenameColumnBuilder.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Column +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Column; + +/// +/// Builds a Rename Column expression. +/// +public interface IRenameColumnBuilder : IFluentBuilder { /// - /// Builds a Rename Column expression. + /// Specifies the table name. /// - public interface IRenameColumnBuilder : IFluentBuilder - { - /// - /// Specifies the table name. - /// - IRenameColumnToBuilder OnTable(string tableName); - } + IRenameColumnToBuilder OnTable(string tableName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Column/IRenameColumnToBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Column/IRenameColumnToBuilder.cs index 5580226c1f72..705cddffdd0f 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Column/IRenameColumnToBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Column/IRenameColumnToBuilder.cs @@ -1,15 +1,14 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Column +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Column; + +/// +/// Builds a Rename Column expression. +/// +public interface IRenameColumnToBuilder : IFluentBuilder { /// - /// Builds a Rename Column expression. + /// Specifies the new name of the column. /// - public interface IRenameColumnToBuilder : IFluentBuilder - { - /// - /// Specifies the new name of the column. - /// - IExecutableBuilder To(string name); - } + IExecutableBuilder To(string name); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Column/RenameColumnBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Column/RenameColumnBuilder.cs index a3a181c5df92..bd3c1e85cebb 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Column/RenameColumnBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Column/RenameColumnBuilder.cs @@ -1,30 +1,30 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Column +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Column; + +public class RenameColumnBuilder : ExpressionBuilderBase, + IRenameColumnToBuilder, IRenameColumnBuilder, IExecutableBuilder { - public class RenameColumnBuilder : ExpressionBuilderBase, - IRenameColumnToBuilder, IRenameColumnBuilder, IExecutableBuilder + public RenameColumnBuilder(RenameColumnExpression expression) + : base(expression) { - public RenameColumnBuilder(RenameColumnExpression expression) - : base(expression) - { } + } - /// - public void Do() => Expression.Execute(); + /// + public void Do() => Expression.Execute(); - /// - public IExecutableBuilder To(string name) - { - Expression.NewName = name; - return this; - } + /// + public IRenameColumnToBuilder OnTable(string tableName) + { + Expression.TableName = tableName; + return this; + } - /// - public IRenameColumnToBuilder OnTable(string tableName) - { - Expression.TableName = tableName; - return this; - } + /// + public IExecutableBuilder To(string name) + { + Expression.NewName = name; + return this; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Expressions/RenameColumnExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Expressions/RenameColumnExpression.cs index cafbc4510813..f47068b38759 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Expressions/RenameColumnExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Expressions/RenameColumnExpression.cs @@ -1,19 +1,16 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Expressions; + +public class RenameColumnExpression : MigrationExpressionBase { - public class RenameColumnExpression : MigrationExpressionBase + public RenameColumnExpression(IMigrationContext context) + : base(context) { - public RenameColumnExpression(IMigrationContext context) - : base(context) - { } + } - public virtual string? TableName { get; set; } - public virtual string? OldName { get; set; } - public virtual string? NewName { get; set; } + public virtual string? TableName { get; set; } + public virtual string? OldName { get; set; } + public virtual string? NewName { get; set; } - /// - protected override string GetSql() - { - return SqlSyntax.FormatColumnRename(TableName, OldName, NewName); - } - } + /// + protected override string GetSql() => SqlSyntax.FormatColumnRename(TableName, OldName, NewName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Expressions/RenameTableExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Expressions/RenameTableExpression.cs index 77f9de03b355..6c12d88dd98d 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Expressions/RenameTableExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Expressions/RenameTableExpression.cs @@ -1,29 +1,26 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Expressions; + +/// +/// Represents a Rename Table expression. +/// +public class RenameTableExpression : MigrationExpressionBase { - /// - /// Represents a Rename Table expression. - /// - public class RenameTableExpression : MigrationExpressionBase + public RenameTableExpression(IMigrationContext context) + : base(context) { - public RenameTableExpression(IMigrationContext context) - : base(context) - { } + } - /// - /// Gets or sets the source name. - /// - public virtual string? OldName { get; set; } + /// + /// Gets or sets the source name. + /// + public virtual string? OldName { get; set; } - /// - /// Gets or sets the target name. - /// - public virtual string? NewName { get; set; } + /// + /// Gets or sets the target name. + /// + public virtual string? NewName { get; set; } - /// - /// - protected override string GetSql() - { - return SqlSyntax.FormatTableRename(OldName, NewName); - } - } + /// + /// + protected override string GetSql() => SqlSyntax.FormatTableRename(OldName, NewName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/IRenameBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/IRenameBuilder.cs index e93842ae2a07..30e7ccdb776a 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/IRenameBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/IRenameBuilder.cs @@ -1,21 +1,20 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Column; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Table; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename; + +/// +/// Builds a Rename expression. +/// +public interface IRenameBuilder : IFluentBuilder { /// - /// Builds a Rename expression. + /// Specifies the table to rename. /// - public interface IRenameBuilder : IFluentBuilder - { - /// - /// Specifies the table to rename. - /// - IRenameTableBuilder Table(string oldName); + IRenameTableBuilder Table(string oldName); - /// - /// Specifies the column to rename. - /// - IRenameColumnBuilder Column(string oldName); - } + /// + /// Specifies the column to rename. + /// + IRenameColumnBuilder Column(string oldName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/RenameBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/RenameBuilder.cs index c0b80f34bb11..7bd40b53c333 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/RenameBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/RenameBuilder.cs @@ -2,29 +2,25 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Expressions; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Table; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename; + +public class RenameBuilder : IRenameBuilder { - public class RenameBuilder : IRenameBuilder - { - private readonly IMigrationContext _context; + private readonly IMigrationContext _context; - public RenameBuilder(IMigrationContext context) - { - _context = context; - } + public RenameBuilder(IMigrationContext context) => _context = context; - /// - public IRenameTableBuilder Table(string oldName) - { - var expression = new RenameTableExpression(_context) { OldName = oldName }; - return new RenameTableBuilder(expression); - } + /// + public IRenameTableBuilder Table(string oldName) + { + var expression = new RenameTableExpression(_context) {OldName = oldName}; + return new RenameTableBuilder(expression); + } - /// - public IRenameColumnBuilder Column(string oldName) - { - var expression = new RenameColumnExpression(_context) { OldName = oldName }; - return new RenameColumnBuilder(expression); - } + /// + public IRenameColumnBuilder Column(string oldName) + { + var expression = new RenameColumnExpression(_context) {OldName = oldName}; + return new RenameColumnBuilder(expression); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Table/IRenameTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Table/IRenameTableBuilder.cs index 53f25a1b41c7..df7c05c02bb3 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Table/IRenameTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Table/IRenameTableBuilder.cs @@ -1,15 +1,14 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Table; + +/// +/// Builds a Rename Table expression. +/// +public interface IRenameTableBuilder : IFluentBuilder { /// - /// Builds a Rename Table expression. + /// Specifies the new name of the table. /// - public interface IRenameTableBuilder : IFluentBuilder - { - /// - /// Specifies the new name of the table. - /// - IExecutableBuilder To(string name); - } + IExecutableBuilder To(string name); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Table/RenameTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Table/RenameTableBuilder.cs index af849b25d772..5ae387ff86a3 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Table/RenameTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Table/RenameTableBuilder.cs @@ -1,23 +1,23 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Table; + +public class RenameTableBuilder : ExpressionBuilderBase, + IRenameTableBuilder, IExecutableBuilder { - public class RenameTableBuilder : ExpressionBuilderBase, - IRenameTableBuilder, IExecutableBuilder + public RenameTableBuilder(RenameTableExpression expression) + : base(expression) { - public RenameTableBuilder(RenameTableExpression expression) - : base(expression) - { } + } - /// - public void Do() => Expression.Execute(); + /// + public void Do() => Expression.Execute(); - /// - public IExecutableBuilder To(string name) - { - Expression.NewName = name; - return this; - } + /// + public IExecutableBuilder To(string name) + { + Expression.NewName = name; + return this; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Update/Expressions/UpdateDataExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Update/Expressions/UpdateDataExpression.cs index 62b6f0acfbad..9560b190404f 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Update/Expressions/UpdateDataExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Update/Expressions/UpdateDataExpression.cs @@ -1,36 +1,34 @@ -using System; -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Update.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Update.Expressions +public class UpdateDataExpression : MigrationExpressionBase { - public class UpdateDataExpression : MigrationExpressionBase + public UpdateDataExpression(IMigrationContext context) + : base(context) { - public UpdateDataExpression(IMigrationContext context) - : base(context) - { } + } - public string? TableName { get; set; } + public string? TableName { get; set; } - public List>? Set { get; set; } - public List>? Where { get; set; } - public bool IsAllRows { get; set; } + public List>? Set { get; set; } + public List>? Where { get; set; } + public bool IsAllRows { get; set; } - protected override string GetSql() - { - var updateItems = Set?.Select(x => $"{SqlSyntax.GetQuotedColumnName(x.Key)} = {GetQuotedValue(x.Value)}"); - var whereClauses = IsAllRows - ? null - : Where?.Select(x => $"{SqlSyntax.GetQuotedColumnName(x.Key)} {(x.Value == null ? "IS" : "=")} {GetQuotedValue(x.Value)}"); + protected override string GetSql() + { + IEnumerable? updateItems = + Set?.Select(x => $"{SqlSyntax.GetQuotedColumnName(x.Key)} = {GetQuotedValue(x.Value)}"); + IEnumerable? whereClauses = IsAllRows + ? null + : Where?.Select(x => + $"{SqlSyntax.GetQuotedColumnName(x.Key)} {(x.Value == null ? "IS" : "=")} {GetQuotedValue(x.Value)}"); - var whereClause = whereClauses == null - ? "(1=1)" - : string.Join(" AND ", whereClauses.ToArray()); + var whereClause = whereClauses == null + ? "(1=1)" + : string.Join(" AND ", whereClauses.ToArray()); - return string.Format(SqlSyntax.UpdateData, - SqlSyntax.GetQuotedTableName(TableName), - string.Join(", ", updateItems ?? Array.Empty()), - whereClause); - } + return string.Format(SqlSyntax.UpdateData, + SqlSyntax.GetQuotedTableName(TableName), + string.Join(", ", updateItems ?? Array.Empty()), + whereClause); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Update/IUpdateBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Update/IUpdateBuilder.cs index 16b1badf4890..141cb7c2a470 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Update/IUpdateBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Update/IUpdateBuilder.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Update +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Update; + +/// +/// Builds an Update expression. +/// +public interface IUpdateBuilder : IFluentBuilder { /// - /// Builds an Update expression. + /// Specifies the table to update. /// - public interface IUpdateBuilder : IFluentBuilder - { - /// - /// Specifies the table to update. - /// - IUpdateTableBuilder Table(string tableName); - } + IUpdateTableBuilder Table(string tableName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Update/IUpdateTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Update/IUpdateTableBuilder.cs index abd5201cc546..21c86fa3a691 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Update/IUpdateTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Update/IUpdateTableBuilder.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Update +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Update; + +/// +/// Builds an Update expression. +/// +public interface IUpdateTableBuilder { /// - /// Builds an Update expression. + /// Specifies the data. /// - public interface IUpdateTableBuilder - { - /// - /// Specifies the data. - /// - IUpdateWhereBuilder Set(object dataAsAnonymousType); - } + IUpdateWhereBuilder Set(object dataAsAnonymousType); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Update/IUpdateWhereBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Update/IUpdateWhereBuilder.cs index 378830cf0f18..9f346307f757 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Update/IUpdateWhereBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Update/IUpdateWhereBuilder.cs @@ -1,20 +1,19 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Update +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Update; + +/// +/// Builds an Update expression. +/// +public interface IUpdateWhereBuilder { /// - /// Builds an Update expression. + /// Specifies rows to update. /// - public interface IUpdateWhereBuilder - { - /// - /// Specifies rows to update. - /// - IExecutableBuilder Where(object dataAsAnonymousType); + IExecutableBuilder Where(object dataAsAnonymousType); - /// - /// Specifies that all rows must be updated. - /// - IExecutableBuilder AllRows(); - } + /// + /// Specifies that all rows must be updated. + /// + IExecutableBuilder AllRows(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Update/UpdateBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Update/UpdateBuilder.cs index e47e31168af7..514cde3014eb 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Update/UpdateBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Update/UpdateBuilder.cs @@ -1,21 +1,17 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Update.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Update +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Update; + +public class UpdateBuilder : IUpdateBuilder { - public class UpdateBuilder : IUpdateBuilder - { - private readonly IMigrationContext _context; + private readonly IMigrationContext _context; - public UpdateBuilder(IMigrationContext context) - { - _context = context; - } + public UpdateBuilder(IMigrationContext context) => _context = context; - /// - public IUpdateTableBuilder Table(string tableName) - { - var expression = new UpdateDataExpression(_context) { TableName = tableName }; - return new UpdateDataBuilder(expression); - } + /// + public IUpdateTableBuilder Table(string tableName) + { + var expression = new UpdateDataExpression(_context) {TableName = tableName}; + return new UpdateDataBuilder(expression); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Update/UpdateDataBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Update/UpdateDataBuilder.cs index fc7608f148c7..cd556b1a2b6e 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Update/UpdateDataBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Update/UpdateDataBuilder.cs @@ -1,49 +1,51 @@ -using System.Collections.Generic; -using System.ComponentModel; +using System.ComponentModel; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Update.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Update +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Update; + +public class UpdateDataBuilder : ExpressionBuilderBase, + IUpdateTableBuilder, IUpdateWhereBuilder, IExecutableBuilder { - public class UpdateDataBuilder : ExpressionBuilderBase, - IUpdateTableBuilder, IUpdateWhereBuilder, IExecutableBuilder + public UpdateDataBuilder(UpdateDataExpression expression) + : base(expression) { - public UpdateDataBuilder(UpdateDataExpression expression) - : base(expression) - { } + } - /// - public void Do() => Expression.Execute(); + /// + public void Do() => Expression.Execute(); - /// - public IUpdateWhereBuilder Set(object dataAsAnonymousType) - { - Expression.Set = GetData(dataAsAnonymousType); - return this; - } + /// + public IUpdateWhereBuilder Set(object dataAsAnonymousType) + { + Expression.Set = GetData(dataAsAnonymousType); + return this; + } - /// - public IExecutableBuilder Where(object dataAsAnonymousType) - { - Expression.Where = GetData(dataAsAnonymousType); - return this; - } + /// + public IExecutableBuilder Where(object dataAsAnonymousType) + { + Expression.Where = GetData(dataAsAnonymousType); + return this; + } - /// - public IExecutableBuilder AllRows() - { - Expression.IsAllRows = true; - return this; - } + /// + public IExecutableBuilder AllRows() + { + Expression.IsAllRows = true; + return this; + } - private static List> GetData(object dataAsAnonymousType) - { - var properties = TypeDescriptor.GetProperties(dataAsAnonymousType); + private static List> GetData(object dataAsAnonymousType) + { + PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(dataAsAnonymousType); - var data = new List>(); - foreach (PropertyDescriptor property in properties) - data.Add(new KeyValuePair(property.Name, property.GetValue(dataAsAnonymousType))); - return data; + var data = new List>(); + foreach (PropertyDescriptor property in properties) + { + data.Add(new KeyValuePair(property.Name, property.GetValue(dataAsAnonymousType))); } + + return data; } } diff --git a/src/Umbraco.Infrastructure/Migrations/IMigrationBuilder.cs b/src/Umbraco.Infrastructure/Migrations/IMigrationBuilder.cs index 087e04d41af0..078bfcaf387b 100644 --- a/src/Umbraco.Infrastructure/Migrations/IMigrationBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/IMigrationBuilder.cs @@ -1,9 +1,6 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Migrations; -namespace Umbraco.Cms.Infrastructure.Migrations +public interface IMigrationBuilder { - public interface IMigrationBuilder - { - MigrationBase Build(Type migrationType, IMigrationContext context); - } + MigrationBase Build(Type migrationType, IMigrationContext context); } diff --git a/src/Umbraco.Infrastructure/Migrations/IMigrationContext.cs b/src/Umbraco.Infrastructure/Migrations/IMigrationContext.cs index 9b164d38a327..d4191e4f4599 100644 --- a/src/Umbraco.Infrastructure/Migrations/IMigrationContext.cs +++ b/src/Umbraco.Infrastructure/Migrations/IMigrationContext.cs @@ -1,47 +1,46 @@ using Microsoft.Extensions.Logging; using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Infrastructure.Migrations +namespace Umbraco.Cms.Infrastructure.Migrations; + +/// +/// Provides context to migrations. +/// +public interface IMigrationContext { /// - /// Provides context to migrations. + /// Gets the current migration plan + /// + MigrationPlan Plan { get; } + + /// + /// Gets the logger. + /// + ILogger Logger { get; } + + /// + /// Gets the database instance. + /// + IUmbracoDatabase Database { get; } + + /// + /// Gets the Sql context. + /// + ISqlContext SqlContext { get; } + + /// + /// Gets or sets the expression index. + /// + int Index { get; set; } + + /// + /// Gets or sets a value indicating whether an expression is being built. + /// + bool BuildingExpression { get; set; } + + /// + /// Adds a post-migration. /// - public interface IMigrationContext - { - /// - /// Gets the current migration plan - /// - MigrationPlan Plan { get; } - - /// - /// Gets the logger. - /// - ILogger Logger { get; } - - /// - /// Gets the database instance. - /// - IUmbracoDatabase Database { get; } - - /// - /// Gets the Sql context. - /// - ISqlContext SqlContext { get; } - - /// - /// Gets or sets the expression index. - /// - int Index { get; set; } - - /// - /// Gets or sets a value indicating whether an expression is being built. - /// - bool BuildingExpression { get; set; } - - /// - /// Adds a post-migration. - /// - void AddPostMigration() - where TMigration : MigrationBase; - } + void AddPostMigration() + where TMigration : MigrationBase; } diff --git a/src/Umbraco.Infrastructure/Migrations/IMigrationExpression.cs b/src/Umbraco.Infrastructure/Migrations/IMigrationExpression.cs index 3a5a4649fe90..00756a3da2a1 100644 --- a/src/Umbraco.Infrastructure/Migrations/IMigrationExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/IMigrationExpression.cs @@ -1,10 +1,9 @@ -namespace Umbraco.Cms.Infrastructure.Migrations +namespace Umbraco.Cms.Infrastructure.Migrations; + +/// +/// Marker interface for migration expressions +/// +public interface IMigrationExpression { - /// - /// Marker interface for migration expressions - /// - public interface IMigrationExpression - { - void Execute(); - } + void Execute(); } diff --git a/src/Umbraco.Infrastructure/Migrations/IMigrationPlanExecutor.cs b/src/Umbraco.Infrastructure/Migrations/IMigrationPlanExecutor.cs index 41a831360aef..552ca21b5e6c 100644 --- a/src/Umbraco.Infrastructure/Migrations/IMigrationPlanExecutor.cs +++ b/src/Umbraco.Infrastructure/Migrations/IMigrationPlanExecutor.cs @@ -1,10 +1,8 @@ -using System.Threading.Tasks; using Umbraco.Cms.Infrastructure.Migrations; -namespace Umbraco.Cms.Core.Migrations +namespace Umbraco.Cms.Core.Migrations; + +public interface IMigrationPlanExecutor { - public interface IMigrationPlanExecutor - { - string Execute(MigrationPlan plan, string fromState); - } + string Execute(MigrationPlan plan, string fromState); } diff --git a/src/Umbraco.Infrastructure/Migrations/IncompleteMigrationExpressionException.cs b/src/Umbraco.Infrastructure/Migrations/IncompleteMigrationExpressionException.cs index 67d559c66d1a..963948d9f6c0 100644 --- a/src/Umbraco.Infrastructure/Migrations/IncompleteMigrationExpressionException.cs +++ b/src/Umbraco.Infrastructure/Migrations/IncompleteMigrationExpressionException.cs @@ -1,49 +1,60 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Infrastructure.Migrations +namespace Umbraco.Cms.Infrastructure.Migrations; + +/// +/// The exception that is thrown when a migration expression is not executed. +/// +/// +/// Migration expressions such as Alter.Table(...).Do() must end with Do(), else they are not executed. +/// When a non-executed expression is detected, an IncompleteMigrationExpressionException is thrown. +/// +/// +[Serializable] +public class IncompleteMigrationExpressionException : Exception { /// - /// The exception that is thrown when a migration expression is not executed. + /// Initializes a new instance of the class. /// - /// - /// Migration expressions such as Alter.Table(...).Do() must end with Do(), else they are not executed. - /// When a non-executed expression is detected, an IncompleteMigrationExpressionException is thrown. - /// - /// - [Serializable] - public class IncompleteMigrationExpressionException : Exception + public IncompleteMigrationExpressionException() { - /// - /// Initializes a new instance of the class. - /// - public IncompleteMigrationExpressionException() - { } + } - /// - /// Initializes a new instance of the class with a message. - /// - /// The message that describes the error. - public IncompleteMigrationExpressionException(string message) - : base(message) - { } + /// + /// Initializes a new instance of the class with a message. + /// + /// The message that describes the error. + public IncompleteMigrationExpressionException(string message) + : base(message) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. - public IncompleteMigrationExpressionException(string message, Exception innerException) - : base(message, innerException) - { } + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// + /// The exception that is the cause of the current exception, or a null reference ( + /// in Visual Basic) if no inner exception is specified. + /// + public IncompleteMigrationExpressionException(string message, Exception innerException) + : base(message, innerException) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected IncompleteMigrationExpressionException(SerializationInfo info, StreamingContext context) - : base(info, context) - { } + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + protected IncompleteMigrationExpressionException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs index 542494458e4c..ef288c9467fd 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs @@ -1,9 +1,7 @@ -using System; -using System.Collections.Generic; using System.Data.Common; -using System.Linq; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; @@ -18,386 +16,393 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Install +namespace Umbraco.Cms.Infrastructure.Migrations.Install; + +/// +/// Supports building and configuring the database. +/// +public class DatabaseBuilder { + private readonly IConfigManipulator _configManipulator; + private readonly IOptionsMonitor _connectionStrings; + private readonly IUmbracoDatabaseFactory _databaseFactory; + private readonly IEnumerable _databaseProviderMetadata; + private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory; + private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator; + private readonly IOptionsMonitor _globalSettings; + private readonly IKeyValueService _keyValueService; + private readonly ILogger _logger; + private readonly IMigrationPlanExecutor _migrationPlanExecutor; + private readonly IRuntimeState _runtimeState; + private readonly IScopeAccessor _scopeAccessor; + private readonly ICoreScopeProvider _scopeProvider; + + private DatabaseSchemaResult? _databaseSchemaValidationResult; + /// - /// Supports building and configuring the database. + /// Initializes a new instance of the class. /// - public class DatabaseBuilder + public DatabaseBuilder( + ICoreScopeProvider scopeProvider, + IScopeAccessor scopeAccessor, + IUmbracoDatabaseFactory databaseFactory, + IRuntimeState runtimeState, + ILoggerFactory loggerFactory, + IKeyValueService keyValueService, + IDbProviderFactoryCreator dbProviderFactoryCreator, + IConfigManipulator configManipulator, + IOptionsMonitor globalSettings, + IOptionsMonitor connectionStrings, + IMigrationPlanExecutor migrationPlanExecutor, + DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, + IEnumerable databaseProviderMetadata) { - private readonly IUmbracoDatabaseFactory _databaseFactory; - private readonly ICoreScopeProvider _scopeProvider; - private readonly IScopeAccessor _scopeAccessor; - private readonly IRuntimeState _runtimeState; - private readonly IKeyValueService _keyValueService; - private readonly ILogger _logger; - private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator; - private readonly IConfigManipulator _configManipulator; - private readonly IOptionsMonitor _globalSettings; - private readonly IOptionsMonitor _connectionStrings; - private readonly IMigrationPlanExecutor _migrationPlanExecutor; - private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory; - private readonly IEnumerable _databaseProviderMetadata; - - private DatabaseSchemaResult? _databaseSchemaValidationResult; - - /// - /// Initializes a new instance of the class. - /// - public DatabaseBuilder( - ICoreScopeProvider scopeProvider, - IScopeAccessor scopeAccessor, - IUmbracoDatabaseFactory databaseFactory, - IRuntimeState runtimeState, - ILoggerFactory loggerFactory, - IKeyValueService keyValueService, - IDbProviderFactoryCreator dbProviderFactoryCreator, - IConfigManipulator configManipulator, - IOptionsMonitor globalSettings, - IOptionsMonitor connectionStrings, - IMigrationPlanExecutor migrationPlanExecutor, - DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, - IEnumerable databaseProviderMetadata) - { - _scopeProvider = scopeProvider; - _scopeAccessor = scopeAccessor; - _databaseFactory = databaseFactory; - _runtimeState = runtimeState; - _logger = loggerFactory.CreateLogger(); - _keyValueService = keyValueService; - _dbProviderFactoryCreator = dbProviderFactoryCreator; - _configManipulator = configManipulator; - _globalSettings = globalSettings; - _connectionStrings = connectionStrings; - _migrationPlanExecutor = migrationPlanExecutor; - _databaseSchemaCreatorFactory = databaseSchemaCreatorFactory; - _databaseProviderMetadata = databaseProviderMetadata; - } + _scopeProvider = scopeProvider; + _scopeAccessor = scopeAccessor; + _databaseFactory = databaseFactory; + _runtimeState = runtimeState; + _logger = loggerFactory.CreateLogger(); + _keyValueService = keyValueService; + _dbProviderFactoryCreator = dbProviderFactoryCreator; + _configManipulator = configManipulator; + _globalSettings = globalSettings; + _connectionStrings = connectionStrings; + _migrationPlanExecutor = migrationPlanExecutor; + _databaseSchemaCreatorFactory = databaseSchemaCreatorFactory; + _databaseProviderMetadata = databaseProviderMetadata; + } - #region Status + #region Status - /// - /// Gets a value indicating whether the database is configured. It does not necessarily - /// mean that it is possible to connect, nor that Umbraco is installed, nor up-to-date. - /// - public bool IsDatabaseConfigured => _databaseFactory.Configured; + /// + /// Gets a value indicating whether the database is configured. It does not necessarily + /// mean that it is possible to connect, nor that Umbraco is installed, nor up-to-date. + /// + public bool IsDatabaseConfigured => _databaseFactory.Configured; - /// - /// Gets a value indicating whether it is possible to connect to the configured database. - /// It does not necessarily mean that Umbraco is installed, nor up-to-date. - /// - public bool CanConnectToDatabase => _databaseFactory.CanConnect; + /// + /// Gets a value indicating whether it is possible to connect to the configured database. + /// It does not necessarily mean that Umbraco is installed, nor up-to-date. + /// + public bool CanConnectToDatabase => _databaseFactory.CanConnect; - /// - /// Verifies whether a it is possible to connect to a database. - /// - public bool CanConnect(string? connectionString, string providerName) - { - DbProviderFactory? factory = _dbProviderFactoryCreator.CreateFactory(providerName); - return DbConnectionExtensions.IsConnectionAvailable(connectionString, factory); - } + /// + /// Verifies whether a it is possible to connect to a database. + /// + public bool CanConnect(string? connectionString, string providerName) + { + DbProviderFactory? factory = _dbProviderFactoryCreator.CreateFactory(providerName); + return DbConnectionExtensions.IsConnectionAvailable(connectionString, factory); + } - public bool HasSomeNonDefaultUser() + public bool HasSomeNonDefaultUser() + { + using (ICoreScope scope = _scopeProvider.CreateCoreScope()) { - using (var scope = _scopeProvider.CreateCoreScope()) + // look for the super user with default password + Sql? sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() + .SelectCount() + .From() + .Where(x => x.Id == Constants.Security.SuperUserId && x.Password == "default"); + var result = _scopeAccessor.AmbientScope?.Database.ExecuteScalar(sql); + var has = result != 1; + if (has == false) { - // look for the super user with default password - var sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() - .SelectCount() - .From() - .Where(x => x.Id == Constants.Security.SuperUserId && x.Password == "default"); - var result = _scopeAccessor.AmbientScope?.Database.ExecuteScalar(sql); - var has = result != 1; - if (has == false) - { - // found only 1 user == the default user with default password - // however this always exists on uCloud, also need to check if there are other users too - result = _scopeAccessor.AmbientScope?.Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoUser"); - has = result != 1; - } - scope.Complete(); - return has; + // found only 1 user == the default user with default password + // however this always exists on uCloud, also need to check if there are other users too + result = _scopeAccessor.AmbientScope?.Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoUser"); + has = result != 1; } + + scope.Complete(); + return has; } + } - internal bool IsUmbracoInstalled() + internal bool IsUmbracoInstalled() + { + using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true)) { - using (var scope = _scopeProvider.CreateCoreScope(autoComplete: true)) - { - return _scopeAccessor.AmbientScope?.Database.IsUmbracoInstalled() ?? false; - } + return _scopeAccessor.AmbientScope?.Database.IsUmbracoInstalled() ?? false; } + } - #endregion + #endregion - #region Configure Connection String + #region Configure Connection String - public bool ConfigureDatabaseConnection(DatabaseModel databaseSettings, bool isTrialRun) - { - IDatabaseProviderMetadata? providerMeta; - - // if the database model is null then we will attempt quick install. - if (databaseSettings == null) - { - providerMeta = _databaseProviderMetadata - .OrderBy(x => x.SortOrder) - .Where(x => x.SupportsQuickInstall) - .FirstOrDefault(x => x.IsAvailable); + public bool ConfigureDatabaseConnection(DatabaseModel databaseSettings, bool isTrialRun) + { + IDatabaseProviderMetadata? providerMeta; - databaseSettings = new DatabaseModel - { - DatabaseName = providerMeta?.DefaultDatabaseName!, - }; - } - else - { - providerMeta = _databaseProviderMetadata - .FirstOrDefault(x => x.Id == databaseSettings.DatabaseProviderMetadataId); - } + // if the database model is null then we will attempt quick install. + if (databaseSettings == null) + { + providerMeta = _databaseProviderMetadata + .OrderBy(x => x.SortOrder) + .Where(x => x.SupportsQuickInstall) + .FirstOrDefault(x => x.IsAvailable); - if (providerMeta == null) - { - throw new InstallException("Unable to determine database provider configuration."); - } + databaseSettings = new DatabaseModel {DatabaseName = providerMeta?.DefaultDatabaseName!}; + } + else + { + providerMeta = _databaseProviderMetadata + .FirstOrDefault(x => x.Id == databaseSettings.DatabaseProviderMetadataId); + } - var connectionString = providerMeta.GenerateConnectionString(databaseSettings); - var providerName = databaseSettings.ProviderName ?? providerMeta.ProviderName; + if (providerMeta == null) + { + throw new InstallException("Unable to determine database provider configuration."); + } - if (providerMeta.RequiresConnectionTest && !CanConnect(connectionString, providerName!)) - { - return false; - } + var connectionString = providerMeta.GenerateConnectionString(databaseSettings); + var providerName = databaseSettings.ProviderName ?? providerMeta.ProviderName; - if (!isTrialRun) - { - _configManipulator.SaveConnectionString(connectionString!, providerName); - Configure(connectionString!, providerName, _globalSettings.CurrentValue.InstallMissingDatabase || providerMeta.ForceCreateDatabase); - } + if (providerMeta.RequiresConnectionTest && !CanConnect(connectionString, providerName!)) + { + return false; + } - return true; + if (!isTrialRun) + { + _configManipulator.SaveConnectionString(connectionString!, providerName); + Configure(connectionString!, providerName, + _globalSettings.CurrentValue.InstallMissingDatabase || providerMeta.ForceCreateDatabase); } + return true; + } + - private void Configure(string connectionString, string? providerName, bool installMissingDatabase) - { - // Update existing connection string - var umbracoConnectionString = _connectionStrings.Get(Core.Constants.System.UmbracoConnectionName); - umbracoConnectionString.ConnectionString = connectionString; - umbracoConnectionString.ProviderName = providerName; + private void Configure(string connectionString, string? providerName, bool installMissingDatabase) + { + // Update existing connection string + ConnectionStrings? umbracoConnectionString = _connectionStrings.Get(Constants.System.UmbracoConnectionName); + umbracoConnectionString.ConnectionString = connectionString; + umbracoConnectionString.ProviderName = providerName; - _databaseFactory.Configure(umbracoConnectionString); + _databaseFactory.Configure(umbracoConnectionString); - if (installMissingDatabase) - { - CreateDatabase(); - } + if (installMissingDatabase) + { + CreateDatabase(); } + } - #endregion + #endregion - #region Database Schema + #region Database Schema - public void CreateDatabase() => _dbProviderFactoryCreator.CreateDatabase(_databaseFactory.ProviderName!, _databaseFactory.ConnectionString!); + public void CreateDatabase() => + _dbProviderFactoryCreator.CreateDatabase(_databaseFactory.ProviderName!, _databaseFactory.ConnectionString!); - /// - /// Validates the database schema. - /// - /// - /// This assumes that the database exists and the connection string is - /// configured and it is possible to connect to the database. - /// - public DatabaseSchemaResult? ValidateSchema() + /// + /// Validates the database schema. + /// + /// + /// + /// This assumes that the database exists and the connection string is + /// configured and it is possible to connect to the database. + /// + /// + public DatabaseSchemaResult? ValidateSchema() + { + using (ICoreScope scope = _scopeProvider.CreateCoreScope()) { - using (var scope = _scopeProvider.CreateCoreScope()) - { - var result = ValidateSchema(scope); - scope.Complete(); - return result; - } + DatabaseSchemaResult? result = ValidateSchema(scope); + scope.Complete(); + return result; } + } - private DatabaseSchemaResult? ValidateSchema(ICoreScope scope) + private DatabaseSchemaResult? ValidateSchema(ICoreScope scope) + { + if (_databaseFactory.Initialized == false) { - if (_databaseFactory.Initialized == false) - return new DatabaseSchemaResult(); + return new DatabaseSchemaResult(); + } - if (_databaseSchemaValidationResult != null) - return _databaseSchemaValidationResult; + if (_databaseSchemaValidationResult != null) + { + return _databaseSchemaValidationResult; + } - _databaseSchemaValidationResult = _scopeAccessor.AmbientScope?.Database.ValidateSchema(); + _databaseSchemaValidationResult = _scopeAccessor.AmbientScope?.Database.ValidateSchema(); - scope.Complete(); + scope.Complete(); - return _databaseSchemaValidationResult; - } + return _databaseSchemaValidationResult; + } - /// - /// Creates the database schema and inserts initial data. - /// - /// - /// This assumes that the database exists and the connection string is - /// configured and it is possible to connect to the database. - /// - public Result? CreateSchemaAndData() + /// + /// Creates the database schema and inserts initial data. + /// + /// + /// + /// This assumes that the database exists and the connection string is + /// configured and it is possible to connect to the database. + /// + /// + public Result? CreateSchemaAndData() + { + using (ICoreScope scope = _scopeProvider.CreateCoreScope()) { - using (var scope = _scopeProvider.CreateCoreScope()) - { - var result = CreateSchemaAndData(scope); - scope.Complete(); - return result; - } + Result? result = CreateSchemaAndData(scope); + scope.Complete(); + return result; } + } - private Result? CreateSchemaAndData(ICoreScope scope) + private Result? CreateSchemaAndData(ICoreScope scope) + { + try { - try + Attempt readyForInstall = CheckReadyForInstall(); + if (readyForInstall.Success == false) { - var readyForInstall = CheckReadyForInstall(); - if (readyForInstall.Success == false) - { - return readyForInstall.Result; - } + return readyForInstall.Result; + } - _logger.LogInformation("Database configuration status: Started"); + _logger.LogInformation("Database configuration status: Started"); - var database = _scopeAccessor.AmbientScope?.Database; + IUmbracoDatabase? database = _scopeAccessor.AmbientScope?.Database; - var message = string.Empty; + var message = string.Empty; - var schemaResult = ValidateSchema(); - var hasInstalledVersion = schemaResult?.DetermineHasInstalledVersion() ?? false; + DatabaseSchemaResult? schemaResult = ValidateSchema(); + var hasInstalledVersion = schemaResult?.DetermineHasInstalledVersion() ?? false; - //If the determined version is "empty" its a new install - otherwise upgrade the existing - if (!hasInstalledVersion) + //If the determined version is "empty" its a new install - otherwise upgrade the existing + if (!hasInstalledVersion) + { + if (_runtimeState.Level == RuntimeLevel.Run) { - if (_runtimeState.Level == RuntimeLevel.Run) - throw new Exception("Umbraco is already configured!"); - - var creator = _databaseSchemaCreatorFactory.Create(database); - creator.InitializeDatabaseSchema(); + throw new Exception("Umbraco is already configured!"); + } - message = message + "

Installation completed!

"; + DatabaseSchemaCreator creator = _databaseSchemaCreatorFactory.Create(database); + creator.InitializeDatabaseSchema(); - //now that everything is done, we need to determine the version of SQL server that is executing - _logger.LogInformation("Database configuration status: {DbConfigStatus}", message); - return new Result { Message = message, Success = true, Percentage = "100" }; - } + message = message + "

Installation completed!

"; - //we need to do an upgrade so return a new status message and it will need to be done during the next step - _logger.LogInformation("Database requires upgrade"); - message = "

Upgrading database, this may take some time...

"; - return new Result - { - RequiresUpgrade = true, - Message = message, - Success = true, - Percentage = "30" - }; - } - catch (Exception ex) - { - return HandleInstallException(ex); + //now that everything is done, we need to determine the version of SQL server that is executing + _logger.LogInformation("Database configuration status: {DbConfigStatus}", message); + return new Result {Message = message, Success = true, Percentage = "100"}; } + + //we need to do an upgrade so return a new status message and it will need to be done during the next step + _logger.LogInformation("Database requires upgrade"); + message = "

Upgrading database, this may take some time...

"; + return new Result {RequiresUpgrade = true, Message = message, Success = true, Percentage = "30"}; } + catch (Exception ex) + { + return HandleInstallException(ex); + } + } - /// - /// Upgrades the database schema and data by running migrations. - /// - /// - /// This assumes that the database exists and the connection string is - /// configured and it is possible to connect to the database. - /// Runs whichever migrations need to run. - /// - public Result? UpgradeSchemaAndData(UmbracoPlan plan) + /// + /// Upgrades the database schema and data by running migrations. + /// + /// + /// + /// This assumes that the database exists and the connection string is + /// configured and it is possible to connect to the database. + /// + /// Runs whichever migrations need to run. + /// + public Result? UpgradeSchemaAndData(UmbracoPlan plan) + { + try { - try + Attempt readyForInstall = CheckReadyForInstall(); + if (readyForInstall.Success == false) { - var readyForInstall = CheckReadyForInstall(); - if (readyForInstall.Success == false) - { - return readyForInstall.Result; - } + return readyForInstall.Result; + } - _logger.LogInformation("Database upgrade started"); + _logger.LogInformation("Database upgrade started"); - // upgrade - var upgrader = new Upgrader(plan); - upgrader.Execute(_migrationPlanExecutor, _scopeProvider, _keyValueService); + // upgrade + var upgrader = new Upgrader(plan); + upgrader.Execute(_migrationPlanExecutor, _scopeProvider, _keyValueService); - var message = "

Upgrade completed!

"; + var message = "

Upgrade completed!

"; - //now that everything is done, we need to determine the version of SQL server that is executing + //now that everything is done, we need to determine the version of SQL server that is executing - _logger.LogInformation("Database configuration status: {DbConfigStatus}", message); + _logger.LogInformation("Database configuration status: {DbConfigStatus}", message); - return new Result { Message = message, Success = true, Percentage = "100" }; - } - catch (Exception ex) - { - return HandleInstallException(ex); - } + return new Result {Message = message, Success = true, Percentage = "100"}; + } + catch (Exception ex) + { + return HandleInstallException(ex); } + } - private Attempt CheckReadyForInstall() + private Attempt CheckReadyForInstall() + { + if (_databaseFactory.CanConnect == false) { - if (_databaseFactory.CanConnect == false) + return Attempt.Fail(new Result { - return Attempt.Fail(new Result - { - Message = "Database configuration is invalid. Please check that the entered database exists and" - + " that the provided username and password has write access to the database.", - Success = false, - Percentage = "10" - }); - } - return Attempt.Succeed(); + Message = "Database configuration is invalid. Please check that the entered database exists and" + + " that the provided username and password has write access to the database.", + Success = false, + Percentage = "10" + }); } - private Result HandleInstallException(Exception ex) - { - _logger.LogError(ex, "Database configuration failed"); + return Attempt.Succeed(); + } - if (_databaseSchemaValidationResult != null) - { - _logger.LogInformation("The database schema validation produced the following summary: {DbSchemaSummary}", _databaseSchemaValidationResult.GetSummary()); - } + private Result HandleInstallException(Exception ex) + { + _logger.LogError(ex, "Database configuration failed"); - return new Result - { - Message = - "The database configuration failed with the following message: " + ex.Message + - $"\n Please check log file for additional information (can be found in '{Constants.SystemDirectories.LogFiles}')", - Success = false, - Percentage = "90" - }; + if (_databaseSchemaValidationResult != null) + { + _logger.LogInformation("The database schema validation produced the following summary: {DbSchemaSummary}", + _databaseSchemaValidationResult.GetSummary()); } + return new Result + { + Message = + "The database configuration failed with the following message: " + ex.Message + + $"\n Please check log file for additional information (can be found in '{Constants.SystemDirectories.LogFiles}')", + Success = false, + Percentage = "90" + }; + } + + /// + /// Represents the result of a database creation or upgrade. + /// + public class Result + { /// - /// Represents the result of a database creation or upgrade. + /// Gets or sets a value indicating whether an upgrade is required. /// - public class Result - { - /// - /// Gets or sets a value indicating whether an upgrade is required. - /// - public bool RequiresUpgrade { get; set; } - - /// - /// Gets or sets the message returned by the operation. - /// - public string? Message { get; set; } - - /// - /// Gets or sets a value indicating whether the operation succeeded. - /// - public bool Success { get; set; } - - /// - /// Gets or sets an install progress pseudo-percentage. - /// - public string? Percentage { get; set; } - } + public bool RequiresUpgrade { get; set; } - #endregion + /// + /// Gets or sets the message returned by the operation. + /// + public string? Message { get; set; } + + /// + /// Gets or sets a value indicating whether the operation succeeded. + /// + public bool Success { get; set; } + + /// + /// Gets or sets an install progress pseudo-percentage. + /// + public string? Percentage { get; set; } } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs index c00a745d8ee9..9f6640035689 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs @@ -1,9 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; @@ -12,1046 +10,2260 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Install +namespace Umbraco.Cms.Infrastructure.Migrations.Install; + +/// +/// Creates the initial database data during install. +/// +internal class DatabaseDataCreator { + private readonly IDatabase _database; + + private readonly IDictionary> _entitiesToAlwaysCreate = new Dictionary> + { + { + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + new List {Constants.DataTypes.Guids.LabelString} + } + }; + + private readonly IOptionsMonitor _installDefaultDataSettings; + private readonly ILogger _logger; + private readonly IUmbracoVersion _umbracoVersion; + + public DatabaseDataCreator(IDatabase database, ILogger logger, IUmbracoVersion umbracoVersion, + IOptionsMonitor installDefaultDataSettings) + { + _database = database; + _logger = logger; + _umbracoVersion = umbracoVersion; + _installDefaultDataSettings = installDefaultDataSettings; + } + /// - /// Creates the initial database data during install. + /// Initialize the base data creation by inserting the data foundation for umbraco + /// specific to a table /// - internal class DatabaseDataCreator + /// Name of the table to create base data for + public void InitializeBaseData(string tableName) { - private readonly IDatabase _database; - private readonly ILogger _logger; - private readonly IUmbracoVersion _umbracoVersion; - private readonly IOptionsMonitor _installDefaultDataSettings; + _logger.LogInformation("Creating data in {TableName}", tableName); - private readonly IDictionary> _entitiesToAlwaysCreate = new Dictionary>() - { - { - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - new List - { - Cms.Core.Constants.DataTypes.Guids.LabelString, - } - } - }; + if (tableName.Equals(Constants.DatabaseSchema.Tables.Node)) + { + CreateNodeData(); + } - public DatabaseDataCreator(IDatabase database, ILogger logger, IUmbracoVersion umbracoVersion, IOptionsMonitor installDefaultDataSettings) + if (tableName.Equals(Constants.DatabaseSchema.Tables.Lock)) { - _database = database; - _logger = logger; - _umbracoVersion = umbracoVersion; - _installDefaultDataSettings = installDefaultDataSettings; + CreateLockData(); } - /// - /// Initialize the base data creation by inserting the data foundation for umbraco - /// specific to a table - /// - /// Name of the table to create base data for - public void InitializeBaseData(string tableName) + if (tableName.Equals(Constants.DatabaseSchema.Tables.ContentType)) { - _logger.LogInformation("Creating data in {TableName}", tableName); + CreateContentTypeData(); + } - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.Node)) - { - CreateNodeData(); - } + if (tableName.Equals(Constants.DatabaseSchema.Tables.User)) + { + CreateUserData(); + } - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.Lock)) - { - CreateLockData(); - } + if (tableName.Equals(Constants.DatabaseSchema.Tables.UserGroup)) + { + CreateUserGroupData(); + } - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.ContentType)) - { - CreateContentTypeData(); - } + if (tableName.Equals(Constants.DatabaseSchema.Tables.User2UserGroup)) + { + CreateUser2UserGroupData(); + } - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.User)) - { - CreateUserData(); - } + if (tableName.Equals(Constants.DatabaseSchema.Tables.UserGroup2App)) + { + CreateUserGroup2AppData(); + } - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup)) - { - CreateUserGroupData(); - } + if (tableName.Equals(Constants.DatabaseSchema.Tables.PropertyTypeGroup)) + { + CreatePropertyTypeGroupData(); + } - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.User2UserGroup)) - { - CreateUser2UserGroupData(); - } + if (tableName.Equals(Constants.DatabaseSchema.Tables.PropertyType)) + { + CreatePropertyTypeData(); + } - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup2App)) - { - CreateUserGroup2AppData(); - } + if (tableName.Equals(Constants.DatabaseSchema.Tables.Language)) + { + CreateLanguageData(); + } - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup)) - { - CreatePropertyTypeGroupData(); - } + if (tableName.Equals(Constants.DatabaseSchema.Tables.ContentChildType)) + { + CreateContentChildTypeData(); + } - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType)) - { - CreatePropertyTypeData(); - } + if (tableName.Equals(Constants.DatabaseSchema.Tables.DataType)) + { + CreateDataTypeData(); + } - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.Language)) - { - CreateLanguageData(); - } + if (tableName.Equals(Constants.DatabaseSchema.Tables.RelationType)) + { + CreateRelationTypeData(); + } - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.ContentChildType)) - { - CreateContentChildTypeData(); - } + if (tableName.Equals(Constants.DatabaseSchema.Tables.KeyValue)) + { + CreateKeyValueData(); + } - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.DataType)) - { - CreateDataTypeData(); - } + if (tableName.Equals(Constants.DatabaseSchema.Tables.LogViewerQuery)) + { + CreateLogViewerQueryData(); + } - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.RelationType)) - { - CreateRelationTypeData(); - } + _logger.LogInformation("Completed creating data in {TableName}", tableName); + } - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.KeyValue)) - { - CreateKeyValueData(); - } + private void CreateNodeData() + { + CreateNodeDataForDataTypes(); + CreateNodeDataForMediaTypes(); + CreateNodeDataForMemberTypes(); + } - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.LogViewerQuery)) - { - CreateLogViewerQueryData(); - } + private void CreateNodeDataForDataTypes() + { + void InsertDataTypeNodeDto(int id, int sortOrder, string uniqueId, string text) + { + var nodeDto = new NodeDto + { + NodeId = id, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1," + id, + SortOrder = sortOrder, + UniqueId = new Guid(uniqueId), + Text = text, + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now + }; - _logger.LogInformation("Completed creating data in {TableName}", tableName); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + uniqueId, + nodeDto, + Constants.DatabaseSchema.Tables.Node, + "id"); } - private void CreateNodeData() + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, + new NodeDto + { + NodeId = -1, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 0, + Path = "-1", + SortOrder = 0, + UniqueId = new Guid("916724a5-173d-4619-b97e-b9de133dd6f5"), + Text = "SYSTEM DATA: umbraco master root", + NodeObjectType = Constants.ObjectTypes.SystemRoot, + CreateDate = DateTime.Now + }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, + new NodeDto + { + NodeId = -20, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 0, + Path = "-1,-20", + SortOrder = 0, + UniqueId = new Guid("0F582A79-1E41-4CF0-BFA0-76340651891A"), + Text = "Recycle Bin", + NodeObjectType = Constants.ObjectTypes.ContentRecycleBin, + CreateDate = DateTime.Now + }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, + new NodeDto + { + NodeId = -21, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 0, + Path = "-1,-21", + SortOrder = 0, + UniqueId = new Guid("BF7C7CBC-952F-4518-97A2-69E9C7B33842"), + Text = "Recycle Bin", + NodeObjectType = Constants.ObjectTypes.MediaRecycleBin, + CreateDate = DateTime.Now + }); + + InsertDataTypeNodeDto(Constants.DataTypes.LabelString, 35, Constants.DataTypes.Guids.LabelString, + "Label (string)"); + InsertDataTypeNodeDto(Constants.DataTypes.LabelInt, 36, Constants.DataTypes.Guids.LabelInt, "Label (integer)"); + InsertDataTypeNodeDto(Constants.DataTypes.LabelBigint, 36, Constants.DataTypes.Guids.LabelBigInt, + "Label (bigint)"); + InsertDataTypeNodeDto(Constants.DataTypes.LabelDateTime, 37, Constants.DataTypes.Guids.LabelDateTime, + "Label (datetime)"); + InsertDataTypeNodeDto(Constants.DataTypes.LabelTime, 38, Constants.DataTypes.Guids.LabelTime, "Label (time)"); + InsertDataTypeNodeDto(Constants.DataTypes.LabelDecimal, 39, Constants.DataTypes.Guids.LabelDecimal, + "Label (decimal)"); + + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.Upload, + new NodeDto + { + NodeId = Constants.DataTypes.Upload, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.Upload}", + SortOrder = 34, + UniqueId = Constants.DataTypes.Guids.UploadGuid, + Text = "Upload File", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.UploadVideo, + new NodeDto + { + NodeId = Constants.DataTypes.UploadVideo, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.UploadVideo}", + SortOrder = 35, + UniqueId = Constants.DataTypes.Guids.UploadVideoGuid, + Text = "Upload Video", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.UploadAudio, + new NodeDto + { + NodeId = Constants.DataTypes.UploadAudio, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.UploadAudio}", + SortOrder = 36, + UniqueId = Constants.DataTypes.Guids.UploadAudioGuid, + Text = "Upload Audio", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.UploadArticle, + new NodeDto + { + NodeId = Constants.DataTypes.UploadArticle, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.UploadArticle}", + SortOrder = 37, + UniqueId = Constants.DataTypes.Guids.UploadArticleGuid, + Text = "Upload Article", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.UploadVectorGraphics, + new NodeDto + { + NodeId = Constants.DataTypes.UploadVectorGraphics, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.UploadVectorGraphics}", + SortOrder = 38, + UniqueId = Constants.DataTypes.Guids.UploadVectorGraphicsGuid, + Text = "Upload Vector Graphics", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.Textarea, + new NodeDto + { + NodeId = Constants.DataTypes.Textarea, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.Textarea}", + SortOrder = 33, + UniqueId = Constants.DataTypes.Guids.TextareaGuid, + Text = "Textarea", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.Textstring, + new NodeDto + { + NodeId = Constants.DataTypes.Textbox, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.Textbox}", + SortOrder = 32, + UniqueId = Constants.DataTypes.Guids.TextstringGuid, + Text = "Textstring", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.RichtextEditor, + new NodeDto + { + NodeId = Constants.DataTypes.RichtextEditor, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.RichtextEditor}", + SortOrder = 4, + UniqueId = Constants.DataTypes.Guids.RichtextEditorGuid, + Text = "Richtext editor", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.Numeric, + new NodeDto + { + NodeId = -51, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,-51", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.NumericGuid, + Text = "Numeric", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.Checkbox, + new NodeDto + { + NodeId = Constants.DataTypes.Boolean, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.Boolean}", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.CheckboxGuid, + Text = "True/false", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.CheckboxList, + new NodeDto + { + NodeId = -43, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,-43", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.CheckboxListGuid, + Text = "Checkbox list", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.Dropdown, + new NodeDto + { + NodeId = Constants.DataTypes.DropDownSingle, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.DropDownSingle}", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.DropdownGuid, + Text = "Dropdown", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.DatePicker, + new NodeDto + { + NodeId = -41, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,-41", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.DatePickerGuid, + Text = "Date Picker", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.Radiobox, + new NodeDto + { + NodeId = -40, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,-40", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.RadioboxGuid, + Text = "Radiobox", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.DropdownMultiple, + new NodeDto + { + NodeId = Constants.DataTypes.DropDownMultiple, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.DropDownMultiple}", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.DropdownMultipleGuid, + Text = "Dropdown multiple", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.ApprovedColor, + new NodeDto + { + NodeId = -37, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,-37", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.ApprovedColorGuid, + Text = "Approved Color", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.DatePickerWithTime, + new NodeDto + { + NodeId = Constants.DataTypes.DateTime, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.DateTime}", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.DatePickerWithTimeGuid, + Text = "Date Picker with time", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.ListViewContent, + new NodeDto + { + NodeId = Constants.DataTypes.DefaultContentListView, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.DefaultContentListView}", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.ListViewContentGuid, + Text = Constants.Conventions.DataTypes.ListViewPrefix + "Content", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.ListViewMedia, + new NodeDto + { + NodeId = Constants.DataTypes.DefaultMediaListView, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.DefaultMediaListView}", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.ListViewMediaGuid, + Text = Constants.Conventions.DataTypes.ListViewPrefix + "Media", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.ListViewMembers, + new NodeDto + { + NodeId = Constants.DataTypes.DefaultMembersListView, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.DefaultMembersListView}", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.ListViewMembersGuid, + Text = Constants.Conventions.DataTypes.ListViewPrefix + "Members", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.Tags, + new NodeDto + { + NodeId = Constants.DataTypes.Tags, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.Tags}", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.TagsGuid, + Text = "Tags", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.ImageCropper, + new NodeDto + { + NodeId = Constants.DataTypes.ImageCropper, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.ImageCropper}", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.ImageCropperGuid, + Text = "Image Cropper", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + + // New UDI pickers with newer Ids + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.ContentPicker, + new NodeDto + { + NodeId = 1046, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1046", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.ContentPickerGuid, + Text = "Content Picker", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.MemberPicker, + new NodeDto + { + NodeId = 1047, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1047", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.MemberPickerGuid, + Text = "Member Picker", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.MediaPicker, + new NodeDto + { + NodeId = 1048, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1048", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.MediaPickerGuid, + Text = "Media Picker (legacy)", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.MultipleMediaPicker, + new NodeDto + { + NodeId = 1049, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1049", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.MultipleMediaPickerGuid, + Text = "Multiple Media Picker (legacy)", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.RelatedLinks, + new NodeDto + { + NodeId = 1050, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1050", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.RelatedLinksGuid, + Text = "Multi URL Picker", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.MediaPicker3, + new NodeDto + { + NodeId = 1051, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1051", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.MediaPicker3Guid, + Text = "Media Picker", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.MediaPicker3Multiple, + new NodeDto + { + NodeId = 1052, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1052", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.MediaPicker3MultipleGuid, + Text = "Multiple Media Picker", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.MediaPicker3SingleImage, + new NodeDto + { + NodeId = 1053, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1053", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.MediaPicker3SingleImageGuid, + Text = "Image Media Picker", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.MediaPicker3MultipleImages, + new NodeDto + { + NodeId = 1054, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1054", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.MediaPicker3MultipleImagesGuid, + Text = "Multiple Image Media Picker", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + } + + private void CreateNodeDataForMediaTypes() + { + var folderUniqueId = new Guid("f38bd2d7-65d0-48e6-95dc-87ce06ec2d3d"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, + folderUniqueId.ToString(), + new NodeDto + { + NodeId = 1031, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1031", + SortOrder = 2, + UniqueId = folderUniqueId, + Text = Constants.Conventions.MediaTypes.Folder, + NodeObjectType = Constants.ObjectTypes.MediaType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + + var imageUniqueId = new Guid("cc07b313-0843-4aa8-bbda-871c8da728c8"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, + imageUniqueId.ToString(), + new NodeDto + { + NodeId = 1032, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1032", + SortOrder = 2, + UniqueId = imageUniqueId, + Text = Constants.Conventions.MediaTypes.Image, + NodeObjectType = Constants.ObjectTypes.MediaType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + + var fileUniqueId = new Guid("4c52d8ab-54e6-40cd-999c-7a5f24903e4d"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, + fileUniqueId.ToString(), + new NodeDto + { + NodeId = 1033, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1033", + SortOrder = 2, + UniqueId = fileUniqueId, + Text = Constants.Conventions.MediaTypes.File, + NodeObjectType = Constants.ObjectTypes.MediaType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + + var videoUniqueId = new Guid("f6c515bb-653c-4bdc-821c-987729ebe327"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, + videoUniqueId.ToString(), + new NodeDto + { + NodeId = 1034, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1034", + SortOrder = 2, + UniqueId = videoUniqueId, + Text = Constants.Conventions.MediaTypes.Video, + NodeObjectType = Constants.ObjectTypes.MediaType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + + var audioUniqueId = new Guid("a5ddeee0-8fd8-4cee-a658-6f1fcdb00de3"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, + audioUniqueId.ToString(), + new NodeDto + { + NodeId = 1035, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1035", + SortOrder = 2, + UniqueId = audioUniqueId, + Text = Constants.Conventions.MediaTypes.Audio, + NodeObjectType = Constants.ObjectTypes.MediaType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + + var articleUniqueId = new Guid("a43e3414-9599-4230-a7d3-943a21b20122"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, + articleUniqueId.ToString(), + new NodeDto + { + NodeId = 1036, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1036", + SortOrder = 2, + UniqueId = articleUniqueId, + Text = Constants.Conventions.MediaTypes.Article, + NodeObjectType = Constants.ObjectTypes.MediaType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + + var svgUniqueId = new Guid("c4b1efcf-a9d5-41c4-9621-e9d273b52a9c"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, + svgUniqueId.ToString(), + new NodeDto + { + NodeId = 1037, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1037", + SortOrder = 2, + UniqueId = svgUniqueId, + Text = "Vector Graphics (SVG)", + NodeObjectType = Constants.ObjectTypes.MediaType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + } + + private void CreateNodeDataForMemberTypes() + { + var memberUniqueId = new Guid("d59be02f-1df9-4228-aa1e-01917d806cda"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.MemberTypes, + memberUniqueId.ToString(), + new NodeDto + { + NodeId = 1044, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1044", + SortOrder = 0, + UniqueId = memberUniqueId, + Text = Constants.Conventions.MemberTypes.DefaultAlias, + NodeObjectType = Constants.ObjectTypes.MemberType, + CreateDate = DateTime.Now + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + } + + private void CreateLockData() + { + // all lock objects + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto {Id = Constants.Locks.Servers, Name = "Servers"}); + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto {Id = Constants.Locks.ContentTypes, Name = "ContentTypes"}); + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto {Id = Constants.Locks.ContentTree, Name = "ContentTree"}); + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto {Id = Constants.Locks.MediaTypes, Name = "MediaTypes"}); + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto {Id = Constants.Locks.MediaTree, Name = "MediaTree"}); + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto {Id = Constants.Locks.MemberTypes, Name = "MemberTypes"}); + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto {Id = Constants.Locks.MemberTree, Name = "MemberTree"}); + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto {Id = Constants.Locks.Domains, Name = "Domains"}); + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto {Id = Constants.Locks.KeyValues, Name = "KeyValues"}); + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto {Id = Constants.Locks.Languages, Name = "Languages"}); + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto {Id = Constants.Locks.ScheduledPublishing, Name = "ScheduledPublishing"}); + + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto {Id = Constants.Locks.MainDom, Name = "MainDom"}); + } + + private void CreateContentTypeData() + { + // Insert content types only if the corresponding Node record exists (which may or may not have been created depending on configuration + // of media or member types to create). + + // Media types. + if (_database.Exists(1031)) { - CreateNodeDataForDataTypes(); - CreateNodeDataForMediaTypes(); - CreateNodeDataForMemberTypes(); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, + new ContentTypeDto + { + PrimaryKey = 532, + NodeId = 1031, + Alias = Constants.Conventions.MediaTypes.Folder, + Icon = Constants.Icons.MediaFolder, + Thumbnail = Constants.Icons.MediaFolder, + IsContainer = false, + AllowAtRoot = true, + Variations = (byte)ContentVariation.Nothing + }); } - private void CreateNodeDataForDataTypes() + if (_database.Exists(1032)) { - void InsertDataTypeNodeDto(int id, int sortOrder, string uniqueId, string text) - { - var nodeDto = new NodeDto - { - NodeId = id, - Trashed = false, - ParentId = -1, - UserId = -1, - Level = 1, - Path = "-1," + id, - SortOrder = sortOrder, - UniqueId = new Guid(uniqueId), - Text = text, - NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, - CreateDate = DateTime.Now, - }; - - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - uniqueId, - nodeDto, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - } - - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -1, Trashed = false, ParentId = -1, UserId = -1, Level = 0, Path = "-1", SortOrder = 0, UniqueId = new Guid("916724a5-173d-4619-b97e-b9de133dd6f5"), Text = "SYSTEM DATA: umbraco master root", NodeObjectType = Cms.Core.Constants.ObjectTypes.SystemRoot, CreateDate = DateTime.Now }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -20, Trashed = false, ParentId = -1, UserId = -1, Level = 0, Path = "-1,-20", SortOrder = 0, UniqueId = new Guid("0F582A79-1E41-4CF0-BFA0-76340651891A"), Text = "Recycle Bin", NodeObjectType = Cms.Core.Constants.ObjectTypes.ContentRecycleBin, CreateDate = DateTime.Now }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -21, Trashed = false, ParentId = -1, UserId = -1, Level = 0, Path = "-1,-21", SortOrder = 0, UniqueId = new Guid("BF7C7CBC-952F-4518-97A2-69E9C7B33842"), Text = "Recycle Bin", NodeObjectType = Cms.Core.Constants.ObjectTypes.MediaRecycleBin, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, + new ContentTypeDto + { + PrimaryKey = 533, + NodeId = 1032, + Alias = Constants.Conventions.MediaTypes.Image, + Icon = Constants.Icons.MediaImage, + Thumbnail = Constants.Icons.MediaImage, + AllowAtRoot = true, + Variations = (byte)ContentVariation.Nothing + }); + } - InsertDataTypeNodeDto(Cms.Core.Constants.DataTypes.LabelString, 35, Cms.Core.Constants.DataTypes.Guids.LabelString, "Label (string)"); - InsertDataTypeNodeDto(Cms.Core.Constants.DataTypes.LabelInt, 36, Cms.Core.Constants.DataTypes.Guids.LabelInt, "Label (integer)"); - InsertDataTypeNodeDto(Cms.Core.Constants.DataTypes.LabelBigint, 36, Cms.Core.Constants.DataTypes.Guids.LabelBigInt, "Label (bigint)"); - InsertDataTypeNodeDto(Cms.Core.Constants.DataTypes.LabelDateTime, 37, Cms.Core.Constants.DataTypes.Guids.LabelDateTime, "Label (datetime)"); - InsertDataTypeNodeDto(Cms.Core.Constants.DataTypes.LabelTime, 38, Cms.Core.Constants.DataTypes.Guids.LabelTime, "Label (time)"); - InsertDataTypeNodeDto(Cms.Core.Constants.DataTypes.LabelDecimal, 39, Cms.Core.Constants.DataTypes.Guids.LabelDecimal, "Label (decimal)"); + if (_database.Exists(1033)) + { + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, + new ContentTypeDto + { + PrimaryKey = 534, + NodeId = 1033, + Alias = Constants.Conventions.MediaTypes.File, + Icon = Constants.Icons.MediaFile, + Thumbnail = Constants.Icons.MediaFile, + AllowAtRoot = true, + Variations = (byte)ContentVariation.Nothing + }); + } - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.Upload, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.Upload, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.Upload}", SortOrder = 34, UniqueId = Cms.Core.Constants.DataTypes.Guids.UploadGuid, Text = "Upload File", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.UploadVideo, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.UploadVideo, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.UploadVideo}", SortOrder = 35, UniqueId = Cms.Core.Constants.DataTypes.Guids.UploadVideoGuid, Text = "Upload Video", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.UploadAudio, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.UploadAudio, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.UploadAudio}", SortOrder = 36, UniqueId = Cms.Core.Constants.DataTypes.Guids.UploadAudioGuid, Text = "Upload Audio", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.UploadArticle, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.UploadArticle, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.UploadArticle}", SortOrder = 37, UniqueId = Cms.Core.Constants.DataTypes.Guids.UploadArticleGuid, Text = "Upload Article", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.UploadVectorGraphics, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.UploadVectorGraphics, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.UploadVectorGraphics}", SortOrder = 38, UniqueId = Cms.Core.Constants.DataTypes.Guids.UploadVectorGraphicsGuid, Text = "Upload Vector Graphics", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.Textarea, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.Textarea, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.Textarea}", SortOrder = 33, UniqueId = Cms.Core.Constants.DataTypes.Guids.TextareaGuid, Text = "Textarea", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.Textstring, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.Textbox, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.Textbox}", SortOrder = 32, UniqueId = Cms.Core.Constants.DataTypes.Guids.TextstringGuid, Text = "Textstring", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.RichtextEditor, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.RichtextEditor, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.RichtextEditor}", SortOrder = 4, UniqueId = Cms.Core.Constants.DataTypes.Guids.RichtextEditorGuid, Text = "Richtext editor", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.Numeric, - new NodeDto { NodeId = -51, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-51", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.NumericGuid, Text = "Numeric", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.Checkbox, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.Boolean, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.Boolean}", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.CheckboxGuid, Text = "True/false", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.CheckboxList, - new NodeDto { NodeId = -43, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-43", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.CheckboxListGuid, Text = "Checkbox list", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.Dropdown, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.DropDownSingle, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.DropDownSingle}", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.DropdownGuid, Text = "Dropdown", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.DatePicker, - new NodeDto { NodeId = -41, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-41", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.DatePickerGuid, Text = "Date Picker", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.Radiobox, - new NodeDto { NodeId = -40, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-40", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.RadioboxGuid, Text = "Radiobox", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.DropdownMultiple, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.DropDownMultiple, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.DropDownMultiple}", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.DropdownMultipleGuid, Text = "Dropdown multiple", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.ApprovedColor, - new NodeDto { NodeId = -37, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-37", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.ApprovedColorGuid, Text = "Approved Color", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.DatePickerWithTime, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.DateTime, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.DateTime}", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.DatePickerWithTimeGuid, Text = "Date Picker with time", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.ListViewContent, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.DefaultContentListView, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.DefaultContentListView}", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.ListViewContentGuid, Text = Cms.Core.Constants.Conventions.DataTypes.ListViewPrefix + "Content", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.ListViewMedia, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.DefaultMediaListView, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.DefaultMediaListView}", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.ListViewMediaGuid, Text = Cms.Core.Constants.Conventions.DataTypes.ListViewPrefix + "Media", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.ListViewMembers, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.DefaultMembersListView, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.DefaultMembersListView}", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.ListViewMembersGuid, Text = Cms.Core.Constants.Conventions.DataTypes.ListViewPrefix + "Members", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.Tags, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.Tags, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.Tags}", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.TagsGuid, Text = "Tags", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.ImageCropper, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.ImageCropper, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.ImageCropper}", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.ImageCropperGuid, Text = "Image Cropper", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); + if (_database.Exists(1034)) + { + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, + new ContentTypeDto + { + PrimaryKey = 540, + NodeId = 1034, + Alias = Constants.Conventions.MediaTypes.VideoAlias, + Icon = Constants.Icons.MediaVideo, + Thumbnail = Constants.Icons.MediaVideo, + AllowAtRoot = true, + Variations = (byte)ContentVariation.Nothing + }); + } - // New UDI pickers with newer Ids - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.ContentPicker, - new NodeDto { NodeId = 1046, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1046", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.ContentPickerGuid, Text = "Content Picker", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.MemberPicker, - new NodeDto { NodeId = 1047, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1047", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.MemberPickerGuid, Text = "Member Picker", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.MediaPicker, - new NodeDto { NodeId = 1048, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1048", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.MediaPickerGuid, Text = "Media Picker (legacy)", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.MultipleMediaPicker, - new NodeDto { NodeId = 1049, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1049", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.MultipleMediaPickerGuid, Text = "Multiple Media Picker (legacy)", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.RelatedLinks, - new NodeDto { NodeId = 1050, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1050", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.RelatedLinksGuid, Text = "Multi URL Picker", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); + if (_database.Exists(1035)) + { + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, + new ContentTypeDto + { + PrimaryKey = 541, + NodeId = 1035, + Alias = Constants.Conventions.MediaTypes.AudioAlias, + Icon = Constants.Icons.MediaAudio, + Thumbnail = Constants.Icons.MediaAudio, + AllowAtRoot = true, + Variations = (byte)ContentVariation.Nothing + }); + } - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.MediaPicker3, - new NodeDto { NodeId = 1051, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1051", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.MediaPicker3Guid, Text = "Media Picker", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.MediaPicker3Multiple, - new NodeDto { NodeId = 1052, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1052", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.MediaPicker3MultipleGuid, Text = "Multiple Media Picker", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.MediaPicker3SingleImage, - new NodeDto { NodeId = 1053, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1053", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.MediaPicker3SingleImageGuid, Text = "Image Media Picker", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.MediaPicker3MultipleImages, - new NodeDto { NodeId = 1054, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1054", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.MediaPicker3MultipleImagesGuid, Text = "Multiple Image Media Picker", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); + if (_database.Exists(1036)) + { + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, + new ContentTypeDto + { + PrimaryKey = 542, + NodeId = 1036, + Alias = Constants.Conventions.MediaTypes.ArticleAlias, + Icon = Constants.Icons.MediaArticle, + Thumbnail = Constants.Icons.MediaArticle, + AllowAtRoot = true, + Variations = (byte)ContentVariation.Nothing + }); } - private void CreateNodeDataForMediaTypes() + if (_database.Exists(1037)) { - var folderUniqueId = new Guid("f38bd2d7-65d0-48e6-95dc-87ce06ec2d3d"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, - folderUniqueId.ToString(), - new NodeDto { NodeId = 1031, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1031", SortOrder = 2, UniqueId = folderUniqueId, Text = Cms.Core.Constants.Conventions.MediaTypes.Folder, NodeObjectType = Cms.Core.Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, + new ContentTypeDto + { + PrimaryKey = 543, + NodeId = 1037, + Alias = Constants.Conventions.MediaTypes.VectorGraphicsAlias, + Icon = Constants.Icons.MediaVectorGraphics, + Thumbnail = Constants.Icons.MediaVectorGraphics, + AllowAtRoot = true, + Variations = (byte)ContentVariation.Nothing + }); + } - var imageUniqueId = new Guid("cc07b313-0843-4aa8-bbda-871c8da728c8"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, - imageUniqueId.ToString(), - new NodeDto { NodeId = 1032, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1032", SortOrder = 2, UniqueId = imageUniqueId, Text = Cms.Core.Constants.Conventions.MediaTypes.Image, NodeObjectType = Cms.Core.Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); + // Member type. + if (_database.Exists(1044)) + { + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, + new ContentTypeDto + { + PrimaryKey = 531, + NodeId = 1044, + Alias = Constants.Conventions.MemberTypes.DefaultAlias, + Icon = Constants.Icons.Member, + Thumbnail = Constants.Icons.Member, + Variations = (byte)ContentVariation.Nothing + }); + } + } - var fileUniqueId = new Guid("4c52d8ab-54e6-40cd-999c-7a5f24903e4d"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, - fileUniqueId.ToString(), - new NodeDto { NodeId = 1033, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1033", SortOrder = 2, UniqueId = fileUniqueId, Text = Cms.Core.Constants.Conventions.MediaTypes.File, NodeObjectType = Cms.Core.Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); + private void CreateUserData() => _database.Insert(Constants.DatabaseSchema.Tables.User, "id", false, + new UserDto + { + Id = Constants.Security.SuperUserId, + Disabled = false, + NoConsole = false, + UserName = "Administrator", + Login = "admin", + Password = "default", + Email = string.Empty, + UserLanguage = "en-US", + CreateDate = DateTime.Now, + UpdateDate = DateTime.Now + }); + + private void CreateUserGroupData() + { + _database.Insert(Constants.DatabaseSchema.Tables.UserGroup, "id", false, + new UserGroupDto + { + Id = 1, + StartMediaId = -1, + StartContentId = -1, + Alias = Constants.Security.AdminGroupAlias, + Name = "Administrators", + DefaultPermissions = "CADMOSKTPIURZ:5F7ïN", + CreateDate = DateTime.Now, + UpdateDate = DateTime.Now, + Icon = "icon-medal" + }); + _database.Insert(Constants.DatabaseSchema.Tables.UserGroup, "id", false, + new UserGroupDto + { + Id = 2, + StartMediaId = -1, + StartContentId = -1, + Alias = Constants.Security.WriterGroupAlias, + Name = "Writers", + DefaultPermissions = "CAH:FN", + CreateDate = DateTime.Now, + UpdateDate = DateTime.Now, + Icon = "icon-edit" + }); + _database.Insert(Constants.DatabaseSchema.Tables.UserGroup, "id", false, + new UserGroupDto + { + Id = 3, + StartMediaId = -1, + StartContentId = -1, + Alias = Constants.Security.EditorGroupAlias, + Name = "Editors", + DefaultPermissions = "CADMOSKTPUZ:5FïN", + CreateDate = DateTime.Now, + UpdateDate = DateTime.Now, + Icon = "icon-tools" + }); + _database.Insert(Constants.DatabaseSchema.Tables.UserGroup, "id", false, + new UserGroupDto + { + Id = 4, + StartMediaId = -1, + StartContentId = -1, + Alias = Constants.Security.TranslatorGroupAlias, + Name = "Translators", + DefaultPermissions = "AF", + CreateDate = DateTime.Now, + UpdateDate = DateTime.Now, + Icon = "icon-globe" + }); + _database.Insert(Constants.DatabaseSchema.Tables.UserGroup, "id", false, + new UserGroupDto + { + Id = 5, + StartMediaId = -1, + StartContentId = -1, + Alias = Constants.Security.SensitiveDataGroupAlias, + Name = "Sensitive data", + DefaultPermissions = string.Empty, + CreateDate = DateTime.Now, + UpdateDate = DateTime.Now, + Icon = "icon-lock" + }); + } - var videoUniqueId = new Guid("f6c515bb-653c-4bdc-821c-987729ebe327"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, - videoUniqueId.ToString(), - new NodeDto { NodeId = 1034, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1034", SortOrder = 2, UniqueId = videoUniqueId, Text = Cms.Core.Constants.Conventions.MediaTypes.Video, NodeObjectType = Cms.Core.Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); + private void CreateUser2UserGroupData() + { + _database.Insert(new User2UserGroupDto + { + UserGroupId = 1, UserId = Constants.Security.SuperUserId + }); // add super to admins + _database.Insert(new User2UserGroupDto + { + UserGroupId = 5, UserId = Constants.Security.SuperUserId + }); // add super to sensitive data + } - var audioUniqueId = new Guid("a5ddeee0-8fd8-4cee-a658-6f1fcdb00de3"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, - audioUniqueId.ToString(), - new NodeDto { NodeId = 1035, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1035", SortOrder = 2, UniqueId = audioUniqueId, Text = Cms.Core.Constants.Conventions.MediaTypes.Audio, NodeObjectType = Cms.Core.Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); + private void CreateUserGroup2AppData() + { + _database.Insert(new UserGroup2AppDto {UserGroupId = 1, AppAlias = Constants.Applications.Content}); + _database.Insert(new UserGroup2AppDto {UserGroupId = 1, AppAlias = Constants.Applications.Packages}); + _database.Insert(new UserGroup2AppDto {UserGroupId = 1, AppAlias = Constants.Applications.Media}); + _database.Insert(new UserGroup2AppDto {UserGroupId = 1, AppAlias = Constants.Applications.Members}); + _database.Insert(new UserGroup2AppDto {UserGroupId = 1, AppAlias = Constants.Applications.Settings}); + _database.Insert(new UserGroup2AppDto {UserGroupId = 1, AppAlias = Constants.Applications.Users}); + _database.Insert(new UserGroup2AppDto {UserGroupId = 1, AppAlias = Constants.Applications.Forms}); + _database.Insert(new UserGroup2AppDto {UserGroupId = 1, AppAlias = Constants.Applications.Translation}); + + _database.Insert(new UserGroup2AppDto {UserGroupId = 2, AppAlias = Constants.Applications.Content}); + + _database.Insert(new UserGroup2AppDto {UserGroupId = 3, AppAlias = Constants.Applications.Content}); + _database.Insert(new UserGroup2AppDto {UserGroupId = 3, AppAlias = Constants.Applications.Media}); + _database.Insert(new UserGroup2AppDto {UserGroupId = 3, AppAlias = Constants.Applications.Forms}); + + _database.Insert(new UserGroup2AppDto {UserGroupId = 4, AppAlias = Constants.Applications.Translation}); + } - var articleUniqueId = new Guid("a43e3414-9599-4230-a7d3-943a21b20122"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, - articleUniqueId.ToString(), - new NodeDto { NodeId = 1036, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1036", SortOrder = 2, UniqueId = articleUniqueId, Text = Cms.Core.Constants.Conventions.MediaTypes.Article, NodeObjectType = Cms.Core.Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); + private void CreatePropertyTypeGroupData() + { + // Insert property groups only if the corresponding content type node record exists (which may or may not have been created depending on configuration + // of media or member types to create). - var svgUniqueId = new Guid("c4b1efcf-a9d5-41c4-9621-e9d273b52a9c"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, - svgUniqueId.ToString(), - new NodeDto { NodeId = 1037, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1037", SortOrder = 2, UniqueId = svgUniqueId, Text = "Vector Graphics (SVG)", NodeObjectType = Cms.Core.Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); + // Media property groups. + if (_database.Exists(1032)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, + new PropertyTypeGroupDto + { + Id = 3, + UniqueId = new Guid(Constants.PropertyTypeGroups.Image), + ContentTypeNodeId = 1032, + Text = "Image", + Alias = "image", + SortOrder = 1 + }); } - private void CreateNodeDataForMemberTypes() + if (_database.Exists(1033)) { - var memberUniqueId = new Guid("d59be02f-1df9-4228-aa1e-01917d806cda"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.MemberTypes, - memberUniqueId.ToString(), - new NodeDto { NodeId = 1044, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1044", SortOrder = 0, UniqueId = memberUniqueId, Text = Cms.Core.Constants.Conventions.MemberTypes.DefaultAlias, NodeObjectType = Cms.Core.Constants.ObjectTypes.MemberType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, + new PropertyTypeGroupDto + { + Id = 4, + UniqueId = new Guid(Constants.PropertyTypeGroups.File), + ContentTypeNodeId = 1033, + Text = "File", + Alias = "file", + SortOrder = 1 + }); } - private void CreateLockData() + if (_database.Exists(1034)) { - // all lock objects - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.Servers, Name = "Servers" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.ContentTypes, Name = "ContentTypes" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.ContentTree, Name = "ContentTree" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.MediaTypes, Name = "MediaTypes" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.MediaTree, Name = "MediaTree" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.MemberTypes, Name = "MemberTypes" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.MemberTree, Name = "MemberTree" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.Domains, Name = "Domains" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.KeyValues, Name = "KeyValues" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.Languages, Name = "Languages" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.ScheduledPublishing, Name = "ScheduledPublishing" }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, + new PropertyTypeGroupDto + { + Id = 52, + UniqueId = new Guid(Constants.PropertyTypeGroups.Video), + ContentTypeNodeId = 1034, + Text = "Video", + Alias = "video", + SortOrder = 1 + }); + } - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.MainDom, Name = "MainDom" }); + if (_database.Exists(1035)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, + new PropertyTypeGroupDto + { + Id = 53, + UniqueId = new Guid(Constants.PropertyTypeGroups.Audio), + ContentTypeNodeId = 1035, + Text = "Audio", + Alias = "audio", + SortOrder = 1 + }); } - private void CreateContentTypeData() + if (_database.Exists(1036)) { - // Insert content types only if the corresponding Node record exists (which may or may not have been created depending on configuration - // of media or member types to create). + _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, + new PropertyTypeGroupDto + { + Id = 54, + UniqueId = new Guid(Constants.PropertyTypeGroups.Article), + ContentTypeNodeId = 1036, + Text = "Article", + Alias = "article", + SortOrder = 1 + }); + } - // Media types. - if (_database.Exists(1031)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 532, NodeId = 1031, Alias = Cms.Core.Constants.Conventions.MediaTypes.Folder, Icon = Cms.Core.Constants.Icons.MediaFolder, Thumbnail = Cms.Core.Constants.Icons.MediaFolder, IsContainer = false, AllowAtRoot = true, Variations = (byte)ContentVariation.Nothing }); - } + if (_database.Exists(1037)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, + new PropertyTypeGroupDto + { + Id = 55, + UniqueId = new Guid(Constants.PropertyTypeGroups.VectorGraphics), + ContentTypeNodeId = 1037, + Text = "Vector Graphics", + Alias = "vectorGraphics", + SortOrder = 1 + }); + } - if (_database.Exists(1032)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 533, NodeId = 1032, Alias = Cms.Core.Constants.Conventions.MediaTypes.Image, Icon = Cms.Core.Constants.Icons.MediaImage, Thumbnail = Cms.Core.Constants.Icons.MediaImage, AllowAtRoot = true, Variations = (byte)ContentVariation.Nothing }); - } + // Membership property group. + if (_database.Exists(1044)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, + new PropertyTypeGroupDto + { + Id = 11, + UniqueId = new Guid(Constants.PropertyTypeGroups.Membership), + ContentTypeNodeId = 1044, + Text = Constants.Conventions.Member.StandardPropertiesGroupName, + Alias = Constants.Conventions.Member.StandardPropertiesGroupAlias, + SortOrder = 1 + }); + } + } - if (_database.Exists(1033)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 534, NodeId = 1033, Alias = Cms.Core.Constants.Conventions.MediaTypes.File, Icon = Cms.Core.Constants.Icons.MediaFile, Thumbnail = Cms.Core.Constants.Icons.MediaFile, AllowAtRoot = true, Variations = (byte)ContentVariation.Nothing }); - } + private void CreatePropertyTypeData() + { + // Insert property types only if the corresponding property group record exists (which may or may not have been created depending on configuration + // of media or member types to create). - if (_database.Exists(1034)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 540, NodeId = 1034, Alias = Cms.Core.Constants.Conventions.MediaTypes.VideoAlias, Icon = Cms.Core.Constants.Icons.MediaVideo, Thumbnail = Cms.Core.Constants.Icons.MediaVideo, AllowAtRoot = true, Variations = (byte)ContentVariation.Nothing }); - } + // Media property types. + if (_database.Exists(3)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 6, + UniqueId = 6.ToGuid(), + DataTypeId = Constants.DataTypes.ImageCropper, + ContentTypeId = 1032, + PropertyTypeGroupId = 3, + Alias = Constants.Conventions.Media.File, + Name = "Image", + SortOrder = 0, + Mandatory = true, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 7, + UniqueId = 7.ToGuid(), + DataTypeId = Constants.DataTypes.LabelInt, + ContentTypeId = 1032, + PropertyTypeGroupId = 3, + Alias = Constants.Conventions.Media.Width, + Name = "Width", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = "in pixels", + Variations = (byte)ContentVariation.Nothing + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 8, + UniqueId = 8.ToGuid(), + DataTypeId = Constants.DataTypes.LabelInt, + ContentTypeId = 1032, + PropertyTypeGroupId = 3, + Alias = Constants.Conventions.Media.Height, + Name = "Height", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = "in pixels", + Variations = (byte)ContentVariation.Nothing + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 9, + UniqueId = 9.ToGuid(), + DataTypeId = Constants.DataTypes.LabelBigint, + ContentTypeId = 1032, + PropertyTypeGroupId = 3, + Alias = Constants.Conventions.Media.Bytes, + Name = "Size", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = "in bytes", + Variations = (byte)ContentVariation.Nothing + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 10, + UniqueId = 10.ToGuid(), + DataTypeId = -92, + ContentTypeId = 1032, + PropertyTypeGroupId = 3, + Alias = Constants.Conventions.Media.Extension, + Name = "Type", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing + }); + } - if (_database.Exists(1035)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 541, NodeId = 1035, Alias = Cms.Core.Constants.Conventions.MediaTypes.AudioAlias, Icon = Cms.Core.Constants.Icons.MediaAudio, Thumbnail = Cms.Core.Constants.Icons.MediaAudio, AllowAtRoot = true, Variations = (byte)ContentVariation.Nothing }); - } + if (_database.Exists(4)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 24, + UniqueId = 24.ToGuid(), + DataTypeId = Constants.DataTypes.Upload, + ContentTypeId = 1033, + PropertyTypeGroupId = 4, + Alias = Constants.Conventions.Media.File, + Name = "File", + SortOrder = 0, + Mandatory = true, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 25, + UniqueId = 25.ToGuid(), + DataTypeId = -92, + ContentTypeId = 1033, + PropertyTypeGroupId = 4, + Alias = Constants.Conventions.Media.Extension, + Name = "Type", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 26, + UniqueId = 26.ToGuid(), + DataTypeId = Constants.DataTypes.LabelBigint, + ContentTypeId = 1033, + PropertyTypeGroupId = 4, + Alias = Constants.Conventions.Media.Bytes, + Name = "Size", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = "in bytes", + Variations = (byte)ContentVariation.Nothing + }); + } - if (_database.Exists(1036)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 542, NodeId = 1036, Alias = Cms.Core.Constants.Conventions.MediaTypes.ArticleAlias, Icon = Cms.Core.Constants.Icons.MediaArticle, Thumbnail = Cms.Core.Constants.Icons.MediaArticle, AllowAtRoot = true, Variations = (byte)ContentVariation.Nothing }); - } + if (_database.Exists(52)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 40, + UniqueId = 40.ToGuid(), + DataTypeId = Constants.DataTypes.UploadVideo, + ContentTypeId = 1034, + PropertyTypeGroupId = 52, + Alias = Constants.Conventions.Media.File, + Name = "Video", + SortOrder = 0, + Mandatory = true, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 41, + UniqueId = 41.ToGuid(), + DataTypeId = -92, + ContentTypeId = 1034, + PropertyTypeGroupId = 52, + Alias = Constants.Conventions.Media.Extension, + Name = "Type", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 42, + UniqueId = 42.ToGuid(), + DataTypeId = Constants.DataTypes.LabelBigint, + ContentTypeId = 1034, + PropertyTypeGroupId = 52, + Alias = Constants.Conventions.Media.Bytes, + Name = "Size", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = "in bytes", + Variations = (byte)ContentVariation.Nothing + }); + } - if (_database.Exists(1037)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 543, NodeId = 1037, Alias = Cms.Core.Constants.Conventions.MediaTypes.VectorGraphicsAlias, Icon = Cms.Core.Constants.Icons.MediaVectorGraphics, Thumbnail = Cms.Core.Constants.Icons.MediaVectorGraphics, AllowAtRoot = true, Variations = (byte)ContentVariation.Nothing }); - } + if (_database.Exists(53)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 43, + UniqueId = 43.ToGuid(), + DataTypeId = Constants.DataTypes.UploadAudio, + ContentTypeId = 1035, + PropertyTypeGroupId = 53, + Alias = Constants.Conventions.Media.File, + Name = "Audio", + SortOrder = 0, + Mandatory = true, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 44, + UniqueId = 44.ToGuid(), + DataTypeId = -92, + ContentTypeId = 1035, + PropertyTypeGroupId = 53, + Alias = Constants.Conventions.Media.Extension, + Name = "Type", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 45, + UniqueId = 45.ToGuid(), + DataTypeId = Constants.DataTypes.LabelBigint, + ContentTypeId = 1035, + PropertyTypeGroupId = 53, + Alias = Constants.Conventions.Media.Bytes, + Name = "Size", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = "in bytes", + Variations = (byte)ContentVariation.Nothing + }); + } - // Member type. - if (_database.Exists(1044)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 531, NodeId = 1044, Alias = Cms.Core.Constants.Conventions.MemberTypes.DefaultAlias, Icon = Cms.Core.Constants.Icons.Member, Thumbnail = Cms.Core.Constants.Icons.Member, Variations = (byte)ContentVariation.Nothing }); - } + if (_database.Exists(54)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 46, + UniqueId = 46.ToGuid(), + DataTypeId = Constants.DataTypes.UploadArticle, + ContentTypeId = 1036, + PropertyTypeGroupId = 54, + Alias = Constants.Conventions.Media.File, + Name = "Article", + SortOrder = 0, + Mandatory = true, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 47, + UniqueId = 47.ToGuid(), + DataTypeId = -92, + ContentTypeId = 1036, + PropertyTypeGroupId = 54, + Alias = Constants.Conventions.Media.Extension, + Name = "Type", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 48, + UniqueId = 48.ToGuid(), + DataTypeId = Constants.DataTypes.LabelBigint, + ContentTypeId = 1036, + PropertyTypeGroupId = 54, + Alias = Constants.Conventions.Media.Bytes, + Name = "Size", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = "in bytes", + Variations = (byte)ContentVariation.Nothing + }); } - private void CreateUserData() + if (_database.Exists(55)) { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.User, "id", false, new UserDto { Id = Cms.Core.Constants.Security.SuperUserId, Disabled = false, NoConsole = false, UserName = "Administrator", Login = "admin", Password = "default", Email = string.Empty, UserLanguage = "en-US", CreateDate = DateTime.Now, UpdateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 49, + UniqueId = 49.ToGuid(), + DataTypeId = Constants.DataTypes.UploadVectorGraphics, + ContentTypeId = 1037, + PropertyTypeGroupId = 55, + Alias = Constants.Conventions.Media.File, + Name = "Vector Graphics", + SortOrder = 0, + Mandatory = true, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 50, + UniqueId = 50.ToGuid(), + DataTypeId = -92, + ContentTypeId = 1037, + PropertyTypeGroupId = 55, + Alias = Constants.Conventions.Media.Extension, + Name = "Type", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 51, + UniqueId = 51.ToGuid(), + DataTypeId = Constants.DataTypes.LabelBigint, + ContentTypeId = 1037, + PropertyTypeGroupId = 55, + Alias = Constants.Conventions.Media.Bytes, + Name = "Size", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = "in bytes", + Variations = (byte)ContentVariation.Nothing + }); } - private void CreateUserGroupData() + // Membership property types. + if (_database.Exists(11)) { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup, "id", false, new UserGroupDto { Id = 1, StartMediaId = -1, StartContentId = -1, Alias = Cms.Core.Constants.Security.AdminGroupAlias, Name = "Administrators", DefaultPermissions = "CADMOSKTPIURZ:5F7ïN", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-medal" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup, "id", false, new UserGroupDto { Id = 2, StartMediaId = -1, StartContentId = -1, Alias = Cms.Core.Constants.Security.WriterGroupAlias, Name = "Writers", DefaultPermissions = "CAH:FN", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-edit" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup, "id", false, new UserGroupDto { Id = 3, StartMediaId = -1, StartContentId = -1, Alias = Cms.Core.Constants.Security.EditorGroupAlias, Name = "Editors", DefaultPermissions = "CADMOSKTPUZ:5FïN", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-tools" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup, "id", false, new UserGroupDto { Id = 4, StartMediaId = -1, StartContentId = -1, Alias = Cms.Core.Constants.Security.TranslatorGroupAlias, Name = "Translators", DefaultPermissions = "AF", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-globe" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup, "id", false, new UserGroupDto { Id = 5, StartMediaId = -1, StartContentId = -1, Alias = Cms.Core.Constants.Security.SensitiveDataGroupAlias, Name = "Sensitive data", DefaultPermissions = string.Empty, CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-lock" }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 28, + UniqueId = 28.ToGuid(), + DataTypeId = Constants.DataTypes.Textarea, + ContentTypeId = 1044, + PropertyTypeGroupId = 11, + Alias = Constants.Conventions.Member.Comments, + Name = Constants.Conventions.Member.CommentsLabel, + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing + }); } + } - private void CreateUser2UserGroupData() - { - _database.Insert(new User2UserGroupDto { UserGroupId = 1, UserId = Cms.Core.Constants.Security.SuperUserId }); // add super to admins - _database.Insert(new User2UserGroupDto { UserGroupId = 5, UserId = Cms.Core.Constants.Security.SuperUserId }); // add super to sensitive data - } + private void CreateLanguageData() => + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.Languages, + "en-us", + new LanguageDto {Id = 1, IsoCode = "en-US", CultureName = "English (United States)", IsDefault = true}, + Constants.DatabaseSchema.Tables.Language, + "id"); - private void CreateUserGroup2AppData() + private void CreateContentChildTypeData() + { + // Insert data if the corresponding Node records exist (which may or may not have been created depending on configuration + // of media types to create). + if (!_database.Exists(1031)) { - _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Cms.Core.Constants.Applications.Content }); - _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Cms.Core.Constants.Applications.Packages }); - _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Cms.Core.Constants.Applications.Media }); - _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Cms.Core.Constants.Applications.Members }); - _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Cms.Core.Constants.Applications.Settings }); - _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Cms.Core.Constants.Applications.Users }); - _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Cms.Core.Constants.Applications.Forms }); - _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Cms.Core.Constants.Applications.Translation }); - - _database.Insert(new UserGroup2AppDto { UserGroupId = 2, AppAlias = Cms.Core.Constants.Applications.Content }); - - _database.Insert(new UserGroup2AppDto { UserGroupId = 3, AppAlias = Cms.Core.Constants.Applications.Content }); - _database.Insert(new UserGroup2AppDto { UserGroupId = 3, AppAlias = Cms.Core.Constants.Applications.Media }); - _database.Insert(new UserGroup2AppDto { UserGroupId = 3, AppAlias = Cms.Core.Constants.Applications.Forms }); - - _database.Insert(new UserGroup2AppDto { UserGroupId = 4, AppAlias = Cms.Core.Constants.Applications.Translation }); + return; } - private void CreatePropertyTypeGroupData() - { - // Insert property groups only if the corresponding content type node record exists (which may or may not have been created depending on configuration - // of media or member types to create). - - // Media property groups. - if (_database.Exists(1032)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 3, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.Image), ContentTypeNodeId = 1032, Text = "Image", Alias = "image", SortOrder = 1 }); - } - - if (_database.Exists(1033)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 4, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.File), ContentTypeNodeId = 1033, Text = "File", Alias = "file", SortOrder = 1, }); - } - - if (_database.Exists(1034)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 52, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.Video), ContentTypeNodeId = 1034, Text = "Video", Alias = "video", SortOrder = 1 }); - } - - if (_database.Exists(1035)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 53, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.Audio), ContentTypeNodeId = 1035, Text = "Audio", Alias = "audio", SortOrder = 1 }); - } - - if (_database.Exists(1036)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 54, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.Article), ContentTypeNodeId = 1036, Text = "Article", Alias = "article", SortOrder = 1 }); - } - - if (_database.Exists(1037)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 55, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.VectorGraphics), ContentTypeNodeId = 1037, Text = "Vector Graphics", Alias = "vectorGraphics", SortOrder = 1 }); - } + _database.Insert(Constants.DatabaseSchema.Tables.ContentChildType, "Id", false, + new ContentTypeAllowedContentTypeDto {Id = 1031, AllowedId = 1031}); - // Membership property group. - if (_database.Exists(1044)) + for (var i = 1032; i <= 1037; i++) + { + if (_database.Exists(i)) { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 11, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.Membership), ContentTypeNodeId = 1044, Text = Cms.Core.Constants.Conventions.Member.StandardPropertiesGroupName, Alias = Cms.Core.Constants.Conventions.Member.StandardPropertiesGroupAlias, SortOrder = 1 }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentChildType, "Id", false, + new ContentTypeAllowedContentTypeDto {Id = 1031, AllowedId = i}); } } + } - private void CreatePropertyTypeData() + private void CreateDataTypeData() + { + void InsertDataTypeDto(int id, string editorAlias, string dbType, string? configuration = null) { - // Insert property types only if the corresponding property group record exists (which may or may not have been created depending on configuration - // of media or member types to create). - - // Media property types. - if (_database.Exists(3)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 6, UniqueId = 6.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.ImageCropper, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Cms.Core.Constants.Conventions.Media.File, Name = "Image", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 7, UniqueId = 7.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.LabelInt, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Cms.Core.Constants.Conventions.Media.Width, Name = "Width", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in pixels", Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 8, UniqueId = 8.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.LabelInt, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Cms.Core.Constants.Conventions.Media.Height, Name = "Height", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in pixels", Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 9, UniqueId = 9.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.LabelBigint, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Cms.Core.Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 10, UniqueId = 10.ToGuid(), DataTypeId = -92, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Cms.Core.Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - } - - if (_database.Exists(4)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 24, UniqueId = 24.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.Upload, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Cms.Core.Constants.Conventions.Media.File, Name = "File", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 25, UniqueId = 25.ToGuid(), DataTypeId = -92, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Cms.Core.Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 26, UniqueId = 26.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.LabelBigint, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Cms.Core.Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte)ContentVariation.Nothing }); - } - - if (_database.Exists(52)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 40, UniqueId = 40.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.UploadVideo, ContentTypeId = 1034, PropertyTypeGroupId = 52, Alias = Cms.Core.Constants.Conventions.Media.File, Name = "Video", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 41, UniqueId = 41.ToGuid(), DataTypeId = -92, ContentTypeId = 1034, PropertyTypeGroupId = 52, Alias = Cms.Core.Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 42, UniqueId = 42.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.LabelBigint, ContentTypeId = 1034, PropertyTypeGroupId = 52, Alias = Cms.Core.Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte)ContentVariation.Nothing }); - } + var dataTypeDto = new DataTypeDto {NodeId = id, EditorAlias = editorAlias, DbType = dbType}; - if (_database.Exists(53)) + if (configuration != null) { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 43, UniqueId = 43.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.UploadAudio, ContentTypeId = 1035, PropertyTypeGroupId = 53, Alias = Cms.Core.Constants.Conventions.Media.File, Name = "Audio", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 44, UniqueId = 44.ToGuid(), DataTypeId = -92, ContentTypeId = 1035, PropertyTypeGroupId = 53, Alias = Cms.Core.Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 45, UniqueId = 45.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.LabelBigint, ContentTypeId = 1035, PropertyTypeGroupId = 53, Alias = Cms.Core.Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte)ContentVariation.Nothing }); + dataTypeDto.Configuration = configuration; } - if (_database.Exists(54)) + if (_database.Exists(id)) { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 46, UniqueId = 46.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.UploadArticle, ContentTypeId = 1036, PropertyTypeGroupId = 54, Alias = Cms.Core.Constants.Conventions.Media.File, Name = "Article", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 47, UniqueId = 47.ToGuid(), DataTypeId = -92, ContentTypeId = 1036, PropertyTypeGroupId = 54, Alias = Cms.Core.Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 48, UniqueId = 48.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.LabelBigint, ContentTypeId = 1036, PropertyTypeGroupId = 54, Alias = Cms.Core.Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte)ContentVariation.Nothing }); - } - - if (_database.Exists(55)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 49, UniqueId = 49.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.UploadVectorGraphics, ContentTypeId = 1037, PropertyTypeGroupId = 55, Alias = Cms.Core.Constants.Conventions.Media.File, Name = "Vector Graphics", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 50, UniqueId = 50.ToGuid(), DataTypeId = -92, ContentTypeId = 1037, PropertyTypeGroupId = 55, Alias = Cms.Core.Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 51, UniqueId = 51.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.LabelBigint, ContentTypeId = 1037, PropertyTypeGroupId = 55, Alias = Cms.Core.Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte)ContentVariation.Nothing }); - } - - // Membership property types. - if (_database.Exists(11)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, - new PropertyTypeDto - { - Id = 28, UniqueId = 28.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.Textarea, - ContentTypeId = 1044, PropertyTypeGroupId = 11, - Alias = Cms.Core.Constants.Conventions.Member.Comments, - Name = Cms.Core.Constants.Conventions.Member.CommentsLabel, SortOrder = 0, Mandatory = false, - ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing - }); + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, dataTypeDto); } } - private void CreateLanguageData() => - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.Languages, - "en-us", - new LanguageDto { Id = 1, IsoCode = "en-US", CultureName = "English (United States)", IsDefault = true }, - Cms.Core.Constants.DatabaseSchema.Tables.Language, - "id"); + //layouts for the list view + const string cardLayout = + "{\"name\": \"Grid\",\"path\": \"views/propertyeditors/listview/layouts/grid/grid.html\", \"icon\": \"icon-thumbnails-small\", \"isSystem\": 1, \"selected\": true}"; + const string listLayout = + "{\"name\": \"List\",\"path\": \"views/propertyeditors/listview/layouts/list/list.html\",\"icon\": \"icon-list\", \"isSystem\": 1,\"selected\": true}"; + const string layouts = "[" + cardLayout + "," + listLayout + "]"; - private void CreateContentChildTypeData() + // Insert data types only if the corresponding Node record exists (which may or may not have been created depending on configuration + // of data types to create). + if (_database.Exists(Constants.DataTypes.Boolean)) { - // Insert data if the corresponding Node records exist (which may or may not have been created depending on configuration - // of media types to create). - if (!_database.Exists(1031)) - { - return; - } - - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentChildType, "Id", false, new ContentTypeAllowedContentTypeDto { Id = 1031, AllowedId = 1031 }); - - for (int i = 1032; i <= 1037; i++) - { - if (_database.Exists(i)) + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentChildType, "Id", false, new ContentTypeAllowedContentTypeDto { Id = 1031, AllowedId = i }); - } - } + NodeId = Constants.DataTypes.Boolean, + EditorAlias = Constants.PropertyEditors.Aliases.Boolean, + DbType = "Integer" + }); } - private void CreateDataTypeData() + if (_database.Exists(-51)) { - void InsertDataTypeDto(int id, string editorAlias, string dbType, string? configuration = null) - { - var dataTypeDto = new DataTypeDto + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto { - NodeId = id, - EditorAlias = editorAlias, - DbType = dbType - }; + NodeId = -51, EditorAlias = Constants.PropertyEditors.Aliases.Integer, DbType = "Integer" + }); + } - if (configuration != null) + if (_database.Exists(-87)) + { + _database.Insert( + Constants.DatabaseSchema.Tables.DataType, + "pk", + false, + new DataTypeDto { - dataTypeDto.Configuration = configuration; - } + NodeId = -87, + EditorAlias = Constants.PropertyEditors.Aliases.TinyMce, + DbType = "Ntext", + Configuration = + "{\"value\":\",code,undo,redo,cut,copy,mcepasteword,stylepicker,bold,italic,bullist,numlist,outdent,indent,mcelink,unlink,mceinsertanchor,mceimage,umbracomacro,mceinserttable,umbracoembed,mcecharmap,|1|1,2,3,|0|500,400|1049,|true|\"}" + }); + } - if (_database.Exists(id)) + if (_database.Exists(Constants.DataTypes.Textbox)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, dataTypeDto); - } - } - - //layouts for the list view - const string cardLayout = "{\"name\": \"Grid\",\"path\": \"views/propertyeditors/listview/layouts/grid/grid.html\", \"icon\": \"icon-thumbnails-small\", \"isSystem\": 1, \"selected\": true}"; - const string listLayout = "{\"name\": \"List\",\"path\": \"views/propertyeditors/listview/layouts/list/list.html\",\"icon\": \"icon-list\", \"isSystem\": 1,\"selected\": true}"; - const string layouts = "[" + cardLayout + "," + listLayout + "]"; - - // Insert data types only if the corresponding Node record exists (which may or may not have been created depending on configuration - // of data types to create). - if (_database.Exists(Cms.Core.Constants.DataTypes.Boolean)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Cms.Core.Constants.DataTypes.Boolean, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.Boolean, DbType = "Integer" }); - } - - if (_database.Exists(-51)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -51, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.Integer, DbType = "Integer" }); - } - - if (_database.Exists(-87)) - { - _database.Insert( - Cms.Core.Constants.DatabaseSchema.Tables.DataType, - "pk", - false, - new DataTypeDto - { - NodeId = -87, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.TinyMce, - DbType = "Ntext", - Configuration = "{\"value\":\",code,undo,redo,cut,copy,mcepasteword,stylepicker,bold,italic,bullist,numlist,outdent,indent,mcelink,unlink,mceinsertanchor,mceimage,umbracomacro,mceinserttable,umbracoembed,mcecharmap,|1|1,2,3,|0|500,400|1049,|true|\"}" - }); - } - - if (_database.Exists(Cms.Core.Constants.DataTypes.Textbox)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Cms.Core.Constants.DataTypes.Textbox, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.TextBox, DbType = "Nvarchar" }); - } - - if (_database.Exists(Cms.Core.Constants.DataTypes.Textarea)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Cms.Core.Constants.DataTypes.Textarea, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.TextArea, DbType = "Ntext" }); - } + NodeId = Constants.DataTypes.Textbox, + EditorAlias = Constants.PropertyEditors.Aliases.TextBox, + DbType = "Nvarchar" + }); + } - if (_database.Exists(Cms.Core.Constants.DataTypes.Upload)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Cms.Core.Constants.DataTypes.Upload, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.UploadField, DbType = "Nvarchar" }); - } + if (_database.Exists(Constants.DataTypes.Textarea)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = Constants.DataTypes.Textarea, + EditorAlias = Constants.PropertyEditors.Aliases.TextArea, + DbType = "Ntext" + }); + } - InsertDataTypeDto(Cms.Core.Constants.DataTypes.LabelString, Cms.Core.Constants.PropertyEditors.Aliases.Label, "Nvarchar", "{\"umbracoDataValueType\":\"STRING\"}"); - InsertDataTypeDto(Cms.Core.Constants.DataTypes.LabelInt, Cms.Core.Constants.PropertyEditors.Aliases.Label, "Integer", "{\"umbracoDataValueType\":\"INT\"}"); - InsertDataTypeDto(Cms.Core.Constants.DataTypes.LabelBigint, Cms.Core.Constants.PropertyEditors.Aliases.Label, "Nvarchar", "{\"umbracoDataValueType\":\"BIGINT\"}"); - InsertDataTypeDto(Cms.Core.Constants.DataTypes.LabelDateTime, Cms.Core.Constants.PropertyEditors.Aliases.Label, "Date", "{\"umbracoDataValueType\":\"DATETIME\"}"); - InsertDataTypeDto(Cms.Core.Constants.DataTypes.LabelDecimal, Cms.Core.Constants.PropertyEditors.Aliases.Label, "Decimal", "{\"umbracoDataValueType\":\"DECIMAL\"}"); - InsertDataTypeDto(Cms.Core.Constants.DataTypes.LabelTime, Cms.Core.Constants.PropertyEditors.Aliases.Label, "Date", "{\"umbracoDataValueType\":\"TIME\"}"); + if (_database.Exists(Constants.DataTypes.Upload)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = Constants.DataTypes.Upload, + EditorAlias = Constants.PropertyEditors.Aliases.UploadField, + DbType = "Nvarchar" + }); + } - if (_database.Exists(Cms.Core.Constants.DataTypes.DateTime)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Cms.Core.Constants.DataTypes.DateTime, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.DateTime, DbType = "Date" }); - } + InsertDataTypeDto(Constants.DataTypes.LabelString, Constants.PropertyEditors.Aliases.Label, "Nvarchar", + "{\"umbracoDataValueType\":\"STRING\"}"); + InsertDataTypeDto(Constants.DataTypes.LabelInt, Constants.PropertyEditors.Aliases.Label, "Integer", + "{\"umbracoDataValueType\":\"INT\"}"); + InsertDataTypeDto(Constants.DataTypes.LabelBigint, Constants.PropertyEditors.Aliases.Label, "Nvarchar", + "{\"umbracoDataValueType\":\"BIGINT\"}"); + InsertDataTypeDto(Constants.DataTypes.LabelDateTime, Constants.PropertyEditors.Aliases.Label, "Date", + "{\"umbracoDataValueType\":\"DATETIME\"}"); + InsertDataTypeDto(Constants.DataTypes.LabelDecimal, Constants.PropertyEditors.Aliases.Label, "Decimal", + "{\"umbracoDataValueType\":\"DECIMAL\"}"); + InsertDataTypeDto(Constants.DataTypes.LabelTime, Constants.PropertyEditors.Aliases.Label, "Date", + "{\"umbracoDataValueType\":\"TIME\"}"); + + if (_database.Exists(Constants.DataTypes.DateTime)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = Constants.DataTypes.DateTime, + EditorAlias = Constants.PropertyEditors.Aliases.DateTime, + DbType = "Date" + }); + } - if (_database.Exists(-37)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -37, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.ColorPicker, DbType = "Nvarchar" }); - } + if (_database.Exists(-37)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = -37, EditorAlias = Constants.PropertyEditors.Aliases.ColorPicker, DbType = "Nvarchar" + }); + } - InsertDataTypeDto(Cms.Core.Constants.DataTypes.DropDownSingle, Cms.Core.Constants.PropertyEditors.Aliases.DropDownListFlexible, "Nvarchar", "{\"multiple\":false}"); + InsertDataTypeDto(Constants.DataTypes.DropDownSingle, Constants.PropertyEditors.Aliases.DropDownListFlexible, + "Nvarchar", "{\"multiple\":false}"); - if (_database.Exists(-40)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -40, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.RadioButtonList, DbType = "Nvarchar" }); - } + if (_database.Exists(-40)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = -40, + EditorAlias = Constants.PropertyEditors.Aliases.RadioButtonList, + DbType = "Nvarchar" + }); + } - if (_database.Exists(-41)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -41, EditorAlias = "Umbraco.DateTime", DbType = "Date", Configuration = "{\"format\":\"YYYY-MM-DD\"}" }); - } + if (_database.Exists(-41)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = -41, + EditorAlias = "Umbraco.DateTime", + DbType = "Date", + Configuration = "{\"format\":\"YYYY-MM-DD\"}" + }); + } - InsertDataTypeDto(Cms.Core.Constants.DataTypes.DropDownMultiple, Cms.Core.Constants.PropertyEditors.Aliases.DropDownListFlexible, "Nvarchar", "{\"multiple\":true}"); + InsertDataTypeDto(Constants.DataTypes.DropDownMultiple, Constants.PropertyEditors.Aliases.DropDownListFlexible, + "Nvarchar", "{\"multiple\":true}"); - if (_database.Exists(-43)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -43, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.CheckBoxList, DbType = "Nvarchar" }); - } + if (_database.Exists(-43)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = -43, EditorAlias = Constants.PropertyEditors.Aliases.CheckBoxList, DbType = "Nvarchar" + }); + } - if (_database.Exists(Cms.Core.Constants.DataTypes.Tags)) - { - _database.Insert( - Cms.Core.Constants.DatabaseSchema.Tables.DataType, - "pk", - false, - new DataTypeDto - { - NodeId = Cms.Core.Constants.DataTypes.Tags, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.Tags, - DbType = "Ntext", - Configuration = "{\"group\":\"default\", \"storageType\":\"Json\"}" - }); - } + if (_database.Exists(Constants.DataTypes.Tags)) + { + _database.Insert( + Constants.DatabaseSchema.Tables.DataType, + "pk", + false, + new DataTypeDto + { + NodeId = Constants.DataTypes.Tags, + EditorAlias = Constants.PropertyEditors.Aliases.Tags, + DbType = "Ntext", + Configuration = "{\"group\":\"default\", \"storageType\":\"Json\"}" + }); + } - if (_database.Exists(Cms.Core.Constants.DataTypes.ImageCropper)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Cms.Core.Constants.DataTypes.ImageCropper, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.ImageCropper, DbType = "Ntext" }); - } + if (_database.Exists(Constants.DataTypes.ImageCropper)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = Constants.DataTypes.ImageCropper, + EditorAlias = Constants.PropertyEditors.Aliases.ImageCropper, + DbType = "Ntext" + }); + } - if (_database.Exists(Cms.Core.Constants.DataTypes.DefaultContentListView)) - { - _database.Insert( - Cms.Core.Constants.DatabaseSchema.Tables.DataType, - "pk", - false, - new DataTypeDto - { - NodeId = Cms.Core.Constants.DataTypes.DefaultContentListView, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.ListView, - DbType = "Nvarchar", - Configuration = "{\"pageSize\":100, \"orderBy\":\"updateDate\", \"orderDirection\":\"desc\", \"layouts\":" + layouts + ", \"includeProperties\":[{\"alias\":\"updateDate\",\"header\":\"Last edited\",\"isSystem\":1},{\"alias\":\"owner\",\"header\":\"Updated by\",\"isSystem\":1}]}" - }); - } + if (_database.Exists(Constants.DataTypes.DefaultContentListView)) + { + _database.Insert( + Constants.DatabaseSchema.Tables.DataType, + "pk", + false, + new DataTypeDto + { + NodeId = Constants.DataTypes.DefaultContentListView, + EditorAlias = Constants.PropertyEditors.Aliases.ListView, + DbType = "Nvarchar", + Configuration = + "{\"pageSize\":100, \"orderBy\":\"updateDate\", \"orderDirection\":\"desc\", \"layouts\":" + + layouts + + ", \"includeProperties\":[{\"alias\":\"updateDate\",\"header\":\"Last edited\",\"isSystem\":1},{\"alias\":\"owner\",\"header\":\"Updated by\",\"isSystem\":1}]}" + }); + } - if (_database.Exists(Cms.Core.Constants.DataTypes.DefaultMediaListView)) - { - _database.Insert( - Cms.Core.Constants.DatabaseSchema.Tables.DataType, - "pk", - false, - new DataTypeDto - { - NodeId = Cms.Core.Constants.DataTypes.DefaultMediaListView, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.ListView, - DbType = "Nvarchar", - Configuration = "{\"pageSize\":100, \"orderBy\":\"updateDate\", \"orderDirection\":\"desc\", \"layouts\":" + layouts + ", \"includeProperties\":[{\"alias\":\"updateDate\",\"header\":\"Last edited\",\"isSystem\":1},{\"alias\":\"owner\",\"header\":\"Updated by\",\"isSystem\":1}]}" - }); - } + if (_database.Exists(Constants.DataTypes.DefaultMediaListView)) + { + _database.Insert( + Constants.DatabaseSchema.Tables.DataType, + "pk", + false, + new DataTypeDto + { + NodeId = Constants.DataTypes.DefaultMediaListView, + EditorAlias = Constants.PropertyEditors.Aliases.ListView, + DbType = "Nvarchar", + Configuration = + "{\"pageSize\":100, \"orderBy\":\"updateDate\", \"orderDirection\":\"desc\", \"layouts\":" + + layouts + + ", \"includeProperties\":[{\"alias\":\"updateDate\",\"header\":\"Last edited\",\"isSystem\":1},{\"alias\":\"owner\",\"header\":\"Updated by\",\"isSystem\":1}]}" + }); + } - if (_database.Exists(Cms.Core.Constants.DataTypes.DefaultMembersListView)) - { - _database.Insert( - Cms.Core.Constants.DatabaseSchema.Tables.DataType, - "pk", - false, - new DataTypeDto - { - NodeId = Cms.Core.Constants.DataTypes.DefaultMembersListView, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.ListView, - DbType = "Nvarchar", - Configuration = "{\"pageSize\":10, \"orderBy\":\"username\", \"orderDirection\":\"asc\", \"includeProperties\":[{\"alias\":\"username\",\"isSystem\":1},{\"alias\":\"email\",\"isSystem\":1},{\"alias\":\"updateDate\",\"header\":\"Last edited\",\"isSystem\":1}]}" - }); - } + if (_database.Exists(Constants.DataTypes.DefaultMembersListView)) + { + _database.Insert( + Constants.DatabaseSchema.Tables.DataType, + "pk", + false, + new DataTypeDto + { + NodeId = Constants.DataTypes.DefaultMembersListView, + EditorAlias = Constants.PropertyEditors.Aliases.ListView, + DbType = "Nvarchar", + Configuration = + "{\"pageSize\":10, \"orderBy\":\"username\", \"orderDirection\":\"asc\", \"includeProperties\":[{\"alias\":\"username\",\"isSystem\":1},{\"alias\":\"email\",\"isSystem\":1},{\"alias\":\"updateDate\",\"header\":\"Last edited\",\"isSystem\":1}]}" + }); + } - // New UDI pickers with newer Ids - if (_database.Exists(1046)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1046, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.ContentPicker, DbType = "Nvarchar" }); - } + // New UDI pickers with newer Ids + if (_database.Exists(1046)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = 1046, + EditorAlias = Constants.PropertyEditors.Aliases.ContentPicker, + DbType = "Nvarchar" + }); + } - if (_database.Exists(1047)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1047, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.MemberPicker, DbType = "Nvarchar" }); - } + if (_database.Exists(1047)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = 1047, EditorAlias = Constants.PropertyEditors.Aliases.MemberPicker, DbType = "Nvarchar" + }); + } - if (_database.Exists(1048)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1048, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker, DbType = "Ntext" }); - } + if (_database.Exists(1048)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = 1048, EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker, DbType = "Ntext" + }); + } - if (_database.Exists(1049)) - { - _database.Insert( - Cms.Core.Constants.DatabaseSchema.Tables.DataType, - "pk", - false, - new DataTypeDto - { - NodeId = 1049, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker, - DbType = "Ntext", - Configuration = "{\"multiPicker\":1}" - }); - } + if (_database.Exists(1049)) + { + _database.Insert( + Constants.DatabaseSchema.Tables.DataType, + "pk", + false, + new DataTypeDto + { + NodeId = 1049, + EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker, + DbType = "Ntext", + Configuration = "{\"multiPicker\":1}" + }); + } - if (_database.Exists(1050)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1050, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.MultiUrlPicker, DbType = "Ntext" }); - } + if (_database.Exists(1050)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = 1050, EditorAlias = Constants.PropertyEditors.Aliases.MultiUrlPicker, DbType = "Ntext" + }); + } - if (_database.Exists(Cms.Core.Constants.DataTypes.UploadVideo)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto + if (_database.Exists(Constants.DataTypes.UploadVideo)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto { - NodeId = Cms.Core.Constants.DataTypes.UploadVideo, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.UploadField, + NodeId = Constants.DataTypes.UploadVideo, + EditorAlias = Constants.PropertyEditors.Aliases.UploadField, DbType = "Nvarchar", - Configuration = "{\"fileExtensions\":[{\"id\":0, \"value\":\"mp4\"}, {\"id\":1, \"value\":\"webm\"}, {\"id\":2, \"value\":\"ogv\"}]}" + Configuration = + "{\"fileExtensions\":[{\"id\":0, \"value\":\"mp4\"}, {\"id\":1, \"value\":\"webm\"}, {\"id\":2, \"value\":\"ogv\"}]}" }); - } + } - if (_database.Exists(Cms.Core.Constants.DataTypes.UploadAudio)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto + if (_database.Exists(Constants.DataTypes.UploadAudio)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto { - NodeId = Cms.Core.Constants.DataTypes.UploadAudio, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.UploadField, + NodeId = Constants.DataTypes.UploadAudio, + EditorAlias = Constants.PropertyEditors.Aliases.UploadField, DbType = "Nvarchar", - Configuration = "{\"fileExtensions\":[{\"id\":0, \"value\":\"mp3\"}, {\"id\":1, \"value\":\"weba\"}, {\"id\":2, \"value\":\"oga\"}, {\"id\":3, \"value\":\"opus\"}]}" + Configuration = + "{\"fileExtensions\":[{\"id\":0, \"value\":\"mp3\"}, {\"id\":1, \"value\":\"weba\"}, {\"id\":2, \"value\":\"oga\"}, {\"id\":3, \"value\":\"opus\"}]}" }); - } + } - if (_database.Exists(Cms.Core.Constants.DataTypes.UploadArticle)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto + if (_database.Exists(Constants.DataTypes.UploadArticle)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto { - NodeId = Cms.Core.Constants.DataTypes.UploadArticle, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.UploadField, + NodeId = Constants.DataTypes.UploadArticle, + EditorAlias = Constants.PropertyEditors.Aliases.UploadField, DbType = "Nvarchar", - Configuration = "{\"fileExtensions\":[{\"id\":0, \"value\":\"pdf\"}, {\"id\":1, \"value\":\"docx\"}, {\"id\":2, \"value\":\"doc\"}]}" + Configuration = + "{\"fileExtensions\":[{\"id\":0, \"value\":\"pdf\"}, {\"id\":1, \"value\":\"docx\"}, {\"id\":2, \"value\":\"doc\"}]}" }); - } + } - if (_database.Exists(Cms.Core.Constants.DataTypes.UploadVectorGraphics)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto + if (_database.Exists(Constants.DataTypes.UploadVectorGraphics)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto { - NodeId = Cms.Core.Constants.DataTypes.UploadVectorGraphics, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.UploadField, + NodeId = Constants.DataTypes.UploadVectorGraphics, + EditorAlias = Constants.PropertyEditors.Aliases.UploadField, DbType = "Nvarchar", Configuration = "{\"fileExtensions\":[{\"id\":0, \"value\":\"svg\"}]}" }); - } + } - if (_database.Exists(1051)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto + if (_database.Exists(1051)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto { NodeId = 1051, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker3, + EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker3, DbType = "Ntext", Configuration = "{\"multiple\": false, \"validationLimit\":{\"min\":0,\"max\":1}}" }); - } + } - if (_database.Exists(1052)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto + if (_database.Exists(1052)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto { NodeId = 1052, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker3, + EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker3, DbType = "Ntext", Configuration = "{\"multiple\": true}" }); - } + } - if (_database.Exists(1053)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto + if (_database.Exists(1053)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto { NodeId = 1053, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker3, + EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker3, DbType = "Ntext", - Configuration = "{\"filter\":\"" + Cms.Core.Constants.Conventions.MediaTypes.Image + "\", \"multiple\": false, \"validationLimit\":{\"min\":0,\"max\":1}}" + Configuration = "{\"filter\":\"" + Constants.Conventions.MediaTypes.Image + + "\", \"multiple\": false, \"validationLimit\":{\"min\":0,\"max\":1}}" }); - } + } - if (_database.Exists(1054)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto + if (_database.Exists(1054)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto { NodeId = 1054, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker3, + EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker3, DbType = "Ntext", - Configuration = "{\"filter\":\"" + Cms.Core.Constants.Conventions.MediaTypes.Image + "\", \"multiple\": true}" + Configuration = "{\"filter\":\"" + Constants.Conventions.MediaTypes.Image + + "\", \"multiple\": true}" }); - } } + } - private void CreateRelationTypeData() - { - CreateRelationTypeData(1, Cms.Core.Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias, Cms.Core.Constants.Conventions.RelationTypes.RelateDocumentOnCopyName, Cms.Core.Constants.ObjectTypes.Document, Cms.Core.Constants.ObjectTypes.Document, true, false); - CreateRelationTypeData(2, Cms.Core.Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias, Cms.Core.Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteName, Cms.Core.Constants.ObjectTypes.Document, Cms.Core.Constants.ObjectTypes.Document, false, false); - CreateRelationTypeData(3, Cms.Core.Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias, Cms.Core.Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName, Cms.Core.Constants.ObjectTypes.Media, Cms.Core.Constants.ObjectTypes.Media, false, false); - CreateRelationTypeData(4, Cms.Core.Constants.Conventions.RelationTypes.RelatedMediaAlias, Cms.Core.Constants.Conventions.RelationTypes.RelatedMediaName, null, null, false, true); - CreateRelationTypeData(5, Cms.Core.Constants.Conventions.RelationTypes.RelatedDocumentAlias, Cms.Core.Constants.Conventions.RelationTypes.RelatedDocumentName, null, null, false, true); - } + private void CreateRelationTypeData() + { + CreateRelationTypeData(1, Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias, + Constants.Conventions.RelationTypes.RelateDocumentOnCopyName, Constants.ObjectTypes.Document, + Constants.ObjectTypes.Document, true, false); + CreateRelationTypeData(2, Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias, + Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteName, Constants.ObjectTypes.Document, + Constants.ObjectTypes.Document, false, false); + CreateRelationTypeData(3, Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias, + Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName, Constants.ObjectTypes.Media, + Constants.ObjectTypes.Media, false, false); + CreateRelationTypeData(4, Constants.Conventions.RelationTypes.RelatedMediaAlias, + Constants.Conventions.RelationTypes.RelatedMediaName, null, null, false, true); + CreateRelationTypeData(5, Constants.Conventions.RelationTypes.RelatedDocumentAlias, + Constants.Conventions.RelationTypes.RelatedDocumentName, null, null, false, true); + } - private void CreateRelationTypeData(int id, string alias, string name, Guid? parentObjectType, Guid? childObjectType, bool dual, bool isDependency) + private void CreateRelationTypeData(int id, string alias, string name, Guid? parentObjectType, + Guid? childObjectType, bool dual, bool isDependency) + { + var relationType = new RelationTypeDto { - var relationType = new RelationTypeDto { Id = id, Alias = alias, ChildObjectType = childObjectType, ParentObjectType = parentObjectType, Dual = dual, Name = name, IsDependency = isDependency }; - relationType.UniqueId = CreateUniqueRelationTypeId(relationType.Alias, relationType.Name); + Id = id, + Alias = alias, + ChildObjectType = childObjectType, + ParentObjectType = parentObjectType, + Dual = dual, + Name = name, + IsDependency = isDependency + }; + relationType.UniqueId = CreateUniqueRelationTypeId(relationType.Alias, relationType.Name); + + _database.Insert(Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType); + } - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType); - } + internal static Guid CreateUniqueRelationTypeId(string alias, string name) => (alias + "____" + name).ToGuid(); - internal static Guid CreateUniqueRelationTypeId(string alias, string name) - { - return (alias + "____" + name).ToGuid(); - } + private void CreateKeyValueData() + { + // On install, initialize the umbraco migration plan with the final state. + var upgrader = new Upgrader(new UmbracoPlan(_umbracoVersion)); + var stateValueKey = upgrader.StateValueKey; + var finalState = upgrader.Plan.FinalState; - private void CreateKeyValueData() - { - // On install, initialize the umbraco migration plan with the final state. - var upgrader = new Upgrader(new UmbracoPlan(_umbracoVersion)); - var stateValueKey = upgrader.StateValueKey; - var finalState = upgrader.Plan.FinalState; + _database.Insert(Constants.DatabaseSchema.Tables.KeyValue, "key", false, + new KeyValueDto {Key = stateValueKey, Value = finalState, UpdateDate = DateTime.Now}); + } - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.KeyValue, "key", false, new KeyValueDto { Key = stateValueKey, Value = finalState, UpdateDate = DateTime.Now }); - } + private void CreateLogViewerQueryData() + { + LogViewerQueryDto[] defaultData = MigrateLogViewerQueriesFromFileToDb.DefaultLogQueries.ToArray(); - private void CreateLogViewerQueryData() + for (var i = 0; i < defaultData.Length; i++) { - LogViewerQueryDto[] defaultData = MigrateLogViewerQueriesFromFileToDb.DefaultLogQueries.ToArray(); - - for (int i = 0; i < defaultData.Length; i++) - { - LogViewerQueryDto dto = defaultData[i]; - dto.Id = i+1; - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.LogViewerQuery, "id", false, dto); - } + LogViewerQueryDto dto = defaultData[i]; + dto.Id = i + 1; + _database.Insert(Constants.DatabaseSchema.Tables.LogViewerQuery, "id", false, dto); } + } - private void ConditionalInsert( - string configKey, - string id, - TDto dto, - string tableName, - string primaryKeyName, - bool autoIncrement = false) - { - var alwaysInsert = _entitiesToAlwaysCreate.ContainsKey(configKey) && - _entitiesToAlwaysCreate[configKey].InvariantContains(id.ToString()); - - InstallDefaultDataSettings installDefaultDataSettings = _installDefaultDataSettings.Get(configKey); + private void ConditionalInsert( + string configKey, + string id, + TDto dto, + string tableName, + string primaryKeyName, + bool autoIncrement = false) + { + var alwaysInsert = _entitiesToAlwaysCreate.ContainsKey(configKey) && + _entitiesToAlwaysCreate[configKey].InvariantContains(id); - // If there's no configuration, we assume to create. - if (installDefaultDataSettings == null) - { - alwaysInsert = true; - } + InstallDefaultDataSettings installDefaultDataSettings = _installDefaultDataSettings.Get(configKey); - if (!alwaysInsert && installDefaultDataSettings?.InstallData == InstallDefaultDataOption.None) - { - return; - } + // If there's no configuration, we assume to create. + if (installDefaultDataSettings == null) + { + alwaysInsert = true; + } - if (!alwaysInsert && installDefaultDataSettings?.InstallData == InstallDefaultDataOption.Values && !installDefaultDataSettings.Values.InvariantContains(id)) - { - return; - } + if (!alwaysInsert && installDefaultDataSettings?.InstallData == InstallDefaultDataOption.None) + { + return; + } - if (!alwaysInsert && installDefaultDataSettings?.InstallData == InstallDefaultDataOption.ExceptValues && installDefaultDataSettings.Values.InvariantContains(id)) - { - return; - } + if (!alwaysInsert && installDefaultDataSettings?.InstallData == InstallDefaultDataOption.Values && + !installDefaultDataSettings.Values.InvariantContains(id)) + { + return; + } - _database.Insert(tableName, primaryKeyName, autoIncrement, dto); + if (!alwaysInsert && installDefaultDataSettings?.InstallData == InstallDefaultDataOption.ExceptValues && + installDefaultDataSettings.Values.InvariantContains(id)) + { + return; } + + _database.Insert(tableName, primaryKeyName, autoIncrement, dto); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs index d4527909e93b..8c376d3afee7 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; using System.Data.SqlTypes; -using System.Linq; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -20,529 +16,540 @@ using Umbraco.Extensions; using ColumnInfo = Umbraco.Cms.Infrastructure.Persistence.SqlSyntax.ColumnInfo; -namespace Umbraco.Cms.Infrastructure.Migrations.Install +namespace Umbraco.Cms.Infrastructure.Migrations.Install; + +/// +/// Creates the initial database schema during install. +/// +public class DatabaseSchemaCreator { - /// - /// Creates the initial database schema during install. - /// - public class DatabaseSchemaCreator + // all tables, in order + internal static readonly List OrderedTables = new() { - // all tables, in order - internal static readonly List OrderedTables = new() - { - typeof(UserDto), - typeof(NodeDto), - typeof(ContentTypeDto), - typeof(TemplateDto), - typeof(ContentDto), - typeof(ContentVersionDto), - typeof(MediaVersionDto), - typeof(DocumentDto), - typeof(ContentTypeTemplateDto), - typeof(DataTypeDto), - typeof(DictionaryDto), - typeof(LanguageDto), - typeof(LanguageTextDto), - typeof(DomainDto), - typeof(LogDto), - typeof(MacroDto), - typeof(MacroPropertyDto), - typeof(MemberPropertyTypeDto), - typeof(MemberDto), - typeof(Member2MemberGroupDto), - typeof(PropertyTypeGroupDto), - typeof(PropertyTypeDto), - typeof(PropertyDataDto), - typeof(RelationTypeDto), - typeof(RelationDto), - typeof(TagDto), - typeof(TagRelationshipDto), - typeof(ContentType2ContentTypeDto), - typeof(ContentTypeAllowedContentTypeDto), - typeof(User2NodeNotifyDto), - typeof(ServerRegistrationDto), - typeof(AccessDto), - typeof(AccessRuleDto), - typeof(CacheInstructionDto), - typeof(ExternalLoginDto), - typeof(ExternalLoginTokenDto), - typeof(TwoFactorLoginDto), - typeof(RedirectUrlDto), - typeof(LockDto), - typeof(UserGroupDto), - typeof(User2UserGroupDto), - typeof(UserGroup2NodePermissionDto), - typeof(UserGroup2AppDto), - typeof(UserStartNodeDto), - typeof(ContentNuDto), - typeof(DocumentVersionDto), - typeof(KeyValueDto), - typeof(UserLoginDto), - typeof(ConsentDto), - typeof(AuditEntryDto), - typeof(ContentVersionCultureVariationDto), - typeof(DocumentCultureVariationDto), - typeof(ContentScheduleDto), - typeof(LogViewerQueryDto), - typeof(ContentVersionCleanupPolicyDto), - typeof(UserGroup2NodeDto), - typeof(CreatedPackageSchemaDto) - }; - - private readonly IUmbracoDatabase _database; - private readonly IEventAggregator _eventAggregator; - private readonly ILogger _logger; - private readonly ILoggerFactory _loggerFactory; - private readonly IUmbracoVersion _umbracoVersion; - private readonly IOptionsMonitor _defaultDataCreationSettings; - - [Obsolete("Please use constructor taking all parameters. Scheduled for removal in V11.")] - public DatabaseSchemaCreator( - IUmbracoDatabase? database, - ILogger logger, - ILoggerFactory loggerFactory, - IUmbracoVersion umbracoVersion, - IEventAggregator eventAggregator) - : this (database, logger, loggerFactory, umbracoVersion, eventAggregator, StaticServiceProvider.Instance.GetRequiredService>()) - { - } + typeof(UserDto), + typeof(NodeDto), + typeof(ContentTypeDto), + typeof(TemplateDto), + typeof(ContentDto), + typeof(ContentVersionDto), + typeof(MediaVersionDto), + typeof(DocumentDto), + typeof(ContentTypeTemplateDto), + typeof(DataTypeDto), + typeof(DictionaryDto), + typeof(LanguageDto), + typeof(LanguageTextDto), + typeof(DomainDto), + typeof(LogDto), + typeof(MacroDto), + typeof(MacroPropertyDto), + typeof(MemberPropertyTypeDto), + typeof(MemberDto), + typeof(Member2MemberGroupDto), + typeof(PropertyTypeGroupDto), + typeof(PropertyTypeDto), + typeof(PropertyDataDto), + typeof(RelationTypeDto), + typeof(RelationDto), + typeof(TagDto), + typeof(TagRelationshipDto), + typeof(ContentType2ContentTypeDto), + typeof(ContentTypeAllowedContentTypeDto), + typeof(User2NodeNotifyDto), + typeof(ServerRegistrationDto), + typeof(AccessDto), + typeof(AccessRuleDto), + typeof(CacheInstructionDto), + typeof(ExternalLoginDto), + typeof(ExternalLoginTokenDto), + typeof(TwoFactorLoginDto), + typeof(RedirectUrlDto), + typeof(LockDto), + typeof(UserGroupDto), + typeof(User2UserGroupDto), + typeof(UserGroup2NodePermissionDto), + typeof(UserGroup2AppDto), + typeof(UserStartNodeDto), + typeof(ContentNuDto), + typeof(DocumentVersionDto), + typeof(KeyValueDto), + typeof(UserLoginDto), + typeof(ConsentDto), + typeof(AuditEntryDto), + typeof(ContentVersionCultureVariationDto), + typeof(DocumentCultureVariationDto), + typeof(ContentScheduleDto), + typeof(LogViewerQueryDto), + typeof(ContentVersionCleanupPolicyDto), + typeof(UserGroup2NodeDto), + typeof(CreatedPackageSchemaDto) + }; + + private readonly IUmbracoDatabase _database; + private readonly IOptionsMonitor _defaultDataCreationSettings; + private readonly IEventAggregator _eventAggregator; + private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; + private readonly IUmbracoVersion _umbracoVersion; + + [Obsolete("Please use constructor taking all parameters. Scheduled for removal in V11.")] + public DatabaseSchemaCreator( + IUmbracoDatabase? database, + ILogger logger, + ILoggerFactory loggerFactory, + IUmbracoVersion umbracoVersion, + IEventAggregator eventAggregator) + : this(database, logger, loggerFactory, umbracoVersion, eventAggregator, + StaticServiceProvider.Instance.GetRequiredService>()) + { + } - public DatabaseSchemaCreator( - IUmbracoDatabase? database, - ILogger logger, - ILoggerFactory loggerFactory, - IUmbracoVersion umbracoVersion, - IEventAggregator eventAggregator, - IOptionsMonitor defaultDataCreationSettings) + public DatabaseSchemaCreator( + IUmbracoDatabase? database, + ILogger logger, + ILoggerFactory loggerFactory, + IUmbracoVersion umbracoVersion, + IEventAggregator eventAggregator, + IOptionsMonitor defaultDataCreationSettings) + { + _database = database ?? throw new ArgumentNullException(nameof(database)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); + _umbracoVersion = umbracoVersion ?? throw new ArgumentNullException(nameof(umbracoVersion)); + _eventAggregator = eventAggregator; + _defaultDataCreationSettings = defaultDataCreationSettings; + + if (_database?.SqlContext?.SqlSyntax == null) { - _database = database ?? throw new ArgumentNullException(nameof(database)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); - _umbracoVersion = umbracoVersion ?? throw new ArgumentNullException(nameof(umbracoVersion)); - _eventAggregator = eventAggregator; - _defaultDataCreationSettings = defaultDataCreationSettings; - - if (_database?.SqlContext?.SqlSyntax == null) - { - throw new InvalidOperationException("No SqlContext has been assigned to the database"); - } + throw new InvalidOperationException("No SqlContext has been assigned to the database"); } + } - private ISqlSyntaxProvider SqlSyntax => _database.SqlContext.SqlSyntax; + private ISqlSyntaxProvider SqlSyntax => _database.SqlContext.SqlSyntax; - /// - /// Drops all Umbraco tables in the db. - /// - internal void UninstallDatabaseSchema() - { - _logger.LogInformation("Start UninstallDatabaseSchema"); + /// + /// Drops all Umbraco tables in the db. + /// + internal void UninstallDatabaseSchema() + { + _logger.LogInformation("Start UninstallDatabaseSchema"); - foreach (Type table in OrderedTables.AsEnumerable().Reverse()) - { - TableNameAttribute? tableNameAttribute = table.FirstAttribute(); - var tableName = tableNameAttribute == null ? table.Name : tableNameAttribute.Value; + foreach (Type table in OrderedTables.AsEnumerable().Reverse()) + { + TableNameAttribute? tableNameAttribute = table.FirstAttribute(); + var tableName = tableNameAttribute == null ? table.Name : tableNameAttribute.Value; - _logger.LogInformation("Uninstall {TableName}", tableName); + _logger.LogInformation("Uninstall {TableName}", tableName); - try - { - if (TableExists(tableName)) - { - DropTable(tableName); - } - } - catch (Exception ex) + try + { + if (TableExists(tableName)) { - //swallow this for now, not sure how best to handle this with diff databases... though this is internal - // and only used for unit tests. If this fails its because the table doesn't exist... generally! - _logger.LogError(ex, "Could not drop table {TableName}", tableName); + DropTable(tableName); } } + catch (Exception ex) + { + //swallow this for now, not sure how best to handle this with diff databases... though this is internal + // and only used for unit tests. If this fails its because the table doesn't exist... generally! + _logger.LogError(ex, "Could not drop table {TableName}", tableName); + } } + } - /// - /// Initializes the database by creating the umbraco db schema. - /// - /// This needs to execute as part of a transaction. - public void InitializeDatabaseSchema() + /// + /// Initializes the database by creating the umbraco db schema. + /// + /// This needs to execute as part of a transaction. + public void InitializeDatabaseSchema() + { + if (!_database.InTransaction) { - if (!_database.InTransaction) - { - throw new InvalidOperationException("Database is not in a transaction."); - } + throw new InvalidOperationException("Database is not in a transaction."); + } - var eventMessages = new EventMessages(); - var creatingNotification = new DatabaseSchemaCreatingNotification(eventMessages); - FireBeforeCreation(creatingNotification); + var eventMessages = new EventMessages(); + var creatingNotification = new DatabaseSchemaCreatingNotification(eventMessages); + FireBeforeCreation(creatingNotification); - if (creatingNotification.Cancel == false) + if (creatingNotification.Cancel == false) + { + var dataCreation = new DatabaseDataCreator( + _database, _loggerFactory.CreateLogger(), + _umbracoVersion, + _defaultDataCreationSettings); + foreach (Type table in OrderedTables) { - var dataCreation = new DatabaseDataCreator( - _database, _loggerFactory.CreateLogger(), - _umbracoVersion, - _defaultDataCreationSettings); - foreach (Type table in OrderedTables) - { - CreateTable(false, table, dataCreation); - } + CreateTable(false, table, dataCreation); } - - DatabaseSchemaCreatedNotification createdNotification = - new DatabaseSchemaCreatedNotification(eventMessages).WithStateFrom(creatingNotification); - FireAfterCreation(createdNotification); } - /// - /// Validates the schema of the current database. - /// - internal DatabaseSchemaResult ValidateSchema() => ValidateSchema(OrderedTables); + DatabaseSchemaCreatedNotification createdNotification = + new DatabaseSchemaCreatedNotification(eventMessages).WithStateFrom(creatingNotification); + FireAfterCreation(createdNotification); + } - internal DatabaseSchemaResult ValidateSchema(IEnumerable orderedTables) - { - var result = new DatabaseSchemaResult(); + /// + /// Validates the schema of the current database. + /// + internal DatabaseSchemaResult ValidateSchema() => ValidateSchema(OrderedTables); - result.IndexDefinitions.AddRange(SqlSyntax.GetDefinedIndexes(_database) - .Select(x => new DbIndexDefinition(x))); + internal DatabaseSchemaResult ValidateSchema(IEnumerable orderedTables) + { + var result = new DatabaseSchemaResult(); - result.TableDefinitions.AddRange(orderedTables - .Select(x => DefinitionFactory.GetTableDefinition(x, SqlSyntax))); + result.IndexDefinitions.AddRange(SqlSyntax.GetDefinedIndexes(_database) + .Select(x => new DbIndexDefinition(x))); - ValidateDbTables(result); - ValidateDbColumns(result); - ValidateDbIndexes(result); - ValidateDbConstraints(result); + result.TableDefinitions.AddRange(orderedTables + .Select(x => DefinitionFactory.GetTableDefinition(x, SqlSyntax))); - return result; - } + ValidateDbTables(result); + ValidateDbColumns(result); + ValidateDbIndexes(result); + ValidateDbConstraints(result); + + return result; + } - /// - /// This validates the Primary/Foreign keys in the database - /// - /// - /// - /// This does not validate any database constraints that are not PKs or FKs because Umbraco does not create a database - /// with non PK/FK constraints. - /// Any unique "constraints" in the database are done with unique indexes. - /// - private void ValidateDbConstraints(DatabaseSchemaResult result) + /// + /// This validates the Primary/Foreign keys in the database + /// + /// + /// + /// This does not validate any database constraints that are not PKs or FKs because Umbraco does not create a database + /// with non PK/FK constraints. + /// Any unique "constraints" in the database are done with unique indexes. + /// + private void ValidateDbConstraints(DatabaseSchemaResult result) + { + //Check constraints in configured database against constraints in schema + var constraintsInDatabase = SqlSyntax.GetConstraintsPerColumn(_database).DistinctBy(x => x.Item3).ToList(); + var foreignKeysInDatabase = constraintsInDatabase.Where(x => x.Item3.InvariantStartsWith("FK_")) + .Select(x => x.Item3).ToList(); + var primaryKeysInDatabase = constraintsInDatabase.Where(x => x.Item3.InvariantStartsWith("PK_")) + .Select(x => x.Item3).ToList(); + + var unknownConstraintsInDatabase = constraintsInDatabase.Where( + x => x.Item3.InvariantStartsWith("FK_") == false && x.Item3.InvariantStartsWith("PK_") == false && + x.Item3.InvariantStartsWith("IX_") == false + ).Select(x => x.Item3).ToList(); + + var foreignKeysInSchema = result.TableDefinitions.SelectMany(x => x.ForeignKeys.Select(y => y.Name)) + .Where(x => x is not null).ToList(); + var primaryKeysInSchema = result.TableDefinitions.SelectMany(x => x.Columns.Select(y => y.PrimaryKeyName)) + .Where(x => x.IsNullOrWhiteSpace() == false).ToList(); + + // Add valid and invalid foreign key differences to the result object + // We'll need to do invariant contains with case insensitivity because foreign key, primary key is not standardized + // In theory you could have: FK_ or fk_ ...or really any standard that your development department (or developer) chooses to use. + foreach (var unknown in unknownConstraintsInDatabase) { - //Check constraints in configured database against constraints in schema - var constraintsInDatabase = SqlSyntax.GetConstraintsPerColumn(_database).DistinctBy(x => x.Item3).ToList(); - var foreignKeysInDatabase = constraintsInDatabase.Where(x => x.Item3.InvariantStartsWith("FK_")).Select(x => x.Item3).ToList(); - var primaryKeysInDatabase = constraintsInDatabase.Where(x => x.Item3.InvariantStartsWith("PK_")).Select(x => x.Item3).ToList(); - - var unknownConstraintsInDatabase = constraintsInDatabase.Where( - x => x.Item3.InvariantStartsWith("FK_") == false && x.Item3.InvariantStartsWith("PK_") == false && x.Item3.InvariantStartsWith("IX_") == false - ).Select(x => x.Item3).ToList(); - - var foreignKeysInSchema = result.TableDefinitions.SelectMany(x => x.ForeignKeys.Select(y => y.Name)).Where(x => x is not null).ToList(); - var primaryKeysInSchema = result.TableDefinitions.SelectMany(x => x.Columns.Select(y => y.PrimaryKeyName)).Where(x => x.IsNullOrWhiteSpace() == false).ToList(); - - // Add valid and invalid foreign key differences to the result object - // We'll need to do invariant contains with case insensitivity because foreign key, primary key is not standardized - // In theory you could have: FK_ or fk_ ...or really any standard that your development department (or developer) chooses to use. - foreach (var unknown in unknownConstraintsInDatabase) + if (foreignKeysInSchema!.InvariantContains(unknown) || primaryKeysInSchema!.InvariantContains(unknown)) { - if (foreignKeysInSchema!.InvariantContains(unknown) || primaryKeysInSchema!.InvariantContains(unknown)) - { - result.ValidConstraints.Add(unknown); - } - else - { - result.Errors.Add(new Tuple("Unknown", unknown)); - } + result.ValidConstraints.Add(unknown); } - - // Foreign keys: - IEnumerable validForeignKeyDifferences = foreignKeysInDatabase.Intersect(foreignKeysInSchema, StringComparer.InvariantCultureIgnoreCase); - foreach (var foreignKey in validForeignKeyDifferences) + else { - if (foreignKey is not null) - { - result.ValidConstraints.Add(foreignKey); - } + result.Errors.Add(new Tuple("Unknown", unknown)); } + } - IEnumerable invalidForeignKeyDifferences = foreignKeysInDatabase.Except(foreignKeysInSchema, StringComparer.InvariantCultureIgnoreCase) - .Union(foreignKeysInSchema.Except(foreignKeysInDatabase, StringComparer.InvariantCultureIgnoreCase)); - foreach (var foreignKey in invalidForeignKeyDifferences) + // Foreign keys: + IEnumerable validForeignKeyDifferences = + foreignKeysInDatabase.Intersect(foreignKeysInSchema, StringComparer.InvariantCultureIgnoreCase); + foreach (var foreignKey in validForeignKeyDifferences) + { + if (foreignKey is not null) { - result.Errors.Add(new Tuple("Constraint", foreignKey ?? "NULL")); + result.ValidConstraints.Add(foreignKey); } + } - // Primary keys: - // Add valid and invalid primary key differences to the result object - IEnumerable validPrimaryKeyDifferences = primaryKeysInDatabase!.Intersect(primaryKeysInSchema, StringComparer.InvariantCultureIgnoreCase)!; - foreach (var primaryKey in validPrimaryKeyDifferences) - { - result.ValidConstraints.Add(primaryKey); - } + IEnumerable invalidForeignKeyDifferences = foreignKeysInDatabase + .Except(foreignKeysInSchema, StringComparer.InvariantCultureIgnoreCase) + .Union(foreignKeysInSchema.Except(foreignKeysInDatabase, StringComparer.InvariantCultureIgnoreCase)); + foreach (var foreignKey in invalidForeignKeyDifferences) + { + result.Errors.Add(new Tuple("Constraint", foreignKey ?? "NULL")); + } - IEnumerable invalidPrimaryKeyDifferences = primaryKeysInDatabase!.Except(primaryKeysInSchema, StringComparer.InvariantCultureIgnoreCase)! - .Union(primaryKeysInSchema.Except(primaryKeysInDatabase, StringComparer.InvariantCultureIgnoreCase))!; - foreach (var primaryKey in invalidPrimaryKeyDifferences) - { - result.Errors.Add(new Tuple("Constraint", primaryKey)); - } + // Primary keys: + // Add valid and invalid primary key differences to the result object + IEnumerable validPrimaryKeyDifferences = + primaryKeysInDatabase!.Intersect(primaryKeysInSchema, StringComparer.InvariantCultureIgnoreCase)!; + foreach (var primaryKey in validPrimaryKeyDifferences) + { + result.ValidConstraints.Add(primaryKey); } - private void ValidateDbColumns(DatabaseSchemaResult result) + IEnumerable invalidPrimaryKeyDifferences = + primaryKeysInDatabase!.Except(primaryKeysInSchema, StringComparer.InvariantCultureIgnoreCase)! + .Union(primaryKeysInSchema.Except(primaryKeysInDatabase, StringComparer.InvariantCultureIgnoreCase))!; + foreach (var primaryKey in invalidPrimaryKeyDifferences) { - //Check columns in configured database against columns in schema - IEnumerable columnsInDatabase = SqlSyntax.GetColumnsInSchema(_database); - var columnsPerTableInDatabase = - columnsInDatabase.Select(x => string.Concat(x.TableName, ",", x.ColumnName)).ToList(); - var columnsPerTableInSchema = result.TableDefinitions - .SelectMany(x => x.Columns.Select(y => string.Concat(y.TableName, ",", y.Name))).ToList(); - //Add valid and invalid column differences to the result object - IEnumerable validColumnDifferences = - columnsPerTableInDatabase.Intersect(columnsPerTableInSchema, StringComparer.InvariantCultureIgnoreCase); - foreach (var column in validColumnDifferences) - { - result.ValidColumns.Add(column); - } + result.Errors.Add(new Tuple("Constraint", primaryKey)); + } + } - IEnumerable invalidColumnDifferences = - columnsPerTableInDatabase.Except(columnsPerTableInSchema, StringComparer.InvariantCultureIgnoreCase) - .Union(columnsPerTableInSchema.Except(columnsPerTableInDatabase, - StringComparer.InvariantCultureIgnoreCase)); - foreach (var column in invalidColumnDifferences) - { - result.Errors.Add(new Tuple("Column", column)); - } + private void ValidateDbColumns(DatabaseSchemaResult result) + { + //Check columns in configured database against columns in schema + IEnumerable columnsInDatabase = SqlSyntax.GetColumnsInSchema(_database); + var columnsPerTableInDatabase = + columnsInDatabase.Select(x => string.Concat(x.TableName, ",", x.ColumnName)).ToList(); + var columnsPerTableInSchema = result.TableDefinitions + .SelectMany(x => x.Columns.Select(y => string.Concat(y.TableName, ",", y.Name))).ToList(); + //Add valid and invalid column differences to the result object + IEnumerable validColumnDifferences = + columnsPerTableInDatabase.Intersect(columnsPerTableInSchema, StringComparer.InvariantCultureIgnoreCase); + foreach (var column in validColumnDifferences) + { + result.ValidColumns.Add(column); } - private void ValidateDbTables(DatabaseSchemaResult result) + IEnumerable invalidColumnDifferences = + columnsPerTableInDatabase.Except(columnsPerTableInSchema, StringComparer.InvariantCultureIgnoreCase) + .Union(columnsPerTableInSchema.Except(columnsPerTableInDatabase, + StringComparer.InvariantCultureIgnoreCase)); + foreach (var column in invalidColumnDifferences) { - //Check tables in configured database against tables in schema - var tablesInDatabase = SqlSyntax.GetTablesInSchema(_database).ToList(); - var tablesInSchema = result.TableDefinitions.Select(x => x.Name).ToList(); - //Add valid and invalid table differences to the result object - IEnumerable validTableDifferences = - tablesInDatabase.Intersect(tablesInSchema, StringComparer.InvariantCultureIgnoreCase); - foreach (var tableName in validTableDifferences) - { - if (tableName is not null) - { - result.ValidTables.Add(tableName); - } - } + result.Errors.Add(new Tuple("Column", column)); + } + } - IEnumerable invalidTableDifferences = - tablesInDatabase.Except(tablesInSchema, StringComparer.InvariantCultureIgnoreCase) - .Union(tablesInSchema.Except(tablesInDatabase, StringComparer.InvariantCultureIgnoreCase)); - foreach (var tableName in invalidTableDifferences) + private void ValidateDbTables(DatabaseSchemaResult result) + { + //Check tables in configured database against tables in schema + var tablesInDatabase = SqlSyntax.GetTablesInSchema(_database).ToList(); + var tablesInSchema = result.TableDefinitions.Select(x => x.Name).ToList(); + //Add valid and invalid table differences to the result object + IEnumerable validTableDifferences = + tablesInDatabase.Intersect(tablesInSchema, StringComparer.InvariantCultureIgnoreCase); + foreach (var tableName in validTableDifferences) + { + if (tableName is not null) { - result.Errors.Add(new Tuple("Table", tableName ?? "NULL")); + result.ValidTables.Add(tableName); } } - private void ValidateDbIndexes(DatabaseSchemaResult result) + IEnumerable invalidTableDifferences = + tablesInDatabase.Except(tablesInSchema, StringComparer.InvariantCultureIgnoreCase) + .Union(tablesInSchema.Except(tablesInDatabase, StringComparer.InvariantCultureIgnoreCase)); + foreach (var tableName in invalidTableDifferences) { - //These are just column indexes NOT constraints or Keys - //var colIndexesInDatabase = result.DbIndexDefinitions.Where(x => x.IndexName.InvariantStartsWith("IX_")).Select(x => x.IndexName).ToList(); - var colIndexesInDatabase = result.IndexDefinitions.Select(x => x.IndexName).ToList(); - var indexesInSchema = result.TableDefinitions.SelectMany(x => x.Indexes.Select(y => y.Name)).ToList(); - - //Add valid and invalid index differences to the result object - IEnumerable validColIndexDifferences = - colIndexesInDatabase.Intersect(indexesInSchema, StringComparer.InvariantCultureIgnoreCase); - foreach (var index in validColIndexDifferences) - { - if (index is not null) - { - result.ValidIndexes.Add(index); - } - } + result.Errors.Add(new Tuple("Table", tableName ?? "NULL")); + } + } - IEnumerable invalidColIndexDifferences = - colIndexesInDatabase.Except(indexesInSchema, StringComparer.InvariantCultureIgnoreCase) - .Union(indexesInSchema.Except(colIndexesInDatabase, StringComparer.InvariantCultureIgnoreCase)); - foreach (var index in invalidColIndexDifferences) + private void ValidateDbIndexes(DatabaseSchemaResult result) + { + //These are just column indexes NOT constraints or Keys + //var colIndexesInDatabase = result.DbIndexDefinitions.Where(x => x.IndexName.InvariantStartsWith("IX_")).Select(x => x.IndexName).ToList(); + var colIndexesInDatabase = result.IndexDefinitions.Select(x => x.IndexName).ToList(); + var indexesInSchema = result.TableDefinitions.SelectMany(x => x.Indexes.Select(y => y.Name)).ToList(); + + //Add valid and invalid index differences to the result object + IEnumerable validColIndexDifferences = + colIndexesInDatabase.Intersect(indexesInSchema, StringComparer.InvariantCultureIgnoreCase); + foreach (var index in validColIndexDifferences) + { + if (index is not null) { - result.Errors.Add(new Tuple("Index", index ?? "NULL")); + result.ValidIndexes.Add(index); } } - #region Notifications - - /// - /// Publishes the notification. - /// - /// Cancelable notification marking the creation having begun. - internal virtual void FireBeforeCreation(DatabaseSchemaCreatingNotification notification) => - _eventAggregator.Publish(notification); - - /// - /// Publishes the notification. - /// - /// Notification marking the creation having completed. - internal virtual void FireAfterCreation(DatabaseSchemaCreatedNotification notification) => - _eventAggregator.Publish(notification); - - #endregion - - #region Utilities - - /// - /// Returns whether a table with the specified exists in the database. - /// - /// The name of the table. - /// true if the table exists; otherwise false. - /// - /// - /// if (schemaHelper.TableExist("MyTable")) - /// { - /// // do something when the table exists - /// } - /// - /// - public bool TableExists(string? tableName) => tableName is not null && SqlSyntax.DoesTableExist(_database, tableName); - - /// - /// Returns whether the table for the specified exists in the database. - /// - /// The type representing the DTO/table. - /// true if the table exists; otherwise false. - /// - /// - /// if (schemaHelper.TableExist<MyDto>) - /// { - /// // do something when the table exists - /// } - /// - /// - /// - /// If has been decorated with an , the name from that - /// attribute will be used for the table name. If the attribute is not present, the name - /// will be used instead. - /// - public bool TableExists() + IEnumerable invalidColIndexDifferences = + colIndexesInDatabase.Except(indexesInSchema, StringComparer.InvariantCultureIgnoreCase) + .Union(indexesInSchema.Except(colIndexesInDatabase, StringComparer.InvariantCultureIgnoreCase)); + foreach (var index in invalidColIndexDifferences) { - TableDefinition table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); - return table != null && TableExists(table.Name); + result.Errors.Add(new Tuple("Index", index ?? "NULL")); } + } + + #region Notifications + + /// + /// Publishes the notification. + /// + /// Cancelable notification marking the creation having begun. + internal virtual void FireBeforeCreation(DatabaseSchemaCreatingNotification notification) => + _eventAggregator.Publish(notification); + + /// + /// Publishes the notification. + /// + /// Notification marking the creation having completed. + internal virtual void FireAfterCreation(DatabaseSchemaCreatedNotification notification) => + _eventAggregator.Publish(notification); + + #endregion + + #region Utilities + + /// + /// Returns whether a table with the specified exists in the database. + /// + /// The name of the table. + /// true if the table exists; otherwise false. + /// + /// + /// if (schemaHelper.TableExist("MyTable")) + /// { + /// // do something when the table exists + /// } + /// + /// + public bool TableExists(string? tableName) => + tableName is not null && SqlSyntax.DoesTableExist(_database, tableName); + + /// + /// Returns whether the table for the specified exists in the database. + /// + /// The type representing the DTO/table. + /// true if the table exists; otherwise false. + /// + /// + /// if (schemaHelper.TableExist<MyDto>) + /// { + /// // do something when the table exists + /// } + /// + /// + /// + /// If has been decorated with an , the name from that + /// attribute will be used for the table name. If the attribute is not present, the name + /// will be used instead. + /// + public bool TableExists() + { + TableDefinition table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); + return table != null && TableExists(table.Name); + } + + /// + /// Creates a new table in the database based on the type of . + /// + /// The type representing the DTO/table. + /// Whether the table should be overwritten if it already exists. + /// + /// If has been decorated with an , the name from that + /// attribute will be used for the table name. If the attribute is not present, the name + /// will be used instead. + /// If a table with the same name already exists, the parameter will determine + /// whether the table is overwritten. If true, the table will be overwritten, whereas this method will + /// not do anything if the parameter is false. + /// + internal void CreateTable(bool overwrite = false) + where T : new() + { + Type tableType = typeof(T); + CreateTable( + overwrite, + tableType, + new DatabaseDataCreator( + _database, + _loggerFactory.CreateLogger(), + _umbracoVersion, + _defaultDataCreationSettings)); + } - /// - /// Creates a new table in the database based on the type of . - /// - /// The type representing the DTO/table. - /// Whether the table should be overwritten if it already exists. - /// - /// If has been decorated with an , the name from that - /// attribute will be used for the table name. If the attribute is not present, the name - /// will be used instead. - /// If a table with the same name already exists, the parameter will determine - /// whether the table is overwritten. If true, the table will be overwritten, whereas this method will - /// not do anything if the parameter is false. - /// - internal void CreateTable(bool overwrite = false) - where T : new() + /// + /// Creates a new table in the database for the specified . + /// + /// Whether the table should be overwritten if it already exists. + /// The representing the table. + /// + /// + /// If has been decorated with an , the name from + /// that attribute will be used for the table name. If the attribute is not present, the name + /// will be used instead. + /// If a table with the same name already exists, the parameter will determine + /// whether the table is overwritten. If true, the table will be overwritten, whereas this method will + /// not do anything if the parameter is false. + /// This need to execute as part of a transaction. + /// + internal void CreateTable(bool overwrite, Type modelType, DatabaseDataCreator dataCreation) + { + if (!_database.InTransaction) { - Type tableType = typeof(T); - CreateTable( - overwrite, - tableType, - new DatabaseDataCreator( - _database, - _loggerFactory.CreateLogger(), - _umbracoVersion, - _defaultDataCreationSettings)); + throw new InvalidOperationException("Database is not in a transaction."); } - /// - /// Creates a new table in the database for the specified . - /// - /// Whether the table should be overwritten if it already exists. - /// The representing the table. - /// - /// - /// If has been decorated with an , the name from - /// that attribute will be used for the table name. If the attribute is not present, the name - /// will be used instead. - /// If a table with the same name already exists, the parameter will determine - /// whether the table is overwritten. If true, the table will be overwritten, whereas this method will - /// not do anything if the parameter is false. - /// This need to execute as part of a transaction. - /// - internal void CreateTable(bool overwrite, Type modelType, DatabaseDataCreator dataCreation) + TableDefinition tableDefinition = DefinitionFactory.GetTableDefinition(modelType, SqlSyntax); + var tableName = tableDefinition.Name; + var tableExist = TableExists(tableName); + if (string.IsNullOrEmpty(tableName)) { - if (!_database.InTransaction) - { - throw new InvalidOperationException("Database is not in a transaction."); - } + throw new SqlNullValueException("Tablename was null"); + } - TableDefinition tableDefinition = DefinitionFactory.GetTableDefinition(modelType, SqlSyntax); - var tableName = tableDefinition.Name; - var tableExist = TableExists(tableName); - if (string.IsNullOrEmpty(tableName)) - { - throw new SqlNullValueException("Tablename was null"); - } - if (overwrite && tableExist) - { - _logger.LogInformation("Table {TableName} already exists, but will be recreated", tableName); + if (overwrite && tableExist) + { + _logger.LogInformation("Table {TableName} already exists, but will be recreated", tableName); - DropTable(tableName); - tableExist = false; - } + DropTable(tableName); + tableExist = false; + } - if (tableExist) - { - // The table exists and was not recreated/overwritten. - _logger.LogInformation("Table {TableName} already exists - no changes were made", tableName); - return; - } + if (tableExist) + { + // The table exists and was not recreated/overwritten. + _logger.LogInformation("Table {TableName} already exists - no changes were made", tableName); + return; + } - //Execute the Create Table sql - SqlSyntax.HandleCreateTable(_database, tableDefinition); + //Execute the Create Table sql + SqlSyntax.HandleCreateTable(_database, tableDefinition); - if (SqlSyntax.SupportsIdentityInsert() && tableDefinition.Columns.Any(x => x.IsIdentity)) - { - // This should probably delegate to whole thing to the syntax provider - _database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(tableName)} ON ")); - } - - //Call the NewTable-event to trigger the insert of base/default data - //OnNewTable(tableName, _db, e, _logger); + if (SqlSyntax.SupportsIdentityInsert() && tableDefinition.Columns.Any(x => x.IsIdentity)) + { + // This should probably delegate to whole thing to the syntax provider + _database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(tableName)} ON ")); + } - dataCreation.InitializeBaseData(tableName); + //Call the NewTable-event to trigger the insert of base/default data + //OnNewTable(tableName, _db, e, _logger); - if (SqlSyntax.SupportsIdentityInsert() && tableDefinition.Columns.Any(x => x.IsIdentity)) - { - _database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(tableName)} OFF;")); - } + dataCreation.InitializeBaseData(tableName); - if (overwrite) - { - _logger.LogInformation("Table {TableName} was recreated", tableName); - } - else - { - _logger.LogInformation("New table {TableName} was created", tableName); - } + if (SqlSyntax.SupportsIdentityInsert() && tableDefinition.Columns.Any(x => x.IsIdentity)) + { + _database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(tableName)} OFF;")); } - /// - /// Drops the table for the specified . - /// - /// The type representing the DTO/table. - /// - /// - /// schemaHelper.DropTable<MyDto>); - /// - /// - /// - /// If has been decorated with an , the name from that - /// attribute will be used for the table name. If the attribute is not present, the name - /// will be used instead. - /// - public void DropTable(string? tableName) + if (overwrite) { - var sql = new Sql(string.Format(SqlSyntax.DropTable, SqlSyntax.GetQuotedTableName(tableName))); - _database.Execute(sql); + _logger.LogInformation("Table {TableName} was recreated", tableName); } + else + { + _logger.LogInformation("New table {TableName} was created", tableName); + } + } - #endregion + /// + /// Drops the table for the specified . + /// + /// The type representing the DTO/table. + /// + /// + /// schemaHelper.DropTable<MyDto>); + /// + /// + /// + /// If has been decorated with an , the name from that + /// attribute will be used for the table name. If the attribute is not present, the name + /// will be used instead. + /// + public void DropTable(string? tableName) + { + var sql = new Sql(string.Format(SqlSyntax.DropTable, SqlSyntax.GetQuotedTableName(tableName))); + _database.Execute(sql); } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreatorFactory.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreatorFactory.cs index d4d0507c0aaf..6c28f08eb61d 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreatorFactory.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreatorFactory.cs @@ -1,4 +1,3 @@ -using System; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -8,46 +7,44 @@ using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Infrastructure.Migrations.Install -{ - /// - /// Creates the initial database schema during install. - /// - public class DatabaseSchemaCreatorFactory - { - private readonly ILogger _logger; - private readonly ILoggerFactory _loggerFactory; - private readonly IUmbracoVersion _umbracoVersion; - private readonly IEventAggregator _eventAggregator; - private readonly IOptionsMonitor _installDefaultDataSettings; +namespace Umbraco.Cms.Infrastructure.Migrations.Install; - [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in V11.")] - public DatabaseSchemaCreatorFactory( - ILogger logger, - ILoggerFactory loggerFactory, - IUmbracoVersion umbracoVersion, - IEventAggregator eventAggregator) - : this(logger, loggerFactory, umbracoVersion, eventAggregator, StaticServiceProvider.Instance.GetRequiredService>()) - { - } +/// +/// Creates the initial database schema during install. +/// +public class DatabaseSchemaCreatorFactory +{ + private readonly IEventAggregator _eventAggregator; + private readonly IOptionsMonitor _installDefaultDataSettings; + private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; + private readonly IUmbracoVersion _umbracoVersion; - public DatabaseSchemaCreatorFactory( - ILogger logger, - ILoggerFactory loggerFactory, - IUmbracoVersion umbracoVersion, - IEventAggregator eventAggregator, - IOptionsMonitor installDefaultDataSettings) - { - _logger = logger; - _loggerFactory = loggerFactory; - _umbracoVersion = umbracoVersion; - _eventAggregator = eventAggregator; - _installDefaultDataSettings = installDefaultDataSettings; - } + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in V11.")] + public DatabaseSchemaCreatorFactory( + ILogger logger, + ILoggerFactory loggerFactory, + IUmbracoVersion umbracoVersion, + IEventAggregator eventAggregator) + : this(logger, loggerFactory, umbracoVersion, eventAggregator, + StaticServiceProvider.Instance.GetRequiredService>()) + { + } - public DatabaseSchemaCreator Create(IUmbracoDatabase? database) - { - return new DatabaseSchemaCreator(database, _logger, _loggerFactory, _umbracoVersion, _eventAggregator, _installDefaultDataSettings); - } + public DatabaseSchemaCreatorFactory( + ILogger logger, + ILoggerFactory loggerFactory, + IUmbracoVersion umbracoVersion, + IEventAggregator eventAggregator, + IOptionsMonitor installDefaultDataSettings) + { + _logger = logger; + _loggerFactory = loggerFactory; + _umbracoVersion = umbracoVersion; + _eventAggregator = eventAggregator; + _installDefaultDataSettings = installDefaultDataSettings; } + + public DatabaseSchemaCreator Create(IUmbracoDatabase? database) => new DatabaseSchemaCreator(database, _logger, + _loggerFactory, _umbracoVersion, _eventAggregator, _installDefaultDataSettings); } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaResult.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaResult.cs index 83c4fd4cefb9..b6b27282f2ef 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaResult.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaResult.cs @@ -1,103 +1,102 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Text; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Install +namespace Umbraco.Cms.Infrastructure.Migrations.Install; + +/// +/// Represents ... +/// +public class DatabaseSchemaResult { - /// - /// Represents ... - /// - public class DatabaseSchemaResult + public DatabaseSchemaResult() { - public DatabaseSchemaResult() - { - Errors = new List>(); - TableDefinitions = new List(); - ValidTables = new List(); - ValidColumns = new List(); - ValidConstraints = new List(); - ValidIndexes = new List(); - IndexDefinitions = new List(); - } + Errors = new List>(); + TableDefinitions = new List(); + ValidTables = new List(); + ValidColumns = new List(); + ValidConstraints = new List(); + ValidIndexes = new List(); + IndexDefinitions = new List(); + } + + public List> Errors { get; } - public List> Errors { get; } + public List TableDefinitions { get; } - public List TableDefinitions { get; } + // TODO: what are these exactly? TableDefinitions are those that should be there, IndexDefinitions are those that... are in DB? + internal List IndexDefinitions { get; } - // TODO: what are these exactly? TableDefinitions are those that should be there, IndexDefinitions are those that... are in DB? - internal List IndexDefinitions { get; } + public List ValidTables { get; } - public List ValidTables { get; } + public List ValidColumns { get; } - public List ValidColumns { get; } + public List ValidConstraints { get; } + + public List ValidIndexes { get; } + + /// + /// Determines whether the database contains an installed version. + /// + /// + /// A database contains an installed version when it contains at least one valid table. + /// + public bool DetermineHasInstalledVersion() => ValidTables.Count > 0; + + /// + /// Gets a summary of the schema validation result + /// + /// A string containing a human readable string with a summary message + public string GetSummary() + { + var sb = new StringBuilder(); + if (Errors.Any() == false) + { + sb.AppendLine("The database schema validation didn't find any errors."); + return sb.ToString(); + } - public List ValidConstraints { get; } + //Table error summary + if (Errors.Any(x => x.Item1.Equals("Table"))) + { + sb.AppendLine("The following tables were found in the database, but are not in the current schema:"); + sb.AppendLine(string.Join(",", Errors.Where(x => x.Item1.Equals("Table")).Select(x => x.Item2))); + sb.AppendLine(" "); + } - public List ValidIndexes { get; } + //Column error summary + if (Errors.Any(x => x.Item1.Equals("Column"))) + { + sb.AppendLine("The following columns were found in the database, but are not in the current schema:"); + sb.AppendLine(string.Join(",", Errors.Where(x => x.Item1.Equals("Column")).Select(x => x.Item2))); + sb.AppendLine(" "); + } - /// - /// Determines whether the database contains an installed version. - /// - /// - /// A database contains an installed version when it contains at least one valid table. - /// - public bool DetermineHasInstalledVersion() + //Constraint error summary + if (Errors.Any(x => x.Item1.Equals("Constraint"))) { - return ValidTables.Count > 0; + sb.AppendLine( + "The following constraints (Primary Keys, Foreign Keys and Indexes) were found in the database, but are not in the current schema:"); + sb.AppendLine(string.Join(",", Errors.Where(x => x.Item1.Equals("Constraint")).Select(x => x.Item2))); + sb.AppendLine(" "); } - /// - /// Gets a summary of the schema validation result - /// - /// A string containing a human readable string with a summary message - public string GetSummary() + //Index error summary + if (Errors.Any(x => x.Item1.Equals("Index"))) { - var sb = new StringBuilder(); - if (Errors.Any() == false) - { - sb.AppendLine("The database schema validation didn't find any errors."); - return sb.ToString(); - } - - //Table error summary - if (Errors.Any(x => x.Item1.Equals("Table"))) - { - sb.AppendLine("The following tables were found in the database, but are not in the current schema:"); - sb.AppendLine(string.Join(",", Errors.Where(x => x.Item1.Equals("Table")).Select(x => x.Item2))); - sb.AppendLine(" "); - } - //Column error summary - if (Errors.Any(x => x.Item1.Equals("Column"))) - { - sb.AppendLine("The following columns were found in the database, but are not in the current schema:"); - sb.AppendLine(string.Join(",", Errors.Where(x => x.Item1.Equals("Column")).Select(x => x.Item2))); - sb.AppendLine(" "); - } - //Constraint error summary - if (Errors.Any(x => x.Item1.Equals("Constraint"))) - { - sb.AppendLine("The following constraints (Primary Keys, Foreign Keys and Indexes) were found in the database, but are not in the current schema:"); - sb.AppendLine(string.Join(",", Errors.Where(x => x.Item1.Equals("Constraint")).Select(x => x.Item2))); - sb.AppendLine(" "); - } - //Index error summary - if (Errors.Any(x => x.Item1.Equals("Index"))) - { - sb.AppendLine("The following indexes were found in the database, but are not in the current schema:"); - sb.AppendLine(string.Join(",", Errors.Where(x => x.Item1.Equals("Index")).Select(x => x.Item2))); - sb.AppendLine(" "); - } - //Unknown constraint error summary - if (Errors.Any(x => x.Item1.Equals("Unknown"))) - { - sb.AppendLine("The following unknown constraints (Primary Keys, Foreign Keys and Indexes) were found in the database, but are not in the current schema:"); - sb.AppendLine(string.Join(",", Errors.Where(x => x.Item1.Equals("Unknown")).Select(x => x.Item2))); - sb.AppendLine(" "); - } + sb.AppendLine("The following indexes were found in the database, but are not in the current schema:"); + sb.AppendLine(string.Join(",", Errors.Where(x => x.Item1.Equals("Index")).Select(x => x.Item2))); + sb.AppendLine(" "); + } - return sb.ToString(); + //Unknown constraint error summary + if (Errors.Any(x => x.Item1.Equals("Unknown"))) + { + sb.AppendLine( + "The following unknown constraints (Primary Keys, Foreign Keys and Indexes) were found in the database, but are not in the current schema:"); + sb.AppendLine(string.Join(",", Errors.Where(x => x.Item1.Equals("Unknown")).Select(x => x.Item2))); + sb.AppendLine(" "); } + + return sb.ToString(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/MergeBuilder.cs b/src/Umbraco.Infrastructure/Migrations/MergeBuilder.cs index a25c16158792..7516e0de3d2f 100644 --- a/src/Umbraco.Infrastructure/Migrations/MergeBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/MergeBuilder.cs @@ -1,94 +1,91 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Infrastructure.Migrations; -namespace Umbraco.Cms.Infrastructure.Migrations +/// +/// Represents a migration plan builder for merges. +/// +public class MergeBuilder { + private readonly List _migrations = new(); + private readonly MigrationPlan _plan; + private bool _with; + private string? _withLast; + /// - /// Represents a migration plan builder for merges. + /// Initializes a new instance of the class. /// - public class MergeBuilder - { - private readonly MigrationPlan _plan; - private readonly List _migrations = new List(); - private string? _withLast; - private bool _with; + internal MergeBuilder(MigrationPlan plan) => _plan = plan; + + /// + /// Adds a transition to a target state through an empty migration. + /// + public MergeBuilder To(string targetState) + => To(targetState); + + /// + /// Adds a transition to a target state through a migration. + /// + public MergeBuilder To(string targetState) + where TMigration : MigrationBase + => To(targetState, typeof(TMigration)); - /// - /// Initializes a new instance of the class. - /// - internal MergeBuilder(MigrationPlan plan) + /// + /// Adds a transition to a target state through a migration. + /// + public MergeBuilder To(string targetState, Type migration) + { + if (_with) { - _plan = plan; + _withLast = targetState; + targetState = _plan.CreateRandomState(); + } + else + { + _migrations.Add(migration); } - /// - /// Adds a transition to a target state through an empty migration. - /// - public MergeBuilder To(string targetState) - => To(targetState); - - /// - /// Adds a transition to a target state through a migration. - /// - public MergeBuilder To(string targetState) - where TMigration : MigrationBase - => To(targetState, typeof(TMigration)); + _plan.To(targetState, migration); + return this; + } - /// - /// Adds a transition to a target state through a migration. - /// - public MergeBuilder To(string targetState, Type migration) + /// + /// Begins the second branch of the merge. + /// + public MergeBuilder With() + { + if (_with) { - if (_with) - { - _withLast = targetState; - targetState = _plan.CreateRandomState(); - } - else - { - _migrations.Add(migration); - } - - _plan.To(targetState, migration); - return this; + throw new InvalidOperationException("Cannot invoke With() twice."); } - /// - /// Begins the second branch of the merge. - /// - public MergeBuilder With() + _with = true; + return this; + } + + /// + /// Completes the merge. + /// + public MigrationPlan As(string targetState) + { + if (!_with) { - if (_with) - throw new InvalidOperationException("Cannot invoke With() twice."); - _with = true; - return this; + throw new InvalidOperationException("Cannot invoke As() without invoking With() first."); } - /// - /// Completes the merge. - /// - public MigrationPlan As(string targetState) - { - if (!_with) - { - throw new InvalidOperationException("Cannot invoke As() without invoking With() first."); - } + // reach final state + _plan.To(targetState); - // reach final state - _plan.To(targetState); + // restart at former end of branch2 + _plan.From(_withLast); - // restart at former end of branch2 - _plan.From(_withLast); + // and replay all branch1 migrations + foreach (Type migration in _migrations) + { + _plan.To(_plan.CreateRandomState(), migration); + } - // and replay all branch1 migrations - foreach (var migration in _migrations) - { - _plan.To(_plan.CreateRandomState(), migration); - } - // reaching final state - _plan.To(targetState); + // reaching final state + _plan.To(targetState); - return _plan; - } + return _plan; } } diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationBase.cs b/src/Umbraco.Infrastructure/Migrations/MigrationBase.cs index 4ffc3ab1e4dd..82119a6d1b2d 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationBase.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationBase.cs @@ -11,118 +11,122 @@ using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -namespace Umbraco.Cms.Infrastructure.Migrations +namespace Umbraco.Cms.Infrastructure.Migrations; + +/// +/// Provides a base class to all migrations. +/// +public abstract partial class MigrationBase : IDiscoverable { /// - /// Provides a base class to all migrations. + /// Initializes a new instance of the class. + /// + /// A migration context. + protected MigrationBase(IMigrationContext context) + => Context = context; + + /// + /// Gets the migration context. + /// + protected IMigrationContext Context { get; } + + /// + /// Gets the logger. + /// + protected ILogger Logger => Context.Logger; + + /// + /// Gets the Sql syntax. + /// + protected ISqlSyntaxProvider SqlSyntax => Context.SqlContext.SqlSyntax; + + /// + /// Gets the database instance. + /// + protected IUmbracoDatabase Database => Context.Database; + + /// + /// Gets the database type. + /// + protected DatabaseType DatabaseType => Context.Database.DatabaseType; + + /// + /// Builds an Alter expression. + /// + public IAlterBuilder Alter => BeginBuild(new AlterBuilder(Context)); + + /// + /// Builds a Create expression. + /// + public ICreateBuilder Create => BeginBuild(new CreateBuilder(Context)); + + /// + /// Builds a Delete expression. + /// + public IDeleteBuilder Delete => BeginBuild(new DeleteBuilder(Context)); + + /// + /// Builds an Execute expression. + /// + public IExecuteBuilder Execute => BeginBuild(new ExecuteBuilder(Context)); + + /// + /// Builds an Insert expression. + /// + public IInsertBuilder Insert => BeginBuild(new InsertBuilder(Context)); + + /// + /// Builds a Rename expression. /// - public abstract partial class MigrationBase : IDiscoverable + public IRenameBuilder Rename => BeginBuild(new RenameBuilder(Context)); + + /// + /// Builds an Update expression. + /// + public IUpdateBuilder Update => BeginBuild(new UpdateBuilder(Context)); + + /// + /// Creates a new Sql statement. + /// + protected Sql Sql() => Context.SqlContext.Sql(); + + /// + /// Creates a new Sql statement with arguments. + /// + protected Sql Sql(string sql, params object[] args) => Context.SqlContext.Sql(sql, args); + + /// + /// Executes the migration. + /// + protected abstract void Migrate(); + + /// + /// Runs the migration. + /// + public void Run() { - /// - /// Initializes a new instance of the class. - /// - /// A migration context. - protected MigrationBase(IMigrationContext context) - => Context = context; - - /// - /// Gets the migration context. - /// - protected IMigrationContext Context { get; } - - /// - /// Gets the logger. - /// - protected ILogger Logger => Context.Logger; - - /// - /// Gets the Sql syntax. - /// - protected ISqlSyntaxProvider SqlSyntax => Context.SqlContext.SqlSyntax; - - /// - /// Gets the database instance. - /// - protected IUmbracoDatabase Database => Context.Database; - - /// - /// Gets the database type. - /// - protected DatabaseType DatabaseType => Context.Database.DatabaseType; - - /// - /// Creates a new Sql statement. - /// - protected Sql Sql() => Context.SqlContext.Sql(); - - /// - /// Creates a new Sql statement with arguments. - /// - protected Sql Sql(string sql, params object[] args) => Context.SqlContext.Sql(sql, args); - - /// - /// Executes the migration. - /// - protected abstract void Migrate(); - - /// - /// Runs the migration. - /// - public void Run() + Migrate(); + + // ensure there is no building expression + // ie we did not forget to .Do() an expression + if (Context.BuildingExpression) { - Migrate(); - - // ensure there is no building expression - // ie we did not forget to .Do() an expression - if (Context.BuildingExpression) - { - throw new IncompleteMigrationExpressionException("The migration has run, but leaves an expression that has not run."); - } + throw new IncompleteMigrationExpressionException( + "The migration has run, but leaves an expression that has not run."); } + } - // ensures we are not already building, - // ie we did not forget to .Do() an expression - private protected T BeginBuild(T builder) + // ensures we are not already building, + // ie we did not forget to .Do() an expression + private protected T BeginBuild(T builder) + { + if (Context.BuildingExpression) { - if (Context.BuildingExpression) - throw new IncompleteMigrationExpressionException("Cannot create a new expression: the previous expression has not run."); - Context.BuildingExpression = true; - return builder; + throw new IncompleteMigrationExpressionException( + "Cannot create a new expression: the previous expression has not run."); } - /// - /// Builds an Alter expression. - /// - public IAlterBuilder Alter => BeginBuild(new AlterBuilder(Context)); - - /// - /// Builds a Create expression. - /// - public ICreateBuilder Create => BeginBuild(new CreateBuilder(Context)); - - /// - /// Builds a Delete expression. - /// - public IDeleteBuilder Delete => BeginBuild(new DeleteBuilder(Context)); - - /// - /// Builds an Execute expression. - /// - public IExecuteBuilder Execute => BeginBuild(new ExecuteBuilder(Context)); - - /// - /// Builds an Insert expression. - /// - public IInsertBuilder Insert => BeginBuild(new InsertBuilder(Context)); - - /// - /// Builds a Rename expression. - /// - public IRenameBuilder Rename => BeginBuild(new RenameBuilder(Context)); - - /// - /// Builds an Update expression. - /// - public IUpdateBuilder Update => BeginBuild(new UpdateBuilder(Context)); + Context.BuildingExpression = true; + return builder; } } diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationBuilder.cs b/src/Umbraco.Infrastructure/Migrations/MigrationBuilder.cs index e68dc7a700e7..40db38e05303 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationBuilder.cs @@ -1,20 +1,13 @@ -using System; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations +namespace Umbraco.Cms.Infrastructure.Migrations; + +public class MigrationBuilder : IMigrationBuilder { - public class MigrationBuilder : IMigrationBuilder - { - private readonly IServiceProvider _container; + private readonly IServiceProvider _container; - public MigrationBuilder(IServiceProvider container) - { - _container = container; - } + public MigrationBuilder(IServiceProvider container) => _container = container; - public MigrationBase Build(Type migrationType, IMigrationContext context) - { - return (MigrationBase) _container.CreateInstance(migrationType, context); - } - } + public MigrationBase Build(Type migrationType, IMigrationContext context) => + (MigrationBase)_container.CreateInstance(migrationType, context); } diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationContext.cs b/src/Umbraco.Infrastructure/Migrations/MigrationContext.cs index 975df9120d5d..8731faa901f4 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationContext.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationContext.cs @@ -1,55 +1,49 @@ -using System; -using System.Collections.Generic; -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Migrations; +using Microsoft.Extensions.Logging; using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Infrastructure.Migrations +namespace Umbraco.Cms.Infrastructure.Migrations; + +/// +/// Implements . +/// +internal class MigrationContext : IMigrationContext { + private readonly List _postMigrations = new(); + /// - /// Implements . + /// Initializes a new instance of the class. /// - internal class MigrationContext : IMigrationContext + public MigrationContext(MigrationPlan plan, IUmbracoDatabase? database, ILogger logger) { - private readonly List _postMigrations = new List(); - - /// - /// Initializes a new instance of the class. - /// - public MigrationContext(MigrationPlan plan, IUmbracoDatabase? database, ILogger logger) - { - Plan = plan; - Database = database ?? throw new ArgumentNullException(nameof(database)); - Logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _postMigrations.AddRange(plan.PostMigrationTypes); - } - - /// - public ILogger Logger { get; } - - public MigrationPlan Plan { get; } - - /// - public IUmbracoDatabase Database { get; } - - /// - public ISqlContext SqlContext => Database.SqlContext; - - /// - public int Index { get; set; } - - /// - public bool BuildingExpression { get; set; } - - // this is only internally exposed - public IReadOnlyList PostMigrations => _postMigrations; - - /// - public void AddPostMigration() - where TMigration : MigrationBase - { - // just adding - will be de-duplicated when executing - _postMigrations.Add(typeof(TMigration)); - } + Plan = plan; + Database = database ?? throw new ArgumentNullException(nameof(database)); + Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _postMigrations.AddRange(plan.PostMigrationTypes); } + + // this is only internally exposed + public IReadOnlyList PostMigrations => _postMigrations; + + /// + public ILogger Logger { get; } + + public MigrationPlan Plan { get; } + + /// + public IUmbracoDatabase Database { get; } + + /// + public ISqlContext SqlContext => Database.SqlContext; + + /// + public int Index { get; set; } + + /// + public bool BuildingExpression { get; set; } + + /// + public void AddPostMigration() + where TMigration : MigrationBase => + // just adding - will be de-duplicated when executing + _postMigrations.Add(typeof(TMigration)); } diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationExpressionBase.cs b/src/Umbraco.Infrastructure/Migrations/MigrationExpressionBase.cs index 48384671975f..4237126910df 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationExpressionBase.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationExpressionBase.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; using System.Text; using Microsoft.Extensions.Logging; using NPoco; @@ -8,155 +5,175 @@ using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations +namespace Umbraco.Cms.Infrastructure.Migrations; + +/// +/// Provides a base class for migration expressions. +/// +public abstract class MigrationExpressionBase : IMigrationExpression { - /// - /// Provides a base class for migration expressions. - /// - public abstract class MigrationExpressionBase : IMigrationExpression - { - private bool _executed; - private List? _expressions; + private bool _executed; + private List? _expressions; - protected MigrationExpressionBase(IMigrationContext context) - { - Context = context ?? throw new ArgumentNullException(nameof(context)); - } + protected MigrationExpressionBase(IMigrationContext context) => + Context = context ?? throw new ArgumentNullException(nameof(context)); - protected IMigrationContext Context { get; } + protected IMigrationContext Context { get; } - protected ILogger Logger => Context.Logger; + protected ILogger Logger => Context.Logger; - protected ISqlSyntaxProvider SqlSyntax => Context.Database.SqlContext.SqlSyntax; + protected ISqlSyntaxProvider SqlSyntax => Context.Database.SqlContext.SqlSyntax; - protected IUmbracoDatabase Database => Context.Database; + protected IUmbracoDatabase Database => Context.Database; - public DatabaseType DatabaseType => Context.Database.DatabaseType; + public DatabaseType DatabaseType => Context.Database.DatabaseType; - public List Expressions => _expressions ?? (_expressions = new List()); + public List Expressions => _expressions ?? (_expressions = new List()); - protected virtual string? GetSql() + /// + /// This might be useful in the future if we add it to the interface, but for now it's used to hack the DeleteAppTables + /// & DeleteForeignKeyExpression + /// to ensure they are not executed twice. + /// + internal string? Name { get; set; } + + public virtual void Execute() + { + if (_executed) { - return ToString(); + throw new InvalidOperationException("This expression has already been executed."); } - public virtual void Execute() - { - if (_executed) - throw new InvalidOperationException("This expression has already been executed."); - _executed = true; - Context.BuildingExpression = false; + _executed = true; + Context.BuildingExpression = false; - var sql = GetSql(); + var sql = GetSql(); - if (string.IsNullOrWhiteSpace(sql)) - { - Logger.LogInformation("SQL [{ContextIndex}]: ", Context.Index); - } - else + if (string.IsNullOrWhiteSpace(sql)) + { + Logger.LogInformation("SQL [{ContextIndex}]: ", Context.Index); + } + else + { + // split multiple statements - required for SQL CE + // http://stackoverflow.com/questions/13665491/sql-ce-inconsistent-with-multiple-statements + var stmtBuilder = new StringBuilder(); + using (var reader = new StringReader(sql)) { - // split multiple statements - required for SQL CE - // http://stackoverflow.com/questions/13665491/sql-ce-inconsistent-with-multiple-statements - var stmtBuilder = new StringBuilder(); - using (var reader = new StringReader(sql)) + string? line; + while ((line = reader.ReadLine()) != null) { - string? line; - while ((line = reader.ReadLine()) != null) + if (line.Trim().Equals("GO", StringComparison.OrdinalIgnoreCase)) { - if (line.Trim().Equals("GO", StringComparison.OrdinalIgnoreCase)) - ExecuteStatement(stmtBuilder); - else - stmtBuilder.Append(line); + ExecuteStatement(stmtBuilder); } + else + { + stmtBuilder.Append(line); + } + } - if (stmtBuilder.Length > 0) - ExecuteStatement(stmtBuilder); + if (stmtBuilder.Length > 0) + { + ExecuteStatement(stmtBuilder); } } + } - Context.Index++; - - if (_expressions == null) - return; + Context.Index++; - foreach (var expression in _expressions) - expression.Execute(); + if (_expressions == null) + { + return; } - protected void Execute(Sql? sql) + foreach (IMigrationExpression expression in _expressions) { - if (_executed) - throw new InvalidOperationException("This expression has already been executed."); - _executed = true; - Context.BuildingExpression = false; + expression.Execute(); + } + } - if (sql == null) - { - Logger.LogInformation($"SQL [{Context.Index}]: "); - } - else - { - Logger.LogInformation($"SQL [{Context.Index}]: {sql.ToText()}"); - Database.Execute(sql); - } + protected virtual string? GetSql() => ToString(); - Context.Index++; + protected void Execute(Sql? sql) + { + if (_executed) + { + throw new InvalidOperationException("This expression has already been executed."); + } - if (_expressions == null) - return; + _executed = true; + Context.BuildingExpression = false; - foreach (var expression in _expressions) - expression.Execute(); + if (sql == null) + { + Logger.LogInformation($"SQL [{Context.Index}]: "); + } + else + { + Logger.LogInformation($"SQL [{Context.Index}]: {sql.ToText()}"); + Database.Execute(sql); } - private void ExecuteStatement(StringBuilder stmtBuilder) + Context.Index++; + + if (_expressions == null) { - var stmt = stmtBuilder.ToString(); - Logger.LogInformation("SQL [{ContextIndex}]: {Sql}", Context.Index, stmt); - Database.Execute(stmt); - stmtBuilder.Clear(); + return; } - protected void AppendStatementSeparator(StringBuilder stmtBuilder) + foreach (IMigrationExpression expression in _expressions) { - stmtBuilder.AppendLine(";"); - if (DatabaseType.IsSqlServer()) - stmtBuilder.AppendLine("GO"); + expression.Execute(); } + } - /// - /// This might be useful in the future if we add it to the interface, but for now it's used to hack the DeleteAppTables & DeleteForeignKeyExpression - /// to ensure they are not executed twice. - /// - internal string? Name { get; set; } + private void ExecuteStatement(StringBuilder stmtBuilder) + { + var stmt = stmtBuilder.ToString(); + Logger.LogInformation("SQL [{ContextIndex}]: {Sql}", Context.Index, stmt); + Database.Execute(stmt); + stmtBuilder.Clear(); + } + + protected void AppendStatementSeparator(StringBuilder stmtBuilder) + { + stmtBuilder.AppendLine(";"); + if (DatabaseType.IsSqlServer()) + { + stmtBuilder.AppendLine("GO"); + } + } - protected string GetQuotedValue(object? val) + protected string GetQuotedValue(object? val) + { + if (val == null) { - if (val == null) return "NULL"; + return "NULL"; + } - var type = val.GetType(); + Type type = val.GetType(); - switch (Type.GetTypeCode(type)) - { - case TypeCode.Boolean: - return ((bool)val) ? "1" : "0"; - case TypeCode.Single: - case TypeCode.Double: - case TypeCode.Decimal: - case TypeCode.SByte: - case TypeCode.Int16: - case TypeCode.Int32: - case TypeCode.Int64: - case TypeCode.Byte: - case TypeCode.UInt16: - case TypeCode.UInt32: - case TypeCode.UInt64: - return val.ToString()!; - case TypeCode.DateTime: - return SqlSyntax.GetQuotedValue(SqlSyntax.FormatDateTime((DateTime) val)); - default: - return SqlSyntax.GetQuotedValue(val.ToString()!); - } + switch (Type.GetTypeCode(type)) + { + case TypeCode.Boolean: + return (bool)val ? "1" : "0"; + case TypeCode.Single: + case TypeCode.Double: + case TypeCode.Decimal: + case TypeCode.SByte: + case TypeCode.Int16: + case TypeCode.Int32: + case TypeCode.Int64: + case TypeCode.Byte: + case TypeCode.UInt16: + case TypeCode.UInt32: + case TypeCode.UInt64: + return val.ToString()!; + case TypeCode.DateTime: + return SqlSyntax.GetQuotedValue(SqlSyntax.FormatDateTime((DateTime)val)); + default: + return SqlSyntax.GetQuotedValue(val.ToString()!); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationPlan.cs b/src/Umbraco.Infrastructure/Migrations/MigrationPlan.cs index 091eebe4961c..503af54598f8 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationPlan.cs @@ -1,394 +1,464 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Extensions; -using Type = System.Type; -namespace Umbraco.Cms.Infrastructure.Migrations +namespace Umbraco.Cms.Infrastructure.Migrations; + +/// +/// Represents a migration plan. +/// +public class MigrationPlan { + private readonly List _postMigrationTypes = new(); + private readonly Dictionary _transitions = new(StringComparer.InvariantCultureIgnoreCase); + private string? _finalState; + + private string? _prevState; /// - /// Represents a migration plan. + /// Initializes a new instance of the class. /// - public class MigrationPlan + /// The name of the plan. + public MigrationPlan(string name) { - private readonly Dictionary _transitions = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - private readonly List _postMigrationTypes = new List(); + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", + nameof(name)); + } - private string? _prevState; - private string? _finalState; + Name = name; + } - /// - /// Initializes a new instance of the class. - /// - /// The name of the plan. - public MigrationPlan(string name) + /// + /// If set to true the plan executor will ignore any current state persisted and + /// run the plan from its initial state to its end state. + /// + public virtual bool IgnoreCurrentState { get; } = false; + + /// + /// Gets the transitions. + /// + public IReadOnlyDictionary Transitions => _transitions; + + public IReadOnlyList PostMigrationTypes => _postMigrationTypes; + + /// + /// Gets the name of the plan. + /// + public string Name { get; } + + /// + /// Gets the initial state. + /// + /// + /// The initial state is the state when the plan has never + /// run. By default, it is the empty string, but plans may override + /// it if they have other ways of determining where to start from. + /// + public virtual string InitialState => string.Empty; + + /// + /// Gets the final state. + /// + public string FinalState + { + get { - if (name == null) + // modifying the plan clears _finalState + // Validate() either sets _finalState, or throws + if (_finalState == null) { - throw new ArgumentNullException(nameof(name)); + Validate(); } - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); - } + return _finalState!; + } + } - Name = name; + // adds a transition + private MigrationPlan Add(string? sourceState, string targetState, Type? migration) + { + if (sourceState == null) + { + throw new ArgumentNullException(nameof(sourceState), + $"{nameof(sourceState)} is null, {nameof(MigrationPlan)}.{nameof(From)} must not have been called."); } - /// - /// If set to true the plan executor will ignore any current state persisted and - /// run the plan from its initial state to its end state. - /// - public virtual bool IgnoreCurrentState { get; } = false; + if (targetState == null) + { + throw new ArgumentNullException(nameof(targetState)); + } - /// - /// Gets the transitions. - /// - public IReadOnlyDictionary Transitions => _transitions; + if (string.IsNullOrWhiteSpace(targetState)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", + nameof(targetState)); + } - public IReadOnlyList PostMigrationTypes => _postMigrationTypes; + if (sourceState == targetState) + { + throw new ArgumentException("Source and target state cannot be identical."); + } - /// - /// Gets the name of the plan. - /// - public string Name { get; } + if (migration == null) + { + throw new ArgumentNullException(nameof(migration)); + } - // adds a transition - private MigrationPlan Add(string? sourceState, string targetState, Type? migration) + if (!migration.Implements()) { - if (sourceState == null) - throw new ArgumentNullException(nameof(sourceState), $"{nameof(sourceState)} is null, {nameof(MigrationPlan)}.{nameof(MigrationPlan.From)} must not have been called."); - if (targetState == null) - throw new ArgumentNullException(nameof(targetState)); - if (string.IsNullOrWhiteSpace(targetState)) - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(targetState)); - if (sourceState == targetState) - throw new ArgumentException("Source and target state cannot be identical."); - if (migration == null) - throw new ArgumentNullException(nameof(migration)); - if (!migration.Implements()) - throw new ArgumentException($"Type {migration.Name} does not implement IMigration.", nameof(migration)); - - sourceState = sourceState.Trim(); - targetState = targetState.Trim(); - - // throw if we already have a transition for that state which is not null, - // null is used to keep track of the last step of the chain - if (_transitions.ContainsKey(sourceState) && _transitions[sourceState] != null) - throw new InvalidOperationException($"A transition from state \"{sourceState}\" has already been defined."); - - // register the transition - _transitions[sourceState] = new Transition(sourceState, targetState, migration); - - // register the target state if we don't know it already - // this is how we keep track of the final state - because - // transitions could be defined in any order, that might - // be overridden afterwards. - if (!_transitions.ContainsKey(targetState)) - _transitions.Add(targetState, null); - - _prevState = targetState; - _finalState = null; // force re-validation - - return this; + throw new ArgumentException($"Type {migration.Name} does not implement IMigration.", nameof(migration)); } - /// - /// Adds a transition to a target state through an empty migration. - /// - public MigrationPlan To(string targetState) - => To(targetState); + sourceState = sourceState.Trim(); + targetState = targetState.Trim(); - public MigrationPlan To(Guid targetState) - => To(targetState.ToString()); + // throw if we already have a transition for that state which is not null, + // null is used to keep track of the last step of the chain + if (_transitions.ContainsKey(sourceState) && _transitions[sourceState] != null) + { + throw new InvalidOperationException($"A transition from state \"{sourceState}\" has already been defined."); + } - /// - /// Adds a transition to a target state through a migration. - /// - public MigrationPlan To(string targetState) - where TMigration : MigrationBase - => To(targetState, typeof(TMigration)); + // register the transition + _transitions[sourceState] = new Transition(sourceState, targetState, migration); - public MigrationPlan To(Guid targetState) - where TMigration : MigrationBase - => To(targetState, typeof(TMigration)); + // register the target state if we don't know it already + // this is how we keep track of the final state - because + // transitions could be defined in any order, that might + // be overridden afterwards. + if (!_transitions.ContainsKey(targetState)) + { + _transitions.Add(targetState, null); + } - /// - /// Adds a transition to a target state through a migration. - /// - public MigrationPlan To(string targetState, Type? migration) - => Add(_prevState, targetState, migration); + _prevState = targetState; + _finalState = null; // force re-validation - public MigrationPlan To(Guid targetState, Type migration) - => Add(_prevState, targetState.ToString(), migration); + return this; + } - /// - /// Sets the starting state. - /// - public MigrationPlan From(string? sourceState) + /// + /// Adds a transition to a target state through an empty migration. + /// + public MigrationPlan To(string targetState) + => To(targetState); + + public MigrationPlan To(Guid targetState) + => To(targetState.ToString()); + + /// + /// Adds a transition to a target state through a migration. + /// + public MigrationPlan To(string targetState) + where TMigration : MigrationBase + => To(targetState, typeof(TMigration)); + + public MigrationPlan To(Guid targetState) + where TMigration : MigrationBase + => To(targetState, typeof(TMigration)); + + /// + /// Adds a transition to a target state through a migration. + /// + public MigrationPlan To(string targetState, Type? migration) + => Add(_prevState, targetState, migration); + + public MigrationPlan To(Guid targetState, Type migration) + => Add(_prevState, targetState.ToString(), migration); + + /// + /// Sets the starting state. + /// + public MigrationPlan From(string? sourceState) + { + _prevState = sourceState ?? throw new ArgumentNullException(nameof(sourceState)); + return this; + } + + /// + /// Adds a transition to a target state through a migration, replacing a previous migration. + /// + /// The new migration. + /// The migration to use to recover from the previous target state. + /// + /// The previous target state, which we need to recover from through + /// . + /// + /// The new target state. + public MigrationPlan ToWithReplace(string recoverState, string targetState) + where TMigrationNew : MigrationBase + where TMigrationRecover : MigrationBase + { + To(targetState); + From(recoverState).To(targetState); + return this; + } + + /// + /// Adds a transition to a target state through a migration, replacing a previous migration. + /// + /// The new migration. + /// The previous target state, which we can recover from directly. + /// The new target state. + public MigrationPlan ToWithReplace(string recoverState, string targetState) + where TMigrationNew : MigrationBase + { + To(targetState); + From(recoverState).To(targetState); + return this; + } + + /// + /// Adds transitions to a target state by cloning transitions from a start state to an end state. + /// + public MigrationPlan ToWithClone(string startState, string endState, string targetState) + { + if (startState == null) { - _prevState = sourceState ?? throw new ArgumentNullException(nameof(sourceState)); - return this; + throw new ArgumentNullException(nameof(startState)); } - /// - /// Adds a transition to a target state through a migration, replacing a previous migration. - /// - /// The new migration. - /// The migration to use to recover from the previous target state. - /// The previous target state, which we need to recover from through . - /// The new target state. - public MigrationPlan ToWithReplace(string recoverState, string targetState) - where TMigrationNew : MigrationBase - where TMigrationRecover : MigrationBase + if (string.IsNullOrWhiteSpace(startState)) { - To(targetState); - From(recoverState).To(targetState); - return this; + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", + nameof(startState)); } - /// - /// Adds a transition to a target state through a migration, replacing a previous migration. - /// - /// The new migration. - /// The previous target state, which we can recover from directly. - /// The new target state. - public MigrationPlan ToWithReplace(string recoverState, string targetState) - where TMigrationNew : MigrationBase + if (endState == null) { - To(targetState); - From(recoverState).To(targetState); - return this; + throw new ArgumentNullException(nameof(endState)); } - /// - /// Adds transitions to a target state by cloning transitions from a start state to an end state. - /// - public MigrationPlan ToWithClone(string startState, string endState, string targetState) + if (string.IsNullOrWhiteSpace(endState)) { - if (startState == null) - throw new ArgumentNullException(nameof(startState)); - if (string.IsNullOrWhiteSpace(startState)) - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(startState)); - if (endState == null) - throw new ArgumentNullException(nameof(endState)); - if (string.IsNullOrWhiteSpace(endState)) - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(endState)); - if (targetState == null) - throw new ArgumentNullException(nameof(targetState)); - if (string.IsNullOrWhiteSpace(targetState)) - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(targetState)); - if (startState == endState) - throw new ArgumentException("Start and end states cannot be identical."); - - startState = startState.Trim(); - endState = endState.Trim(); - targetState = targetState.Trim(); - - var state = startState; - var visited = new HashSet(); - - while (state != endState) - { - if (state is null || visited.Contains(state)) - throw new InvalidOperationException("A loop was detected in the copied chain."); - visited.Add(state); - - if (!_transitions.TryGetValue(state, out var transition)) - throw new InvalidOperationException($"There is no transition from state \"{state}\"."); - - var newTargetState = transition?.TargetState == endState - ? targetState - : CreateRandomState(); - To(newTargetState, transition?.MigrationType); - state = transition?.TargetState; - } - - return this; + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", + nameof(endState)); } - /// - /// Adds a post-migration to the plan. - /// - public virtual MigrationPlan AddPostMigration() - where TMigration : MigrationBase + if (targetState == null) { - // TODO: Post migrations are obsolete/irrelevant. Notifications should be used instead. - // The only place we use this is to clear cookies in the installer which could be done - // via notification. Then we can clean up all the code related to post migrations which is - // not insignificant. + throw new ArgumentNullException(nameof(targetState)); + } - _postMigrationTypes.Add(typeof(TMigration)); - return this; + if (string.IsNullOrWhiteSpace(targetState)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", + nameof(targetState)); } - /// - /// Creates a random, unique state. - /// - public virtual string CreateRandomState() - => Guid.NewGuid().ToString("B").ToUpper(); + if (startState == endState) + { + throw new ArgumentException("Start and end states cannot be identical."); + } - /// - /// Begins a merge. - /// - public MergeBuilder Merge() => new MergeBuilder(this); + startState = startState.Trim(); + endState = endState.Trim(); + targetState = targetState.Trim(); - /// - /// Gets the initial state. - /// - /// The initial state is the state when the plan has never - /// run. By default, it is the empty string, but plans may override - /// it if they have other ways of determining where to start from. - public virtual string InitialState => string.Empty; + var state = startState; + var visited = new HashSet(); - /// - /// Gets the final state. - /// - public string FinalState + while (state != endState) { - get + if (state is null || visited.Contains(state)) { - // modifying the plan clears _finalState - // Validate() either sets _finalState, or throws - if (_finalState == null) - Validate(); + throw new InvalidOperationException("A loop was detected in the copied chain."); + } + + visited.Add(state); - return _finalState!; + if (!_transitions.TryGetValue(state, out Transition? transition)) + { + throw new InvalidOperationException($"There is no transition from state \"{state}\"."); } + + var newTargetState = transition?.TargetState == endState + ? targetState + : CreateRandomState(); + To(newTargetState, transition?.MigrationType); + state = transition?.TargetState; } - /// - /// Validates the plan. - /// - /// The plan's final state. - public void Validate() + return this; + } + + /// + /// Adds a post-migration to the plan. + /// + public virtual MigrationPlan AddPostMigration() + where TMigration : MigrationBase + { + // TODO: Post migrations are obsolete/irrelevant. Notifications should be used instead. + // The only place we use this is to clear cookies in the installer which could be done + // via notification. Then we can clean up all the code related to post migrations which is + // not insignificant. + + _postMigrationTypes.Add(typeof(TMigration)); + return this; + } + + /// + /// Creates a random, unique state. + /// + public virtual string CreateRandomState() + => Guid.NewGuid().ToString("B").ToUpper(); + + /// + /// Begins a merge. + /// + public MergeBuilder Merge() => new(this); + + /// + /// Validates the plan. + /// + /// The plan's final state. + public void Validate() + { + if (_finalState != null) { - if (_finalState != null) - return; - - // quick check for dead ends - a dead end is a transition that has a target state - // that is not null and does not match any source state. such a target state has - // been registered as a source state with a null transition. so there should be only - // one. - string? finalState = null; - foreach (var kvp in _transitions.Where(x => x.Value == null)) + return; + } + + // quick check for dead ends - a dead end is a transition that has a target state + // that is not null and does not match any source state. such a target state has + // been registered as a source state with a null transition. so there should be only + // one. + string? finalState = null; + foreach (KeyValuePair kvp in _transitions.Where(x => x.Value == null)) + { + if (finalState == null) { - if (finalState == null) - finalState = kvp.Key; - else - throw new InvalidOperationException($"Multiple final states have been detected in the plan (\"{finalState}\", \"{kvp.Key}\")." - + " Make sure the plan contains only one final state."); + finalState = kvp.Key; } + else + { + throw new InvalidOperationException( + $"Multiple final states have been detected in the plan (\"{finalState}\", \"{kvp.Key}\")." + + " Make sure the plan contains only one final state."); + } + } - // now check for loops - var verified = new List(); - foreach (var transition in _transitions.Values) + // now check for loops + var verified = new List(); + foreach (Transition? transition in _transitions.Values) + { + if (transition == null || verified.Contains(transition.SourceState)) { - if (transition == null || verified.Contains(transition.SourceState)) - continue; + continue; + } - var visited = new List { transition.SourceState }; - var nextTransition = _transitions[transition.TargetState]; - while (nextTransition != null && !verified.Contains(nextTransition.SourceState)) + var visited = new List {transition.SourceState}; + Transition? nextTransition = _transitions[transition.TargetState]; + while (nextTransition != null && !verified.Contains(nextTransition.SourceState)) + { + if (visited.Contains(nextTransition.SourceState)) { - if (visited.Contains(nextTransition.SourceState)) - throw new InvalidOperationException($"A loop has been detected in the plan around state \"{nextTransition.SourceState}\"." - + " Make sure the plan does not contain circular transition paths."); - visited.Add(nextTransition.SourceState); - nextTransition = _transitions[nextTransition.TargetState]; + throw new InvalidOperationException( + $"A loop has been detected in the plan around state \"{nextTransition.SourceState}\"." + + " Make sure the plan does not contain circular transition paths."); } - verified.AddRange(visited); + + visited.Add(nextTransition.SourceState); + nextTransition = _transitions[nextTransition.TargetState]; } - _finalState = finalState!; + verified.AddRange(visited); } - /// - /// Throws an exception when the initial state is unknown. - /// - public virtual void ThrowOnUnknownInitialState(string state) - { - throw new InvalidOperationException($"The migration plan does not support migrating from state \"{state}\"."); - } + _finalState = finalState!; + } - /// - /// Follows a path (for tests and debugging). - /// - /// Does the same thing Execute does, but does not actually execute migrations. - internal IReadOnlyList FollowPath(string? fromState = null, string? toState = null) - { - toState = toState?.NullOrWhiteSpaceAsNull(); + /// + /// Throws an exception when the initial state is unknown. + /// + public virtual void ThrowOnUnknownInitialState(string state) => + throw new InvalidOperationException($"The migration plan does not support migrating from state \"{state}\"."); - Validate(); + /// + /// Follows a path (for tests and debugging). + /// + /// Does the same thing Execute does, but does not actually execute migrations. + internal IReadOnlyList FollowPath(string? fromState = null, string? toState = null) + { + toState = toState?.NullOrWhiteSpaceAsNull(); - var origState = fromState ?? string.Empty; - var states = new List { origState }; + Validate(); - if (!_transitions.TryGetValue(origState, out var transition)) - throw new InvalidOperationException($"Unknown state \"{origState}\"."); + var origState = fromState ?? string.Empty; + var states = new List {origState}; - while (transition != null) - { - var nextState = transition.TargetState; - origState = nextState; - states.Add(origState); + if (!_transitions.TryGetValue(origState, out Transition? transition)) + { + throw new InvalidOperationException($"Unknown state \"{origState}\"."); + } - if (nextState == toState) - { - transition = null; - continue; - } + while (transition != null) + { + var nextState = transition.TargetState; + origState = nextState; + states.Add(origState); - if (!_transitions.TryGetValue(origState, out transition)) - throw new InvalidOperationException($"Unknown state \"{origState}\"."); + if (nextState == toState) + { + transition = null; + continue; } - // safety check - if (origState != (toState ?? _finalState)) - throw new InvalidOperationException($"Internal error, reached state {origState} which is not state {toState ?? _finalState}"); + if (!_transitions.TryGetValue(origState, out transition)) + { + throw new InvalidOperationException($"Unknown state \"{origState}\"."); + } + } - return states; + // safety check + if (origState != (toState ?? _finalState)) + { + throw new InvalidOperationException( + $"Internal error, reached state {origState} which is not state {toState ?? _finalState}"); } + return states; + } + + /// + /// Represents a plan transition. + /// + public class Transition + { /// - /// Represents a plan transition. + /// Initializes a new instance of the class. /// - public class Transition + public Transition(string sourceState, string targetState, Type migrationTtype) { - /// - /// Initializes a new instance of the class. - /// - public Transition(string sourceState, string targetState, Type migrationTtype) - { - SourceState = sourceState; - TargetState = targetState; - MigrationType = migrationTtype; - } + SourceState = sourceState; + TargetState = targetState; + MigrationType = migrationTtype; + } - /// - /// Gets the source state. - /// - public string SourceState { get; } + /// + /// Gets the source state. + /// + public string SourceState { get; } - /// - /// Gets the target state. - /// - public string TargetState { get; } + /// + /// Gets the target state. + /// + public string TargetState { get; } - /// - /// Gets the migration type. - /// - public Type MigrationType { get; } + /// + /// Gets the migration type. + /// + public Type MigrationType { get; } - /// - public override string ToString() - { - return MigrationType == typeof(NoopMigration) - ? $"{(SourceState == string.Empty ? "" : SourceState)} --> {TargetState}" - : $"{SourceState} -- ({MigrationType.FullName}) --> {TargetState}"; - } - } + /// + public override string ToString() => + MigrationType == typeof(NoopMigration) + ? $"{(SourceState == string.Empty ? "" : SourceState)} --> {TargetState}" + : $"{SourceState} -- ({MigrationType.FullName}) --> {TargetState}"; } } diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs b/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs index a89f89c7bc98..caf498132e2d 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs @@ -1,118 +1,118 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Migrations; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Scoping; -using Umbraco.Extensions; -using Type = System.Type; -namespace Umbraco.Cms.Infrastructure.Migrations +namespace Umbraco.Cms.Infrastructure.Migrations; + +public class MigrationPlanExecutor : IMigrationPlanExecutor { - public class MigrationPlanExecutor : IMigrationPlanExecutor + private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; + private readonly IMigrationBuilder _migrationBuilder; + private readonly IScopeAccessor _scopeAccessor; + private readonly ICoreScopeProvider _scopeProvider; + + public MigrationPlanExecutor( + ICoreScopeProvider scopeProvider, + IScopeAccessor scopeAccessor, + ILoggerFactory loggerFactory, + IMigrationBuilder migrationBuilder) { - private readonly ICoreScopeProvider _scopeProvider; - private readonly IScopeAccessor _scopeAccessor; - private readonly ILoggerFactory _loggerFactory; - private readonly IMigrationBuilder _migrationBuilder; - private readonly ILogger _logger; - - public MigrationPlanExecutor( - ICoreScopeProvider scopeProvider, - IScopeAccessor scopeAccessor, - ILoggerFactory loggerFactory, - IMigrationBuilder migrationBuilder) - { - _scopeProvider = scopeProvider; - _scopeAccessor = scopeAccessor; - _loggerFactory = loggerFactory; - _migrationBuilder = migrationBuilder; - _logger = _loggerFactory.CreateLogger(); - } + _scopeProvider = scopeProvider; + _scopeAccessor = scopeAccessor; + _loggerFactory = loggerFactory; + _migrationBuilder = migrationBuilder; + _logger = _loggerFactory.CreateLogger(); + } - /// - /// Executes the plan. - /// - /// A scope. - /// The state to start execution at. - /// A migration builder. - /// A logger. - /// - /// The final state. - /// The plan executes within the scope, which must then be completed. - public string Execute(MigrationPlan plan, string fromState) - { - plan.Validate(); + /// + /// Executes the plan. + /// + /// A scope. + /// The state to start execution at. + /// A migration builder. + /// A logger. + /// + /// The final state. + /// The plan executes within the scope, which must then be completed. + public string Execute(MigrationPlan plan, string fromState) + { + plan.Validate(); - _logger.LogInformation("Starting '{MigrationName}'...", plan.Name); + _logger.LogInformation("Starting '{MigrationName}'...", plan.Name); - fromState ??= string.Empty; - var nextState = fromState; + fromState ??= string.Empty; + var nextState = fromState; - _logger.LogInformation("At {OrigState}", string.IsNullOrWhiteSpace(nextState) ? "origin" : nextState); + _logger.LogInformation("At {OrigState}", string.IsNullOrWhiteSpace(nextState) ? "origin" : nextState); - if (!plan.Transitions.TryGetValue(nextState, out MigrationPlan.Transition? transition)) - { - plan.ThrowOnUnknownInitialState(nextState); - } + if (!plan.Transitions.TryGetValue(nextState, out MigrationPlan.Transition? transition)) + { + plan.ThrowOnUnknownInitialState(nextState); + } - using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true)) + using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true)) + { + // We want to suppress scope (service, etc...) notifications during a migration plan + // execution. This is because if a package that doesn't have their migration plan + // executed is listening to service notifications to perform some persistence logic, + // that packages notification handlers may explode because that package isn't fully installed yet. + using (scope.Notifications.Suppress()) { - // We want to suppress scope (service, etc...) notifications during a migration plan - // execution. This is because if a package that doesn't have their migration plan - // executed is listening to service notifications to perform some persistence logic, - // that packages notification handlers may explode because that package isn't fully installed yet. - using (scope.Notifications.Suppress()) - { - var context = new MigrationContext(plan, _scopeAccessor.AmbientScope?.Database, _loggerFactory.CreateLogger()); - - while (transition != null) - { - _logger.LogInformation("Execute {MigrationType}", transition.MigrationType.Name); - - var migration = _migrationBuilder.Build(transition.MigrationType, context); - migration.Run(); + var context = new MigrationContext(plan, _scopeAccessor.AmbientScope?.Database, + _loggerFactory.CreateLogger()); - nextState = transition.TargetState; + while (transition != null) + { + _logger.LogInformation("Execute {MigrationType}", transition.MigrationType.Name); - _logger.LogInformation("At {OrigState}", nextState); + MigrationBase migration = _migrationBuilder.Build(transition.MigrationType, context); + migration.Run(); - // throw a raw exception here: this should never happen as the plan has - // been validated - this is just a paranoid safety test - if (!plan.Transitions.TryGetValue(nextState, out transition)) - { - throw new InvalidOperationException($"Unknown state \"{nextState}\"."); - } - } + nextState = transition.TargetState; - // prepare and de-duplicate post-migrations, only keeping the 1st occurence - var temp = new HashSet(); - var postMigrationTypes = context.PostMigrations - .Where(x => !temp.Contains(x)) - .Select(x => { temp.Add(x); return x; }); + _logger.LogInformation("At {OrigState}", nextState); - // run post-migrations - foreach (var postMigrationType in postMigrationTypes) + // throw a raw exception here: this should never happen as the plan has + // been validated - this is just a paranoid safety test + if (!plan.Transitions.TryGetValue(nextState, out transition)) { - _logger.LogInformation($"PostMigration: {postMigrationType.FullName}."); - var postMigration = _migrationBuilder.Build(postMigrationType, context); - postMigration.Run(); + throw new InvalidOperationException($"Unknown state \"{nextState}\"."); } } - } - _logger.LogInformation("Done (pending scope completion)."); + // prepare and de-duplicate post-migrations, only keeping the 1st occurence + var temp = new HashSet(); + IEnumerable postMigrationTypes = context.PostMigrations + .Where(x => !temp.Contains(x)) + .Select(x => + { + temp.Add(x); + return x; + }); - // safety check - again, this should never happen as the plan has been validated, - // and this is just a paranoid safety test - var finalState = plan.FinalState; - if (nextState != finalState) - { - throw new InvalidOperationException($"Internal error, reached state {nextState} which is not final state {finalState}"); + // run post-migrations + foreach (Type postMigrationType in postMigrationTypes) + { + _logger.LogInformation($"PostMigration: {postMigrationType.FullName}."); + MigrationBase postMigration = _migrationBuilder.Build(postMigrationType, context); + postMigration.Run(); + } } + } - return nextState; + _logger.LogInformation("Done (pending scope completion)."); + + // safety check - again, this should never happen as the plan has been validated, + // and this is just a paranoid safety test + var finalState = plan.FinalState; + if (nextState != finalState) + { + throw new InvalidOperationException( + $"Internal error, reached state {nextState} which is not final state {finalState}"); } + + return nextState; } } diff --git a/src/Umbraco.Infrastructure/Migrations/NoopMigration.cs b/src/Umbraco.Infrastructure/Migrations/NoopMigration.cs index 0cc2fbad25ed..b8f49cd73cee 100644 --- a/src/Umbraco.Infrastructure/Migrations/NoopMigration.cs +++ b/src/Umbraco.Infrastructure/Migrations/NoopMigration.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Infrastructure.Migrations +namespace Umbraco.Cms.Infrastructure.Migrations; + +public class NoopMigration : MigrationBase { - public class NoopMigration : MigrationBase + public NoopMigration(IMigrationContext context) : base(context) { - public NoopMigration(IMigrationContext context) : base(context) - { - } + } - protected override void Migrate() - { - // nop - } + protected override void Migrate() + { + // nop } } diff --git a/src/Umbraco.Infrastructure/Migrations/Notifications/DatabaseSchemaCreatedNotification.cs b/src/Umbraco.Infrastructure/Migrations/Notifications/DatabaseSchemaCreatedNotification.cs index 2c2888c19da4..75875b93847c 100644 --- a/src/Umbraco.Infrastructure/Migrations/Notifications/DatabaseSchemaCreatedNotification.cs +++ b/src/Umbraco.Infrastructure/Migrations/Notifications/DatabaseSchemaCreatedNotification.cs @@ -1,13 +1,11 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Infrastructure.Migrations.Notifications -{ - internal class DatabaseSchemaCreatedNotification : StatefulNotification - { - public DatabaseSchemaCreatedNotification(EventMessages eventMessages) => EventMessages = eventMessages; +namespace Umbraco.Cms.Infrastructure.Migrations.Notifications; - public EventMessages EventMessages { get; } +internal class DatabaseSchemaCreatedNotification : StatefulNotification +{ + public DatabaseSchemaCreatedNotification(EventMessages eventMessages) => EventMessages = eventMessages; - } + public EventMessages EventMessages { get; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Notifications/DatabaseSchemaCreatingNotification.cs b/src/Umbraco.Infrastructure/Migrations/Notifications/DatabaseSchemaCreatingNotification.cs index 1be96c9a9a6c..e618a7c85c41 100644 --- a/src/Umbraco.Infrastructure/Migrations/Notifications/DatabaseSchemaCreatingNotification.cs +++ b/src/Umbraco.Infrastructure/Migrations/Notifications/DatabaseSchemaCreatingNotification.cs @@ -1,12 +1,11 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Infrastructure.Migrations.Notifications +namespace Umbraco.Cms.Infrastructure.Migrations.Notifications; + +internal class DatabaseSchemaCreatingNotification : CancelableNotification { - internal class DatabaseSchemaCreatingNotification : CancelableNotification + public DatabaseSchemaCreatingNotification(EventMessages messages) : base(messages) { - public DatabaseSchemaCreatingNotification(EventMessages messages) : base(messages) - { - } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Notifications/MigrationPlansExecutedNotification.cs b/src/Umbraco.Infrastructure/Migrations/Notifications/MigrationPlansExecutedNotification.cs index 50ee5c35827c..22c7e0710df1 100644 --- a/src/Umbraco.Infrastructure/Migrations/Notifications/MigrationPlansExecutedNotification.cs +++ b/src/Umbraco.Infrastructure/Migrations/Notifications/MigrationPlansExecutedNotification.cs @@ -1,18 +1,14 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Infrastructure.Migrations.Notifications -{ - /// - /// Published when one or more migration plans have been successfully executed. - /// - public class MigrationPlansExecutedNotification : INotification - { - public MigrationPlansExecutedNotification(IReadOnlyList executedPlans) - => ExecutedPlans = executedPlans; - - public IReadOnlyList ExecutedPlans { get; } +namespace Umbraco.Cms.Infrastructure.Migrations.Notifications; +/// +/// Published when one or more migration plans have been successfully executed. +/// +public class MigrationPlansExecutedNotification : INotification +{ + public MigrationPlansExecutedNotification(IReadOnlyList executedPlans) + => ExecutedPlans = executedPlans; - } + public IReadOnlyList ExecutedPlans { get; } } diff --git a/src/Umbraco.Infrastructure/Migrations/PostMigrations/ClearCsrfCookies.cs b/src/Umbraco.Infrastructure/Migrations/PostMigrations/ClearCsrfCookies.cs index 2a61351d1f56..c991d35f0110 100644 --- a/src/Umbraco.Infrastructure/Migrations/PostMigrations/ClearCsrfCookies.cs +++ b/src/Umbraco.Infrastructure/Migrations/PostMigrations/ClearCsrfCookies.cs @@ -1,22 +1,21 @@ +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Web; -using Constants = Umbraco.Cms.Core.Constants; -namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations +namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations; + +/// +/// Clears Csrf tokens. +/// +public class ClearCsrfCookies : MigrationBase { - /// - /// Clears Csrf tokens. - /// - public class ClearCsrfCookies : MigrationBase - { - private readonly ICookieManager _cookieManager; + private readonly ICookieManager _cookieManager; - public ClearCsrfCookies(IMigrationContext context, ICookieManager cookieManager) - : base(context) => _cookieManager = cookieManager; + public ClearCsrfCookies(IMigrationContext context, ICookieManager cookieManager) + : base(context) => _cookieManager = cookieManager; - protected override void Migrate() - { - _cookieManager.ExpireCookie(Constants.Web.AngularCookieName); - _cookieManager.ExpireCookie(Constants.Web.CsrfValidationCookieName); - } + protected override void Migrate() + { + _cookieManager.ExpireCookie(Constants.Web.AngularCookieName); + _cookieManager.ExpireCookie(Constants.Web.CsrfValidationCookieName); } } diff --git a/src/Umbraco.Infrastructure/Migrations/PostMigrations/DeleteLogViewerQueryFile.cs b/src/Umbraco.Infrastructure/Migrations/PostMigrations/DeleteLogViewerQueryFile.cs index 3531959dbbb2..ea44c1dac4e7 100644 --- a/src/Umbraco.Infrastructure/Migrations/PostMigrations/DeleteLogViewerQueryFile.cs +++ b/src/Umbraco.Infrastructure/Migrations/PostMigrations/DeleteLogViewerQueryFile.cs @@ -1,34 +1,31 @@ -using System.IO; using Umbraco.Cms.Core.Hosting; + // using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; -namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations +namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations; + +/// +/// Deletes the old file that saved log queries +/// +public class DeleteLogViewerQueryFile : MigrationBase { + private readonly IHostingEnvironment _hostingEnvironment; + /// - /// Deletes the old file that saved log queries + /// Initializes a new instance of the class. /// - public class DeleteLogViewerQueryFile : MigrationBase - { - private readonly IHostingEnvironment _hostingEnvironment; - - /// - /// Initializes a new instance of the class. - /// - public DeleteLogViewerQueryFile(IMigrationContext context, IHostingEnvironment hostingEnvironment) - : base(context) - { - _hostingEnvironment = hostingEnvironment; - } + public DeleteLogViewerQueryFile(IMigrationContext context, IHostingEnvironment hostingEnvironment) + : base(context) => + _hostingEnvironment = hostingEnvironment; - /// - protected override void Migrate() - { - // var logViewerQueryFile = MigrateLogViewerQueriesFromFileToDb.GetLogViewerQueryFile(_hostingEnvironment); - // - // if(File.Exists(logViewerQueryFile)) - // { - // File.Delete(logViewerQueryFile); - // } - } + /// + protected override void Migrate() + { + // var logViewerQueryFile = MigrateLogViewerQueriesFromFileToDb.GetLogViewerQueryFile(_hostingEnvironment); + // + // if(File.Exists(logViewerQueryFile)) + // { + // File.Delete(logViewerQueryFile); + // } } } diff --git a/src/Umbraco.Infrastructure/Migrations/PostMigrations/IPublishedSnapshotRebuilder.cs b/src/Umbraco.Infrastructure/Migrations/PostMigrations/IPublishedSnapshotRebuilder.cs index 94a2bc3aad48..ef0230ee9217 100644 --- a/src/Umbraco.Infrastructure/Migrations/PostMigrations/IPublishedSnapshotRebuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/PostMigrations/IPublishedSnapshotRebuilder.cs @@ -1,18 +1,19 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations +namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations; + +/// +/// Rebuilds the published snapshot. +/// +/// +/// +/// This interface exists because the entire published snapshot lives in Umbraco.Web +/// but we may want to trigger rebuilds from Umbraco.Core. These two assemblies should +/// be refactored, really. +/// +/// +public interface IPublishedSnapshotRebuilder { /// - /// Rebuilds the published snapshot. + /// Rebuilds. /// - /// - /// This interface exists because the entire published snapshot lives in Umbraco.Web - /// but we may want to trigger rebuilds from Umbraco.Core. These two assemblies should - /// be refactored, really. - /// - public interface IPublishedSnapshotRebuilder - { - /// - /// Rebuilds. - /// - void Rebuild(); - } + void Rebuild(); } diff --git a/src/Umbraco.Infrastructure/Migrations/PostMigrations/PublishedSnapshotRebuilder.cs b/src/Umbraco.Infrastructure/Migrations/PostMigrations/PublishedSnapshotRebuilder.cs index b4afea633edb..63b6b7e181c3 100644 --- a/src/Umbraco.Infrastructure/Migrations/PostMigrations/PublishedSnapshotRebuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/PostMigrations/PublishedSnapshotRebuilder.cs @@ -2,30 +2,30 @@ using Umbraco.Cms.Core.PublishedCache; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations +namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations; + +/// +/// Implements in Umbraco.Web (rebuilding). +/// +public class PublishedSnapshotRebuilder : IPublishedSnapshotRebuilder { + private readonly DistributedCache _distributedCache; + private readonly IPublishedSnapshotService _publishedSnapshotService; + /// - /// Implements in Umbraco.Web (rebuilding). + /// Initializes a new instance of the class. /// - public class PublishedSnapshotRebuilder : IPublishedSnapshotRebuilder + public PublishedSnapshotRebuilder(IPublishedSnapshotService publishedSnapshotService, + DistributedCache distributedCache) { - private readonly IPublishedSnapshotService _publishedSnapshotService; - private readonly DistributedCache _distributedCache; - - /// - /// Initializes a new instance of the class. - /// - public PublishedSnapshotRebuilder(IPublishedSnapshotService publishedSnapshotService, DistributedCache distributedCache) - { - _publishedSnapshotService = publishedSnapshotService; - _distributedCache = distributedCache; - } + _publishedSnapshotService = publishedSnapshotService; + _distributedCache = distributedCache; + } - /// - public void Rebuild() - { - _publishedSnapshotService.Rebuild(); - _distributedCache.RefreshAllPublishedSnapshot(); - } + /// + public void Rebuild() + { + _publishedSnapshotService.Rebuild(); + _distributedCache.RefreshAllPublishedSnapshot(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/PostMigrations/RebuildPublishedSnapshot.cs b/src/Umbraco.Infrastructure/Migrations/PostMigrations/RebuildPublishedSnapshot.cs index e2de75b7ec6e..f8f81acd7b41 100644 --- a/src/Umbraco.Infrastructure/Migrations/PostMigrations/RebuildPublishedSnapshot.cs +++ b/src/Umbraco.Infrastructure/Migrations/PostMigrations/RebuildPublishedSnapshot.cs @@ -1,20 +1,19 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations +namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations; + +/// +/// Rebuilds the published snapshot. +/// +public class RebuildPublishedSnapshot : MigrationBase { + private readonly IPublishedSnapshotRebuilder _rebuilder; + /// - /// Rebuilds the published snapshot. + /// Initializes a new instance of the class. /// - public class RebuildPublishedSnapshot : MigrationBase - { - private readonly IPublishedSnapshotRebuilder _rebuilder; - - /// - /// Initializes a new instance of the class. - /// - public RebuildPublishedSnapshot(IMigrationContext context, IPublishedSnapshotRebuilder rebuilder) - : base(context) - => _rebuilder = rebuilder; + public RebuildPublishedSnapshot(IMigrationContext context, IPublishedSnapshotRebuilder rebuilder) + : base(context) + => _rebuilder = rebuilder; - /// - protected override void Migrate() => _rebuilder.Rebuild(); - } + /// + protected override void Migrate() => _rebuilder.Rebuild(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/Common/CreateKeysAndIndexes.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/Common/CreateKeysAndIndexes.cs index bacd875f3fed..db89adff2586 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/Common/CreateKeysAndIndexes.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/Common/CreateKeysAndIndexes.cs @@ -1,22 +1,25 @@ +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Migrations.Install; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.Common +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.Common; + +public class CreateKeysAndIndexes : MigrationBase { - public class CreateKeysAndIndexes : MigrationBase + public CreateKeysAndIndexes(IMigrationContext context) + : base(context) { - public CreateKeysAndIndexes(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - // remove those that may already have keys - Delete.KeysAndIndexes(Cms.Core.Constants.DatabaseSchema.Tables.KeyValue).Do(); - Delete.KeysAndIndexes(Cms.Core.Constants.DatabaseSchema.Tables.PropertyData).Do(); + protected override void Migrate() + { + // remove those that may already have keys + Delete.KeysAndIndexes(Constants.DatabaseSchema.Tables.KeyValue).Do(); + Delete.KeysAndIndexes(Constants.DatabaseSchema.Tables.PropertyData).Do(); - // re-create *all* keys and indexes - foreach (var x in DatabaseSchemaCreator.OrderedTables) - Create.KeysAndIndexes(x).Do(); + // re-create *all* keys and indexes + foreach (Type x in DatabaseSchemaCreator.OrderedTables) + { + Create.KeysAndIndexes(x).Do(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/Common/DeleteKeysAndIndexes.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/Common/DeleteKeysAndIndexes.cs index 14e4a5236a4a..0442f692ec86 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/Common/DeleteKeysAndIndexes.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/Common/DeleteKeysAndIndexes.cs @@ -1,75 +1,40 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.Common +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.Common; + +public class DeleteKeysAndIndexes : MigrationBase { - public class DeleteKeysAndIndexes : MigrationBase + public DeleteKeysAndIndexes(IMigrationContext context) + : base(context) { - public DeleteKeysAndIndexes(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() + protected override void Migrate() + { + // all v7.14 tables + var tables = new[] { - // all v7.14 tables - var tables = new[] - { - "cmsContent", - "cmsContentType", - "cmsContentType2ContentType", - "cmsContentTypeAllowedContentType", - "cmsContentVersion", - "cmsContentXml", - "cmsDataType", - "cmsDataTypePreValues", - "cmsDictionary", - "cmsDocument", - "cmsDocumentType", - "cmsLanguageText", - "cmsMacro", - "cmsMacroProperty", - "cmsMedia", - "cmsMember", - "cmsMember2MemberGroup", - "cmsMemberType", - "cmsPreviewXml", - "cmsPropertyData", - "cmsPropertyType", - "cmsPropertyTypeGroup", - "cmsTagRelationship", - "cmsTags", - "cmsTask", - "cmsTaskType", - "cmsTemplate", - "umbracoAccess", - "umbracoAccessRule", - "umbracoAudit", - "umbracoCacheInstruction", - "umbracoConsent", - "umbracoDomains", - "umbracoExternalLogin", - "umbracoLanguage", - "umbracoLock", - "umbracoLog", - "umbracoMigration", - "umbracoNode", - "umbracoRedirectUrl", - "umbracoRelation", - "umbracoRelationType", - "umbracoServer", - "umbracoUser", - "umbracoUser2NodeNotify", - "umbracoUser2UserGroup", - "umbracoUserGroup", - "umbracoUserGroup2App", - "umbracoUserGroup2NodePermission", - "umbracoUserLogin", - "umbracoUserStartNode", - }; + "cmsContent", "cmsContentType", "cmsContentType2ContentType", "cmsContentTypeAllowedContentType", + "cmsContentVersion", "cmsContentXml", "cmsDataType", "cmsDataTypePreValues", "cmsDictionary", + "cmsDocument", "cmsDocumentType", "cmsLanguageText", "cmsMacro", "cmsMacroProperty", "cmsMedia", + "cmsMember", "cmsMember2MemberGroup", "cmsMemberType", "cmsPreviewXml", "cmsPropertyData", + "cmsPropertyType", "cmsPropertyTypeGroup", "cmsTagRelationship", "cmsTags", "cmsTask", "cmsTaskType", + "cmsTemplate", "umbracoAccess", "umbracoAccessRule", "umbracoAudit", "umbracoCacheInstruction", + "umbracoConsent", "umbracoDomains", "umbracoExternalLogin", "umbracoLanguage", "umbracoLock", + "umbracoLog", "umbracoMigration", "umbracoNode", "umbracoRedirectUrl", "umbracoRelation", + "umbracoRelationType", "umbracoServer", "umbracoUser", "umbracoUser2NodeNotify", + "umbracoUser2UserGroup", "umbracoUserGroup", "umbracoUserGroup2App", "umbracoUserGroup2NodePermission", + "umbracoUserLogin", "umbracoUserStartNode" + }; - // delete *all* keys and indexes - because of FKs - // on known v7 tables only - foreach (var table in tables) - Delete.KeysAndIndexes(table, false, true).Do(); - foreach (var table in tables) - Delete.KeysAndIndexes(table, true, false).Do(); + // delete *all* keys and indexes - because of FKs + // on known v7 tables only + foreach (var table in tables) + { + Delete.KeysAndIndexes(table, false).Do(); + } + + foreach (var table in tables) + { + Delete.KeysAndIndexes(table, true, false).Do(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index 37c2ab6c0eb5..90fc6838534c 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -1,4 +1,3 @@ -using System; using System.Diagnostics.CodeAnalysis; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration; @@ -21,274 +20,272 @@ using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_4_0; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade; + +/// +/// Represents the Umbraco CMS migration plan. +/// +/// +public class UmbracoPlan : MigrationPlan { + private const string InitPrefix = "{init-"; + private const string InitSuffix = "}"; + private readonly IUmbracoVersion _umbracoVersion; + /// - /// Represents the Umbraco CMS migration plan. + /// Initializes a new instance of the class. /// - /// - public class UmbracoPlan : MigrationPlan + /// The Umbraco version. + public UmbracoPlan(IUmbracoVersion umbracoVersion) + : base(Constants.Conventions.Migrations.UmbracoUpgradePlanName) { - private const string InitPrefix = "{init-"; - private const string InitSuffix = "}"; - private readonly IUmbracoVersion _umbracoVersion; - - /// - /// Initializes a new instance of the class. - /// - /// The Umbraco version. - public UmbracoPlan(IUmbracoVersion umbracoVersion) - : base(Constants.Conventions.Migrations.UmbracoUpgradePlanName) - { - _umbracoVersion = umbracoVersion; - DefinePlan(); - } + _umbracoVersion = umbracoVersion; + DefinePlan(); + } - /// - /// - /// The default initial state in plans is string.Empty. - /// - /// When upgrading from version 7, we want to use specific initial states - /// that are e.g. "{init-7.9.3}", "{init-7.11.1}", etc. so we can chain the proper - /// migrations. - /// - /// - /// This is also where we detect the current version, and reject invalid - /// upgrades (from a tool old version, or going back in time, etc). - /// - /// - public override string InitialState + /// + /// + /// The default initial state in plans is string.Empty. + /// + /// When upgrading from version 7, we want to use specific initial states + /// that are e.g. "{init-7.9.3}", "{init-7.11.1}", etc. so we can chain the proper + /// migrations. + /// + /// + /// This is also where we detect the current version, and reject invalid + /// upgrades (from a tool old version, or going back in time, etc). + /// + /// + public override string InitialState + { + get { - get + SemVersion currentVersion = _umbracoVersion.SemanticVersion; + + // only from 8.0.0 and above + var minVersion = new SemVersion(8); + if (currentVersion < minVersion) { - SemVersion currentVersion = _umbracoVersion.SemanticVersion; - - // only from 8.0.0 and above - var minVersion = new SemVersion(8); - if (currentVersion < minVersion) - { - throw new InvalidOperationException( - $"Version {currentVersion} cannot be migrated to {_umbracoVersion.SemanticVersion}." - + $" Please upgrade first to at least {minVersion}."); - } - - // Force versions between 7.14.*-7.15.* into into 7.14 initial state. Because there is no db-changes, - // and we don't want users to workaround my putting in version 7.14.0 them self. - if (minVersion <= currentVersion && currentVersion < new SemVersion(7, 16)) - { - return GetInitState(minVersion); - } - - // initial state is eg "{init-7.14.0}" - return GetInitState(currentVersion); + throw new InvalidOperationException( + $"Version {currentVersion} cannot be migrated to {_umbracoVersion.SemanticVersion}." + + $" Please upgrade first to at least {minVersion}."); } - } - /// - /// Gets the initial state corresponding to a version. - /// - /// The version. - /// - /// The initial state. - /// - private static string GetInitState(SemVersion version) => InitPrefix + version + InitSuffix; - - /// - /// Tries to extract a version from an initial state. - /// - /// The state. - /// The version. - /// - /// true when the state contains a version; otherwise, false.D - /// - private static bool TryGetInitStateVersion(string state, [MaybeNullWhen(false)] out string version) - { - if (state.StartsWith(InitPrefix) && state.EndsWith(InitSuffix)) + // Force versions between 7.14.*-7.15.* into into 7.14 initial state. Because there is no db-changes, + // and we don't want users to workaround my putting in version 7.14.0 them self. + if (minVersion <= currentVersion && currentVersion < new SemVersion(7, 16)) { - version = state.TrimStart(InitPrefix).TrimEnd(InitSuffix); - return true; + return GetInitState(minVersion); } - version = null; - return false; + // initial state is eg "{init-7.14.0}" + return GetInitState(currentVersion); } + } - /// - public override void ThrowOnUnknownInitialState(string state) - { - if (TryGetInitStateVersion(state, out var initVersion)) - { - throw new InvalidOperationException( - $"Version {_umbracoVersion.SemanticVersion} does not support migrating from {initVersion}." - + $" Please verify which versions support migrating from {initVersion}."); - } + /// + /// Gets the initial state corresponding to a version. + /// + /// The version. + /// + /// The initial state. + /// + private static string GetInitState(SemVersion version) => InitPrefix + version + InitSuffix; - base.ThrowOnUnknownInitialState(state); + /// + /// Tries to extract a version from an initial state. + /// + /// The state. + /// The version. + /// + /// true when the state contains a version; otherwise, false.D + /// + private static bool TryGetInitStateVersion(string state, [MaybeNullWhen(false)] out string version) + { + if (state.StartsWith(InitPrefix) && state.EndsWith(InitSuffix)) + { + version = state.TrimStart(InitPrefix).TrimEnd(InitSuffix); + return true; } - /// - /// Defines the plan. - /// - protected void DefinePlan() + version = null; + return false; + } + + /// + public override void ThrowOnUnknownInitialState(string state) + { + if (TryGetInitStateVersion(state, out var initVersion)) { - // MODIFYING THE PLAN - // - // Please take great care when modifying the plan! - // - // * Creating a migration for version 8: - // Append the migration to the main chain, using a new guid, before the "//FINAL" comment - // - // If the new migration causes a merge conflict, because someone else also added another - // new migration, you NEED to fix the conflict by providing one default path, and paths - // out of the conflict states (see examples below). - // - // * Porting from version 7: - // Append the ported migration to the main chain, using a new guid (same as above). - // Create a new special chain from the {init-...} state to the main chain. - - - // plan starts at 7.14.0 (anything before 7.14.0 is not supported) - From(GetInitState(new SemVersion(7, 14))); - - // begin migrating from v7 - remove all keys and indexes - To("{B36B9ABD-374E-465B-9C5F-26AB0D39326F}"); - - To("{7C447271-CA3F-4A6A-A913-5D77015655CB}"); - To("{CBFF58A2-7B50-4F75-8E98-249920DB0F37}"); - To("{5CB66059-45F4-48BA-BCBD-C5035D79206B}"); - To("{FB0A5429-587E-4BD0-8A67-20F0E7E62FF7}"); - To("{F0C42457-6A3B-4912-A7EA-F27ED85A2092}"); - To("{8640C9E4-A1C0-4C59-99BB-609B4E604981}"); - To("{DD1B99AF-8106-4E00-BAC7-A43003EA07F8}"); - To("{9DF05B77-11D1-475C-A00A-B656AF7E0908}"); - To("{6FE3EF34-44A0-4992-B379-B40BC4EF1C4D}"); - To("{7F59355A-0EC9-4438-8157-EB517E6D2727}"); - ToWithReplace("{941B2ABA-2D06-4E04-81F5-74224F1DB037}", - "{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}"); // kill AddVariationTable1 - To("{A7540C58-171D-462A-91C5-7A9AA5CB8BFD}"); - - Merge() - .To("{3E44F712-E2E3-473A-AE49-5D7F8E67CE3F}") - .With() - .To("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}") - .As("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); - - To("{1350617A-4930-4D61-852F-E3AA9E692173}"); - To("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}"); - To("{5F4597F4-A4E0-4AFE-90B5-6D2F896830EB}"); - To("{290C18EE-B3DE-4769-84F1-1F467F3F76DA}"); - To("{6A2C7C1B-A9DB-4EA9-B6AB-78E7D5B722A7}"); - To("{8804D8E8-FE62-4E3A-B8A2-C047C2118C38}"); - To("{23275462-446E-44C7-8C2C-3B8C1127B07D}"); - To("{6B251841-3069-4AD5-8AE9-861F9523E8DA}"); - To("{EE429F1B-9B26-43CA-89F8-A86017C809A3}"); - To("{08919C4B-B431-449C-90EC-2B8445B5C6B1}"); - To("{7EB0254C-CB8B-4C75-B15B-D48C55B449EB}"); - To("{C39BF2A7-1454-4047-BBFE-89E40F66ED63}"); - To("{64EBCE53-E1F0-463A-B40B-E98EFCCA8AE2}"); - To("{0009109C-A0B8-4F3F-8FEB-C137BBDDA268}"); - To("{ED28B66A-E248-4D94-8CDB-9BDF574023F0}"); - To("{38C809D5-6C34-426B-9BEA-EFD39162595C}"); - To("{6017F044-8E70-4E10-B2A3-336949692ADD}"); - - Merge() - .To("{CDBEDEE4-9496-4903-9CF2-4104E00FF960}") - .With() - .To("{940FD19A-00A8-4D5C-B8FF-939143585726}") - .As("{0576E786-5C30-4000-B969-302B61E90CA3}"); - - To("{48AD6CCD-C7A4-4305-A8AB-38728AD23FC5}"); - To("{DF470D86-E5CA-42AC-9780-9D28070E25F9}"); - - // finish migrating from v7 - recreate all keys and indexes - To("{3F9764F5-73D0-4D45-8804-1240A66E43A2}"); - - To("{E0CBE54D-A84F-4A8F-9B13-900945FD7ED9}"); - To("{78BAF571-90D0-4D28-8175-EF96316DA789}"); - // release-8.0.0 - - // to 8.0.1 - To("{80C0A0CB-0DD5-4573-B000-C4B7C313C70D}"); - // release-8.0.1 - - // to 8.1.0 - To("{B69B6E8C-A769-4044-A27E-4A4E18D1645A}"); - To("{0372A42B-DECF-498D-B4D1-6379E907EB94}"); - To("{5B1E0D93-F5A3-449B-84BA-65366B84E2D4}"); - - // to 8.6.0 - To("{4759A294-9860-46BC-99F9-B4C975CAE580}"); - To("{0BC866BC-0665-487A-9913-0290BD0169AD}"); - To("{3D67D2C8-5E65-47D0-A9E1-DC2EE0779D6B}"); - To("{EE288A91-531B-4995-8179-1D62D9AA3E2E}"); - To("{2AB29964-02A1-474D-BD6B-72148D2A53A2}"); - - // to 8.7.0 - To("{a78e3369-8ea3-40ec-ad3f-5f76929d2b20}"); - - // to 8.9.0 - To("{B5838FF5-1D22-4F6C-BCEB-F83ACB14B575}"); - - // to 8.10.0 - To("{D6A8D863-38EC-44FB-91EC-ACD6A668BD18}"); - - // NOTE: we need to do a merge migration here because as of 'now', - // v9-beta* is already out and 8.15 isn't out yet - // so we need to ensure that migrations from 8.15 are included in the next - // v9*. - - // to 8.15.0 - To("{8DDDCD0B-D7D5-4C97-BD6A-6B38CA65752F}"); - To("{4695D0C9-0729-4976-985B-048D503665D8}"); - To("{5C424554-A32D-4852-8ED1-A13508187901}"); - - // to 8.17.0 - To("{153865E9-7332-4C2A-9F9D-F20AEE078EC7}"); - - // Hack to support migration from 8.18 - To("{03482BB0-CF13-475C-845E-ECB8319DBE3C}"); - - // This should be safe to execute again. We need it with a new name to ensure updates from all the following has executed this step. - // - 8.15.0 RC - Current state: {4695D0C9-0729-4976-985B-048D503665D8} - // - 8.15.0 Final - Current state: {5C424554-A32D-4852-8ED1-A13508187901} - // - 9.0.0 RC1 - Current state: {5060F3D2-88BE-4D30-8755-CF51F28EAD12} - To("{622E5172-42E1-4662-AD80-9504AF5A4E53}"); - To("{10F7BB61-C550-426B-830B-7F954F689CDF}"); - To("{5AAE6276-80DB-4ACF-B845-199BC6C37538}"); - - // to 9.0.0 RC1 - To("{22D801BA-A1FF-4539-BFCC-2139B55594F8}"); - To("{50A43237-A6F4-49E2-A7A6-5DAD65C84669}"); - To("{3D8DADEF-0FDA-4377-A5F0-B52C2110E8F2}"); - To("{1303BDCF-2295-4645-9526-2F32E8B35ABD}"); - To("{5060F3D2-88BE-4D30-8755-CF51F28EAD12}"); - To( - "{A2686B49-A082-4B22-97FD-AAB154D46A57}"); // Re-run this migration to make sure it has executed to account for migrations going out of sync between versions. - - // TO 9.0.0-rc4 - To( - "5E02F241-5253-403D-B5D3-7DB00157E20F"); // Jaddie: This GUID is missing the { }, although this likely can't be changed now as it will break installs going forwards - - // TO 9.1.0 - To("{8BAF5E6C-DCB7-41AE-824F-4215AE4F1F98}"); - - // TO 9.2.0 - To("{0571C395-8F0B-44E9-8E3F-47BDD08D817B}"); - To("{AD3D3B7F-8E74-45A4-85DB-7FFAD57F9243}"); - - - - // TO 9.3.0 - To("{A2F22F17-5870-4179-8A8D-2362AA4A0A5F}"); - To("{CA7A1D9D-C9D4-4914-BC0A-459E7B9C3C8C}"); - To("{0828F206-DCF7-4F73-ABBB-6792275532EB}"); - - // TO 9.4.0 - To("{DBBA1EA0-25A1-4863-90FB-5D306FB6F1E1}"); - To("{DED98755-4059-41BB-ADBD-3FEAB12D1D7B}"); - - // TO 10.0.0 - To("{B7E0D53C-2B0E-418B-AB07-2DDE486E225F}"); + throw new InvalidOperationException( + $"Version {_umbracoVersion.SemanticVersion} does not support migrating from {initVersion}." + + $" Please verify which versions support migrating from {initVersion}."); } + + base.ThrowOnUnknownInitialState(state); + } + + /// + /// Defines the plan. + /// + protected void DefinePlan() + { + // MODIFYING THE PLAN + // + // Please take great care when modifying the plan! + // + // * Creating a migration for version 8: + // Append the migration to the main chain, using a new guid, before the "//FINAL" comment + // + // If the new migration causes a merge conflict, because someone else also added another + // new migration, you NEED to fix the conflict by providing one default path, and paths + // out of the conflict states (see examples below). + // + // * Porting from version 7: + // Append the ported migration to the main chain, using a new guid (same as above). + // Create a new special chain from the {init-...} state to the main chain. + + + // plan starts at 7.14.0 (anything before 7.14.0 is not supported) + From(GetInitState(new SemVersion(7, 14))); + + // begin migrating from v7 - remove all keys and indexes + To("{B36B9ABD-374E-465B-9C5F-26AB0D39326F}"); + + To("{7C447271-CA3F-4A6A-A913-5D77015655CB}"); + To("{CBFF58A2-7B50-4F75-8E98-249920DB0F37}"); + To("{5CB66059-45F4-48BA-BCBD-C5035D79206B}"); + To("{FB0A5429-587E-4BD0-8A67-20F0E7E62FF7}"); + To("{F0C42457-6A3B-4912-A7EA-F27ED85A2092}"); + To("{8640C9E4-A1C0-4C59-99BB-609B4E604981}"); + To("{DD1B99AF-8106-4E00-BAC7-A43003EA07F8}"); + To("{9DF05B77-11D1-475C-A00A-B656AF7E0908}"); + To("{6FE3EF34-44A0-4992-B379-B40BC4EF1C4D}"); + To("{7F59355A-0EC9-4438-8157-EB517E6D2727}"); + ToWithReplace("{941B2ABA-2D06-4E04-81F5-74224F1DB037}", + "{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}"); // kill AddVariationTable1 + To("{A7540C58-171D-462A-91C5-7A9AA5CB8BFD}"); + + Merge() + .To("{3E44F712-E2E3-473A-AE49-5D7F8E67CE3F}") + .With() + .To("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}") + .As("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); + + To("{1350617A-4930-4D61-852F-E3AA9E692173}"); + To("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}"); + To("{5F4597F4-A4E0-4AFE-90B5-6D2F896830EB}"); + To("{290C18EE-B3DE-4769-84F1-1F467F3F76DA}"); + To("{6A2C7C1B-A9DB-4EA9-B6AB-78E7D5B722A7}"); + To("{8804D8E8-FE62-4E3A-B8A2-C047C2118C38}"); + To("{23275462-446E-44C7-8C2C-3B8C1127B07D}"); + To("{6B251841-3069-4AD5-8AE9-861F9523E8DA}"); + To("{EE429F1B-9B26-43CA-89F8-A86017C809A3}"); + To("{08919C4B-B431-449C-90EC-2B8445B5C6B1}"); + To("{7EB0254C-CB8B-4C75-B15B-D48C55B449EB}"); + To("{C39BF2A7-1454-4047-BBFE-89E40F66ED63}"); + To("{64EBCE53-E1F0-463A-B40B-E98EFCCA8AE2}"); + To("{0009109C-A0B8-4F3F-8FEB-C137BBDDA268}"); + To("{ED28B66A-E248-4D94-8CDB-9BDF574023F0}"); + To("{38C809D5-6C34-426B-9BEA-EFD39162595C}"); + To("{6017F044-8E70-4E10-B2A3-336949692ADD}"); + + Merge() + .To("{CDBEDEE4-9496-4903-9CF2-4104E00FF960}") + .With() + .To("{940FD19A-00A8-4D5C-B8FF-939143585726}") + .As("{0576E786-5C30-4000-B969-302B61E90CA3}"); + + To("{48AD6CCD-C7A4-4305-A8AB-38728AD23FC5}"); + To("{DF470D86-E5CA-42AC-9780-9D28070E25F9}"); + + // finish migrating from v7 - recreate all keys and indexes + To("{3F9764F5-73D0-4D45-8804-1240A66E43A2}"); + + To("{E0CBE54D-A84F-4A8F-9B13-900945FD7ED9}"); + To("{78BAF571-90D0-4D28-8175-EF96316DA789}"); + // release-8.0.0 + + // to 8.0.1 + To("{80C0A0CB-0DD5-4573-B000-C4B7C313C70D}"); + // release-8.0.1 + + // to 8.1.0 + To("{B69B6E8C-A769-4044-A27E-4A4E18D1645A}"); + To("{0372A42B-DECF-498D-B4D1-6379E907EB94}"); + To("{5B1E0D93-F5A3-449B-84BA-65366B84E2D4}"); + + // to 8.6.0 + To("{4759A294-9860-46BC-99F9-B4C975CAE580}"); + To("{0BC866BC-0665-487A-9913-0290BD0169AD}"); + To("{3D67D2C8-5E65-47D0-A9E1-DC2EE0779D6B}"); + To("{EE288A91-531B-4995-8179-1D62D9AA3E2E}"); + To("{2AB29964-02A1-474D-BD6B-72148D2A53A2}"); + + // to 8.7.0 + To("{a78e3369-8ea3-40ec-ad3f-5f76929d2b20}"); + + // to 8.9.0 + To("{B5838FF5-1D22-4F6C-BCEB-F83ACB14B575}"); + + // to 8.10.0 + To("{D6A8D863-38EC-44FB-91EC-ACD6A668BD18}"); + + // NOTE: we need to do a merge migration here because as of 'now', + // v9-beta* is already out and 8.15 isn't out yet + // so we need to ensure that migrations from 8.15 are included in the next + // v9*. + + // to 8.15.0 + To("{8DDDCD0B-D7D5-4C97-BD6A-6B38CA65752F}"); + To("{4695D0C9-0729-4976-985B-048D503665D8}"); + To("{5C424554-A32D-4852-8ED1-A13508187901}"); + + // to 8.17.0 + To("{153865E9-7332-4C2A-9F9D-F20AEE078EC7}"); + + // Hack to support migration from 8.18 + To("{03482BB0-CF13-475C-845E-ECB8319DBE3C}"); + + // This should be safe to execute again. We need it with a new name to ensure updates from all the following has executed this step. + // - 8.15.0 RC - Current state: {4695D0C9-0729-4976-985B-048D503665D8} + // - 8.15.0 Final - Current state: {5C424554-A32D-4852-8ED1-A13508187901} + // - 9.0.0 RC1 - Current state: {5060F3D2-88BE-4D30-8755-CF51F28EAD12} + To("{622E5172-42E1-4662-AD80-9504AF5A4E53}"); + To("{10F7BB61-C550-426B-830B-7F954F689CDF}"); + To("{5AAE6276-80DB-4ACF-B845-199BC6C37538}"); + + // to 9.0.0 RC1 + To("{22D801BA-A1FF-4539-BFCC-2139B55594F8}"); + To("{50A43237-A6F4-49E2-A7A6-5DAD65C84669}"); + To("{3D8DADEF-0FDA-4377-A5F0-B52C2110E8F2}"); + To("{1303BDCF-2295-4645-9526-2F32E8B35ABD}"); + To("{5060F3D2-88BE-4D30-8755-CF51F28EAD12}"); + To( + "{A2686B49-A082-4B22-97FD-AAB154D46A57}"); // Re-run this migration to make sure it has executed to account for migrations going out of sync between versions. + + // TO 9.0.0-rc4 + To( + "5E02F241-5253-403D-B5D3-7DB00157E20F"); // Jaddie: This GUID is missing the { }, although this likely can't be changed now as it will break installs going forwards + + // TO 9.1.0 + To("{8BAF5E6C-DCB7-41AE-824F-4215AE4F1F98}"); + + // TO 9.2.0 + To("{0571C395-8F0B-44E9-8E3F-47BDD08D817B}"); + To("{AD3D3B7F-8E74-45A4-85DB-7FFAD57F9243}"); + + + // TO 9.3.0 + To("{A2F22F17-5870-4179-8A8D-2362AA4A0A5F}"); + To("{CA7A1D9D-C9D4-4914-BC0A-459E7B9C3C8C}"); + To("{0828F206-DCF7-4F73-ABBB-6792275532EB}"); + + // TO 9.4.0 + To("{DBBA1EA0-25A1-4863-90FB-5D306FB6F1E1}"); + To("{DED98755-4059-41BB-ADBD-3FEAB12D1D7B}"); + + // TO 10.0.0 + To("{B7E0D53C-2B0E-418B-AB07-2DDE486E225F}"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/Upgrader.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/Upgrader.cs index 15225b868a8c..8c2c1aeaa715 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/Upgrader.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/Upgrader.cs @@ -1,79 +1,85 @@ -using System; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Migrations; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade; + +/// +/// Used to run a +/// +public class Upgrader { /// - /// Used to run a + /// Initializes a new instance of the class. /// - public class Upgrader - { - /// - /// Initializes a new instance of the class. - /// - public Upgrader(MigrationPlan plan) => Plan = plan; + public Upgrader(MigrationPlan plan) => Plan = plan; - /// - /// Gets the name of the migration plan. - /// - public string Name => Plan.Name; + /// + /// Gets the name of the migration plan. + /// + public string Name => Plan.Name; - /// - /// Gets the migration plan. - /// - public MigrationPlan Plan { get; } + /// + /// Gets the migration plan. + /// + public MigrationPlan Plan { get; } - /// - /// Gets the key for the state value. - /// - public virtual string StateValueKey => Constants.Conventions.Migrations.KeyValuePrefix + Name; + /// + /// Gets the key for the state value. + /// + public virtual string StateValueKey => Constants.Conventions.Migrations.KeyValuePrefix + Name; - /// - /// Executes. - /// - /// A scope provider. - /// A key-value service. - public ExecutedMigrationPlan Execute(IMigrationPlanExecutor migrationPlanExecutor, ICoreScopeProvider scopeProvider, IKeyValueService keyValueService) + /// + /// Executes. + /// + /// A scope provider. + /// A key-value service. + public ExecutedMigrationPlan Execute(IMigrationPlanExecutor migrationPlanExecutor, ICoreScopeProvider scopeProvider, + IKeyValueService keyValueService) + { + if (scopeProvider == null) { - if (scopeProvider == null) throw new ArgumentNullException(nameof(scopeProvider)); - if (keyValueService == null) throw new ArgumentNullException(nameof(keyValueService)); + throw new ArgumentNullException(nameof(scopeProvider)); + } - using (ICoreScope scope = scopeProvider.CreateCoreScope()) - { - // read current state - var currentState = keyValueService.GetValue(StateValueKey); - var forceState = false; + if (keyValueService == null) + { + throw new ArgumentNullException(nameof(keyValueService)); + } - if (currentState == null || Plan.IgnoreCurrentState) - { - currentState = Plan.InitialState; - forceState = true; - } + using (ICoreScope scope = scopeProvider.CreateCoreScope()) + { + // read current state + var currentState = keyValueService.GetValue(StateValueKey); + var forceState = false; - // execute plan - var state = migrationPlanExecutor.Execute(Plan, currentState); - if (string.IsNullOrWhiteSpace(state)) - { - throw new InvalidOperationException("Plan execution returned an invalid null or empty state."); - } + if (currentState == null || Plan.IgnoreCurrentState) + { + currentState = Plan.InitialState; + forceState = true; + } - // save new state - if (forceState) - { - keyValueService.SetValue(StateValueKey, state); - } - else if (currentState != state) - { - keyValueService.SetValue(StateValueKey, currentState, state); - } - - scope.Complete(); + // execute plan + var state = migrationPlanExecutor.Execute(Plan, currentState); + if (string.IsNullOrWhiteSpace(state)) + { + throw new InvalidOperationException("Plan execution returned an invalid null or empty state."); + } - return new ExecutedMigrationPlan(Plan, currentState, state); + // save new state + if (forceState) + { + keyValueService.SetValue(StateValueKey, state); + } + else if (currentState != state) + { + keyValueService.SetValue(StateValueKey, currentState, state); } + + scope.Complete(); + + return new ExecutedMigrationPlan(Plan, currentState, state); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_0_0/AddMemberPropertiesAsColumns.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_0_0/AddMemberPropertiesAsColumns.cs index 5bc58c9b25e6..5a1c48cdb60c 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_0_0/AddMemberPropertiesAsColumns.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_0_0/AddMemberPropertiesAsColumns.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Text; using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence; @@ -28,7 +26,8 @@ protected override void Migrate() AddColumnIfNotExists(columns, "lastPasswordChangeDate"); Sql newestContentVersionQuery = Database.SqlContext.Sql() - .Select($"MAX({GetQuotedSelector("cv", "id")}) as {SqlSyntax.GetQuotedColumnName("id")}", GetQuotedSelector("cv", "nodeId")) + .Select($"MAX({GetQuotedSelector("cv", "id")}) as {SqlSyntax.GetQuotedColumnName("id")}", + GetQuotedSelector("cv", "nodeId")) .From("cv") .GroupBy(GetQuotedSelector("cv", "nodeId")); @@ -62,15 +61,21 @@ protected override void Migrate() .From("pt") .Where($"{GetQuotedSelector("pt", "Alias")} = 'umbracoMemberLastPasswordChangeDate'"); - StringBuilder queryBuilder = new StringBuilder(); + var queryBuilder = new StringBuilder(); queryBuilder.AppendLine($"UPDATE {Constants.DatabaseSchema.Tables.Member}"); queryBuilder.AppendLine("SET"); - queryBuilder.AppendLine($"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate(x => x.FailedPasswordAttempts)} = {GetQuotedSelector("umbracoPropertyData", "intValue")},"); - queryBuilder.AppendLine($"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate(x => x.IsApproved)} = {GetQuotedSelector("pdmp", "intValue")},"); - queryBuilder.AppendLine($"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate(x => x.IsLockedOut)} = {GetQuotedSelector("pdlo", "intValue")},"); - queryBuilder.AppendLine($"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate(x => x.LastLockoutDate)} = {GetQuotedSelector("pdlout", "dateValue")},"); - queryBuilder.AppendLine($"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate(x => x.LastLoginDate)} = {GetQuotedSelector("pdlin", "dateValue")},"); - queryBuilder.Append($"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate(x => x.LastPasswordChangeDate)} = {GetQuotedSelector("pdlpc", "dateValue")}"); + queryBuilder.AppendLine( + $"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate(x => x.FailedPasswordAttempts)} = {GetQuotedSelector("umbracoPropertyData", "intValue")},"); + queryBuilder.AppendLine( + $"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate(x => x.IsApproved)} = {GetQuotedSelector("pdmp", "intValue")},"); + queryBuilder.AppendLine( + $"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate(x => x.IsLockedOut)} = {GetQuotedSelector("pdlo", "intValue")},"); + queryBuilder.AppendLine( + $"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate(x => x.LastLockoutDate)} = {GetQuotedSelector("pdlout", "dateValue")},"); + queryBuilder.AppendLine( + $"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate(x => x.LastLoginDate)} = {GetQuotedSelector("pdlin", "dateValue")},"); + queryBuilder.Append( + $"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate(x => x.LastPasswordChangeDate)} = {GetQuotedSelector("pdlpc", "dateValue")}"); Sql updateMemberColumnsQuery = Database.SqlContext.Sql(queryBuilder.ToString()) .From() @@ -87,37 +92,43 @@ protected override void Migrate() .LeftJoin() .On((left, right) => left.DataTypeId == right.NodeId) .LeftJoin() - .On((left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id) + .On((left, middle, right) => + left.PropertyTypeId == middle.Id && left.VersionId == right.Id) .LeftJoin(memberApprovedQuery, "memberApprovedType") .On((left, right) => left.ContentTypeId == right.ContentTypeId) .LeftJoin("dtmp") .On((left, right) => left.DataTypeId == right.NodeId, null, "dtmp") .LeftJoin("pdmp") - .On((left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id, "pdmp") + .On( + (left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id, "pdmp") .LeftJoin(memberLockedOutQuery, "memberLockedOutType") .On((left, right) => left.ContentTypeId == right.ContentTypeId) .LeftJoin("dtlo") .On((left, right) => left.DataTypeId == right.NodeId, null, "dtlo") .LeftJoin("pdlo") - .On((left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id, "pdlo") + .On( + (left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id, "pdlo") .LeftJoin(memberLastLockoutDateQuery, "lastLockOutDateType") .On((left, right) => left.ContentTypeId == right.ContentTypeId) .LeftJoin("dtlout") .On((left, right) => left.DataTypeId == right.NodeId, null, "dtlout") .LeftJoin("pdlout") - .On((left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id, "pdlout") + .On( + (left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id, "pdlout") .LeftJoin(memberLastLoginDateQuery, "lastLoginDateType") .On((left, right) => left.ContentTypeId == right.ContentTypeId) .LeftJoin("dtlin") .On((left, right) => left.DataTypeId == right.NodeId, null, "dtlin") .LeftJoin("pdlin") - .On((left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id, "pdlin") + .On( + (left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id, "pdlin") .LeftJoin(memberLastPasswordChangeDateQuery, "lastPasswordChangeType") .On((left, right) => left.ContentTypeId == right.ContentTypeId) .LeftJoin("dtlpc") .On((left, right) => left.DataTypeId == right.NodeId, null, "dtlpc") .LeftJoin("pdlpc") - .On((left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id, "pdlpc") + .On( + (left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id, "pdlpc") .Where(x => x.NodeObjectType == Constants.ObjectTypes.Member); Database.Execute(updateMemberColumnsQuery); @@ -126,12 +137,8 @@ protected override void Migrate() // Hard coding the aliases, since we want to be able to delete the constants... string[] propertyTypesToDelete = { - "umbracoMemberFailedPasswordAttempts", - "umbracoMemberApproved", - "umbracoMemberLockedOut", - "umbracoMemberLastLockoutDate", - "umbracoMemberLastLogin", - "umbracoMemberLastPasswordChangeDate" + "umbracoMemberFailedPasswordAttempts", "umbracoMemberApproved", "umbracoMemberLockedOut", + "umbracoMemberLastLockoutDate", "umbracoMemberLastLogin", "umbracoMemberLastPasswordChangeDate" }; Sql idQuery = Database.SqlContext.Sql().Select(x => x.Id) @@ -157,86 +164,67 @@ private string GetQuotedSelector(string tableName, string columnName) private object[] GetSubQueryColumns() => new object[] { - SqlSyntax.GetQuotedColumnName("contentTypeId"), - SqlSyntax.GetQuotedColumnName("dataTypeId"), - SqlSyntax.GetQuotedColumnName("id"), + SqlSyntax.GetQuotedColumnName("contentTypeId"), SqlSyntax.GetQuotedColumnName("dataTypeId"), + SqlSyntax.GetQuotedColumnName("id") }; [TableName("failedAttemptsType")] private class FailedAttempts { - [Column("contentTypeId")] - public int ContentTypeId { get; set; } + [Column("contentTypeId")] public int ContentTypeId { get; set; } - [Column("dataTypeId")] - public int DataTypeId { get; set; } + [Column("dataTypeId")] public int DataTypeId { get; set; } - [Column("id")] - public int Id { get; set; } + [Column("id")] public int Id { get; set; } } [TableName("memberApprovedType")] private class MemberApproved { - [Column("contentTypeId")] - public int ContentTypeId { get; set; } + [Column("contentTypeId")] public int ContentTypeId { get; set; } - [Column("dataTypeId")] - public int DataTypeId { get; set; } + [Column("dataTypeId")] public int DataTypeId { get; set; } - [Column("id")] - public int Id { get; set; } + [Column("id")] public int Id { get; set; } } [TableName("memberLockedOutType")] private class MemberLockedOut { - [Column("contentTypeId")] - public int ContentTypeId { get; set; } + [Column("contentTypeId")] public int ContentTypeId { get; set; } - [Column("dataTypeId")] - public int DataTypeId { get; set; } + [Column("dataTypeId")] public int DataTypeId { get; set; } - [Column("id")] - public int Id { get; set; } + [Column("id")] public int Id { get; set; } } [TableName("lastLockOutDateType")] private class LastLockoutDate { - [Column("contentTypeId")] - public int ContentTypeId { get; set; } + [Column("contentTypeId")] public int ContentTypeId { get; set; } - [Column("dataTypeId")] - public int DataTypeId { get; set; } + [Column("dataTypeId")] public int DataTypeId { get; set; } - [Column("id")] - public int Id { get; set; } + [Column("id")] public int Id { get; set; } } [TableName("lastLoginDateType")] private class LastLoginDate { - [Column("contentTypeId")] - public int ContentTypeId { get; set; } + [Column("contentTypeId")] public int ContentTypeId { get; set; } - [Column("dataTypeId")] - public int DataTypeId { get; set; } + [Column("dataTypeId")] public int DataTypeId { get; set; } - [Column("id")] - public int Id { get; set; } + [Column("id")] public int Id { get; set; } } [TableName("lastPasswordChangeType")] private class LastPasswordChange { - [Column("contentTypeId")] - public int ContentTypeId { get; set; } + [Column("contentTypeId")] public int ContentTypeId { get; set; } - [Column("dataTypeId")] - public int DataTypeId { get; set; } + [Column("dataTypeId")] public int DataTypeId { get; set; } - [Column("id")] - public int Id { get; set; } + [Column("id")] public int Id { get; set; } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddContentNuTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddContentNuTable.cs index b53fd867b298..a216abf045b4 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddContentNuTable.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddContentNuTable.cs @@ -1,20 +1,23 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +internal class AddContentNuTable : MigrationBase { - class AddContentNuTable : MigrationBase + public AddContentNuTable(IMigrationContext context) + : base(context) { - public AddContentNuTable(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() + protected override void Migrate() + { + IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database); + if (tables.InvariantContains("cmsContentNu")) { - var tables = SqlSyntax.GetTablesInSchema(Context.Database); - if (tables.InvariantContains("cmsContentNu")) return; - - Create.Table(true).Do(); + return; } + + Create.Table(true).Do(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddContentTypeIsElementColumn.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddContentTypeIsElementColumn.cs index 28f6e8e6de70..f2152d5751e6 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddContentTypeIsElementColumn.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddContentTypeIsElementColumn.cs @@ -1,15 +1,12 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class AddContentTypeIsElementColumn : MigrationBase { - public class AddContentTypeIsElementColumn : MigrationBase + public AddContentTypeIsElementColumn(IMigrationContext context) : base(context) { - public AddContentTypeIsElementColumn(IMigrationContext context) : base(context) - { } - - protected override void Migrate() - { - AddColumn("isElement"); - } } + + protected override void Migrate() => AddColumn("isElement"); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddLockObjects.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddLockObjects.cs index f8332fb0e2fb..3953bce5bb34 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddLockObjects.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddLockObjects.cs @@ -1,43 +1,44 @@ -using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class AddLockObjects : MigrationBase { - public class AddLockObjects : MigrationBase + public AddLockObjects(IMigrationContext context) + : base(context) { - public AddLockObjects(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - // some may already exist, just ensure everything we need is here - EnsureLockObject(Cms.Core.Constants.Locks.Servers, "Servers"); - EnsureLockObject(Cms.Core.Constants.Locks.ContentTypes, "ContentTypes"); - EnsureLockObject(Cms.Core.Constants.Locks.ContentTree, "ContentTree"); - EnsureLockObject(Cms.Core.Constants.Locks.MediaTree, "MediaTree"); - EnsureLockObject(Cms.Core.Constants.Locks.MemberTree, "MemberTree"); - EnsureLockObject(Cms.Core.Constants.Locks.MediaTypes, "MediaTypes"); - EnsureLockObject(Cms.Core.Constants.Locks.MemberTypes, "MemberTypes"); - EnsureLockObject(Cms.Core.Constants.Locks.Domains, "Domains"); - } + protected override void Migrate() + { + // some may already exist, just ensure everything we need is here + EnsureLockObject(Constants.Locks.Servers, "Servers"); + EnsureLockObject(Constants.Locks.ContentTypes, "ContentTypes"); + EnsureLockObject(Constants.Locks.ContentTree, "ContentTree"); + EnsureLockObject(Constants.Locks.MediaTree, "MediaTree"); + EnsureLockObject(Constants.Locks.MemberTree, "MemberTree"); + EnsureLockObject(Constants.Locks.MediaTypes, "MediaTypes"); + EnsureLockObject(Constants.Locks.MemberTypes, "MemberTypes"); + EnsureLockObject(Constants.Locks.Domains, "Domains"); + } - private void EnsureLockObject(int id, string name) - { - EnsureLockObject(Database, id, name); - } + private void EnsureLockObject(int id, string name) => EnsureLockObject(Database, id, name); - internal static void EnsureLockObject(IUmbracoDatabase db, int id, string name) + internal static void EnsureLockObject(IUmbracoDatabase db, int id, string name) + { + // not if it already exists + var exists = db.Exists(id); + if (exists) { - // not if it already exists - var exists = db.Exists(id); - if (exists) return; + return; + } - // be safe: delete old umbracoNode lock objects if any - db.Execute($"DELETE FROM umbracoNode WHERE id={id};"); + // be safe: delete old umbracoNode lock objects if any + db.Execute($"DELETE FROM umbracoNode WHERE id={id};"); - // then create umbracoLock object - db.Execute($"INSERT umbracoLock (id, name, value) VALUES ({id}, '{name}', 1);"); - } + // then create umbracoLock object + db.Execute($"INSERT umbracoLock (id, name, value) VALUES ({id}, '{name}', 1);"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddLogTableColumns.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddLogTableColumns.cs index 4ef9d4ff142a..f465e01282a0 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddLogTableColumns.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddLogTableColumns.cs @@ -1,20 +1,19 @@ -using System.Linq; -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class AddLogTableColumns : MigrationBase { - public class AddLogTableColumns : MigrationBase + public AddLogTableColumns(IMigrationContext context) + : base(context) { - public AddLogTableColumns(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); + protected override void Migrate() + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); - AddColumnIfNotExists(columns, "entityType"); - AddColumnIfNotExists(columns, "parameters"); - } + AddColumnIfNotExists(columns, "entityType"); + AddColumnIfNotExists(columns, "parameters"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddPackagesSectionAccess.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddPackagesSectionAccess.cs index fc708b1f4b72..26272add225b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddPackagesSectionAccess.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddPackagesSectionAccess.cs @@ -1,19 +1,19 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class AddPackagesSectionAccess : MigrationBase { - public class AddPackagesSectionAccess : MigrationBase + public AddPackagesSectionAccess(IMigrationContext context) + : base(context) { - public AddPackagesSectionAccess(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - // Any user group which had access to the Developer section should have access to Packages - Database.Execute($@" - insert into {Cms.Core.Constants.DatabaseSchema.Tables.UserGroup2App} - select userGroupId, '{Cms.Core.Constants.Applications.Packages}' - from {Cms.Core.Constants.DatabaseSchema.Tables.UserGroup2App} + protected override void Migrate() => + // Any user group which had access to the Developer section should have access to Packages + Database.Execute($@" + insert into {Constants.DatabaseSchema.Tables.UserGroup2App} + select userGroupId, '{Constants.Applications.Packages}' + from {Constants.DatabaseSchema.Tables.UserGroup2App} where app='developer'"); - } - } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddTypedLabels.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddTypedLabels.cs index 69431867b19f..8034a756a10f 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddTypedLabels.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddTypedLabels.cs @@ -1,129 +1,161 @@ -using System; -using System.Globalization; -using System.Linq; +using System.Globalization; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class AddTypedLabels : MigrationBase { - public class AddTypedLabels : MigrationBase + public AddTypedLabels(IMigrationContext context) + : base(context) { - public AddTypedLabels(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - // insert other label datatypes + protected override void Migrate() + { + // insert other label datatypes - void InsertNodeDto(int id, int sortOrder, string uniqueId, string text) + void InsertNodeDto(int id, int sortOrder, string uniqueId, string text) + { + var nodeDto = new NodeDto { - var nodeDto = new NodeDto - { - NodeId = id, - Trashed = false, - ParentId = -1, - UserId = -1, - Level = 1, - Path = "-1,-" + id, - SortOrder = sortOrder, - UniqueId = new Guid(uniqueId), - Text = text, - NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, - CreateDate = DateTime.Now - }; - - Database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Node, "id", false, nodeDto); - } + NodeId = id, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,-" + id, + SortOrder = sortOrder, + UniqueId = new Guid(uniqueId), + Text = text, + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now + }; + + Database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, nodeDto); + } - if (SqlSyntax.SupportsIdentityInsert()) - Database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(Cms.Core.Constants.DatabaseSchema.Tables.Node)} ON ")); + if (SqlSyntax.SupportsIdentityInsert()) + { + Database.Execute(new Sql( + $"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(Constants.DatabaseSchema.Tables.Node)} ON ")); + } - InsertNodeDto(Cms.Core.Constants.DataTypes.LabelInt, 36, "8e7f995c-bd81-4627-9932-c40e568ec788", "Label (integer)"); - InsertNodeDto(Cms.Core.Constants.DataTypes.LabelBigint, 36, "930861bf-e262-4ead-a704-f99453565708", "Label (bigint)"); - InsertNodeDto(Cms.Core.Constants.DataTypes.LabelDateTime, 37, "0e9794eb-f9b5-4f20-a788-93acd233a7e4", "Label (datetime)"); - InsertNodeDto(Cms.Core.Constants.DataTypes.LabelTime, 38, "a97cec69-9b71-4c30-8b12-ec398860d7e8", "Label (time)"); - InsertNodeDto(Cms.Core.Constants.DataTypes.LabelDecimal, 39, "8f1ef1e1-9de4-40d3-a072-6673f631ca64", "Label (decimal)"); + InsertNodeDto(Constants.DataTypes.LabelInt, 36, "8e7f995c-bd81-4627-9932-c40e568ec788", "Label (integer)"); + InsertNodeDto(Constants.DataTypes.LabelBigint, 36, "930861bf-e262-4ead-a704-f99453565708", "Label (bigint)"); + InsertNodeDto(Constants.DataTypes.LabelDateTime, 37, "0e9794eb-f9b5-4f20-a788-93acd233a7e4", + "Label (datetime)"); + InsertNodeDto(Constants.DataTypes.LabelTime, 38, "a97cec69-9b71-4c30-8b12-ec398860d7e8", "Label (time)"); + InsertNodeDto(Constants.DataTypes.LabelDecimal, 39, "8f1ef1e1-9de4-40d3-a072-6673f631ca64", "Label (decimal)"); - if (SqlSyntax.SupportsIdentityInsert()) - Database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(Cms.Core.Constants.DatabaseSchema.Tables.Node)} OFF ")); + if (SqlSyntax.SupportsIdentityInsert()) + { + Database.Execute(new Sql( + $"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(Constants.DatabaseSchema.Tables.Node)} OFF ")); + } - void InsertDataTypeDto(int id, string dbType, string? configuration = null) + void InsertDataTypeDto(int id, string dbType, string? configuration = null) + { + var dataTypeDto = new DataTypeDto { - var dataTypeDto = new DataTypeDto - { - NodeId = id, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.Label, - DbType = dbType - }; - - if (configuration != null) - dataTypeDto.Configuration = configuration; + NodeId = id, EditorAlias = Constants.PropertyEditors.Aliases.Label, DbType = dbType + }; - Database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, dataTypeDto); + if (configuration != null) + { + dataTypeDto.Configuration = configuration; } - InsertDataTypeDto(Cms.Core.Constants.DataTypes.LabelInt, "Integer", "{\"umbracoDataValueType\":\"INT\"}"); - InsertDataTypeDto(Cms.Core.Constants.DataTypes.LabelBigint, "Nvarchar", "{\"umbracoDataValueType\":\"BIGINT\"}"); - InsertDataTypeDto(Cms.Core.Constants.DataTypes.LabelDateTime, "Date", "{\"umbracoDataValueType\":\"DATETIME\"}"); - InsertDataTypeDto(Cms.Core.Constants.DataTypes.LabelDecimal, "Decimal", "{\"umbracoDataValueType\":\"DECIMAL\"}"); - InsertDataTypeDto(Cms.Core.Constants.DataTypes.LabelTime, "Date", "{\"umbracoDataValueType\":\"TIME\"}"); - - // flip known property types - - var labelPropertyTypes = Database.Fetch(Sql() - .Select(x => x.Id, x => x.Alias) - .From() - .Where(x => x.DataTypeId == Cms.Core.Constants.DataTypes.LabelString)); - - var intPropertyAliases = new[] { Cms.Core.Constants.Conventions.Media.Width, Cms.Core.Constants.Conventions.Media.Height, Cms.Core.Constants.Conventions.Member.FailedPasswordAttempts }; - var bigintPropertyAliases = new[] { Cms.Core.Constants.Conventions.Media.Bytes }; - var dtPropertyAliases = new[] { Cms.Core.Constants.Conventions.Member.LastLockoutDate, Cms.Core.Constants.Conventions.Member.LastLoginDate, Cms.Core.Constants.Conventions.Member.LastPasswordChangeDate }; - - var intPropertyTypes = labelPropertyTypes.Where(pt => intPropertyAliases.Contains(pt.Alias)).Select(pt => pt.Id).ToArray(); - var bigintPropertyTypes = labelPropertyTypes.Where(pt => bigintPropertyAliases.Contains(pt.Alias)).Select(pt => pt.Id).ToArray(); - var dtPropertyTypes = labelPropertyTypes.Where(pt => dtPropertyAliases.Contains(pt.Alias)).Select(pt => pt.Id).ToArray(); - - Database.Execute(Sql().Update(u => u.Set(x => x.DataTypeId, Cms.Core.Constants.DataTypes.LabelInt)).WhereIn(x => x.Id, intPropertyTypes)); - Database.Execute(Sql().Update(u => u.Set(x => x.DataTypeId, Cms.Core.Constants.DataTypes.LabelInt)).WhereIn(x => x.Id, intPropertyTypes)); - Database.Execute(Sql().Update(u => u.Set(x => x.DataTypeId, Cms.Core.Constants.DataTypes.LabelBigint)).WhereIn(x => x.Id, bigintPropertyTypes)); - Database.Execute(Sql().Update(u => u.Set(x => x.DataTypeId, Cms.Core.Constants.DataTypes.LabelDateTime)).WhereIn(x => x.Id, dtPropertyTypes)); - - // update values for known property types - // depending on the size of the site, that *may* take time - // but we want to parse in C# not in the database - var values = Database.Fetch(Sql() - .Select(x => x.Id, x => x.VarcharValue) - .From() - .WhereIn(x => x.PropertyTypeId, intPropertyTypes)); - foreach (var value in values) - Database.Execute(Sql() - .Update(u => u - .Set(x => x.IntegerValue, string.IsNullOrWhiteSpace(value.VarcharValue) ? (int?)null : int.Parse(value.VarcharValue, NumberStyles.Any, CultureInfo.InvariantCulture)) - .Set(x => x.TextValue, null) - .Set(x => x.VarcharValue, null)) - .Where(x => x.Id == value.Id)); - - values = Database.Fetch(Sql().Select(x => x.Id, x => x.VarcharValue).From().WhereIn(x => x.PropertyTypeId, dtPropertyTypes)); - foreach (var value in values) - Database.Execute(Sql() - .Update(u => u - .Set(x => x.DateValue, string.IsNullOrWhiteSpace(value.VarcharValue) ? (DateTime?)null : DateTime.Parse(value.VarcharValue, CultureInfo.InvariantCulture, DateTimeStyles.None)) - .Set(x => x.TextValue, null) - .Set(x => x.VarcharValue, null)) - .Where(x => x.Id == value.Id)); - - // anything that's custom... ppl will have to figure it out manually, there isn't much we can do about it + Database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, dataTypeDto); } - // ReSharper disable once ClassNeverInstantiated.Local - // ReSharper disable UnusedAutoPropertyAccessor.Local - private class PropertyDataValue + InsertDataTypeDto(Constants.DataTypes.LabelInt, "Integer", "{\"umbracoDataValueType\":\"INT\"}"); + InsertDataTypeDto(Constants.DataTypes.LabelBigint, "Nvarchar", "{\"umbracoDataValueType\":\"BIGINT\"}"); + InsertDataTypeDto(Constants.DataTypes.LabelDateTime, "Date", "{\"umbracoDataValueType\":\"DATETIME\"}"); + InsertDataTypeDto(Constants.DataTypes.LabelDecimal, "Decimal", "{\"umbracoDataValueType\":\"DECIMAL\"}"); + InsertDataTypeDto(Constants.DataTypes.LabelTime, "Date", "{\"umbracoDataValueType\":\"TIME\"}"); + + // flip known property types + + List? labelPropertyTypes = Database.Fetch(Sql() + .Select(x => x.Id, x => x.Alias) + .From() + .Where(x => x.DataTypeId == Constants.DataTypes.LabelString)); + + var intPropertyAliases = new[] { - public int Id { get; set; } - public string? VarcharValue { get;set; } + Constants.Conventions.Media.Width, Constants.Conventions.Media.Height, + Constants.Conventions.Member.FailedPasswordAttempts + }; + var bigintPropertyAliases = new[] {Constants.Conventions.Media.Bytes}; + var dtPropertyAliases = new[] + { + Constants.Conventions.Member.LastLockoutDate, Constants.Conventions.Member.LastLoginDate, + Constants.Conventions.Member.LastPasswordChangeDate + }; + + var intPropertyTypes = labelPropertyTypes.Where(pt => intPropertyAliases.Contains(pt.Alias)).Select(pt => pt.Id) + .ToArray(); + var bigintPropertyTypes = labelPropertyTypes.Where(pt => bigintPropertyAliases.Contains(pt.Alias)) + .Select(pt => pt.Id).ToArray(); + var dtPropertyTypes = labelPropertyTypes.Where(pt => dtPropertyAliases.Contains(pt.Alias)).Select(pt => pt.Id) + .ToArray(); + + Database.Execute(Sql().Update(u => u.Set(x => x.DataTypeId, Constants.DataTypes.LabelInt)) + .WhereIn(x => x.Id, intPropertyTypes)); + Database.Execute(Sql().Update(u => u.Set(x => x.DataTypeId, Constants.DataTypes.LabelInt)) + .WhereIn(x => x.Id, intPropertyTypes)); + Database.Execute(Sql().Update(u => u.Set(x => x.DataTypeId, Constants.DataTypes.LabelBigint)) + .WhereIn(x => x.Id, bigintPropertyTypes)); + Database.Execute(Sql().Update(u => u.Set(x => x.DataTypeId, Constants.DataTypes.LabelDateTime)) + .WhereIn(x => x.Id, dtPropertyTypes)); + + // update values for known property types + // depending on the size of the site, that *may* take time + // but we want to parse in C# not in the database + List? values = Database.Fetch(Sql() + .Select(x => x.Id, x => x.VarcharValue) + .From() + .WhereIn(x => x.PropertyTypeId, intPropertyTypes)); + foreach (PropertyDataValue? value in values) + { + Database.Execute(Sql() + .Update(u => u + .Set(x => x.IntegerValue, + string.IsNullOrWhiteSpace(value.VarcharValue) + ? null + : int.Parse(value.VarcharValue, NumberStyles.Any, CultureInfo.InvariantCulture)) + .Set(x => x.TextValue, null) + .Set(x => x.VarcharValue, null)) + .Where(x => x.Id == value.Id)); } - // ReSharper restore UnusedAutoPropertyAccessor.Local + + values = Database.Fetch(Sql().Select(x => x.Id, x => x.VarcharValue) + .From().WhereIn(x => x.PropertyTypeId, dtPropertyTypes)); + foreach (PropertyDataValue? value in values) + { + Database.Execute(Sql() + .Update(u => u + .Set(x => x.DateValue, + string.IsNullOrWhiteSpace(value.VarcharValue) + ? null + : DateTime.Parse(value.VarcharValue, CultureInfo.InvariantCulture, DateTimeStyles.None)) + .Set(x => x.TextValue, null) + .Set(x => x.VarcharValue, null)) + .Where(x => x.Id == value.Id)); + } + + // anything that's custom... ppl will have to figure it out manually, there isn't much we can do about it + } + + // ReSharper disable once ClassNeverInstantiated.Local + // ReSharper disable UnusedAutoPropertyAccessor.Local + private class PropertyDataValue + { + public int Id { get; set; } + public string? VarcharValue { get; set; } } + // ReSharper restore UnusedAutoPropertyAccessor.Local } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddVariationTables1A.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddVariationTables1A.cs index 465b17d7fcd8..a1e4c9d88203 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddVariationTables1A.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddVariationTables1A.cs @@ -1,45 +1,50 @@ -using System; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class AddVariationTables1A : MigrationBase { - public class AddVariationTables1A : MigrationBase + public AddVariationTables1A(IMigrationContext context) + : base(context) { - public AddVariationTables1A(IMigrationContext context) - : base(context) - { } + } + // note - original AddVariationTables1 just did + // Create.Table().Do(); + // + // this is taking care of ppl left in this state + + protected override void Migrate() + { // note - original AddVariationTables1 just did // Create.Table().Do(); // - // this is taking care of ppl left in this state + // it's been deprecated, not part of the main upgrade path, + // but we need to take care of ppl caught into the state - protected override void Migrate() + // was not used + Delete.Column("available").FromTable(Constants.DatabaseSchema.Tables.ContentVersionCultureVariation).Do(); + + // was not used + Delete.Column("availableDate").FromTable(Constants.DatabaseSchema.Tables.ContentVersionCultureVariation).Do(); + + //special trick to add the column without constraints and return the sql to add them later + AddColumn("date", out IEnumerable sqls); + //now we need to update the new column with some values because this column doesn't allow NULL values + Update.Table(ContentVersionCultureVariationDto.TableName).Set(new {date = DateTime.Now}).AllRows().Do(); + //now apply constraints (NOT NULL) to new table + foreach (var sql in sqls) { - // note - original AddVariationTables1 just did - // Create.Table().Do(); - // - // it's been deprecated, not part of the main upgrade path, - // but we need to take care of ppl caught into the state - - // was not used - Delete.Column("available").FromTable(Cms.Core.Constants.DatabaseSchema.Tables.ContentVersionCultureVariation).Do(); - - // was not used - Delete.Column("availableDate").FromTable(Cms.Core.Constants.DatabaseSchema.Tables.ContentVersionCultureVariation).Do(); - - //special trick to add the column without constraints and return the sql to add them later - AddColumn("date", out var sqls); - //now we need to update the new column with some values because this column doesn't allow NULL values - Update.Table(ContentVersionCultureVariationDto.TableName).Set(new {date = DateTime.Now}).AllRows().Do(); - //now apply constraints (NOT NULL) to new table - foreach (var sql in sqls) Execute.Sql(sql).Do(); - - // name, languageId are now non-nullable - AlterColumn(Cms.Core.Constants.DatabaseSchema.Tables.ContentVersionCultureVariation, "name"); - AlterColumn(Cms.Core.Constants.DatabaseSchema.Tables.ContentVersionCultureVariation, "languageId"); - - Create.Table().Do(); + Execute.Sql(sql).Do(); } + + // name, languageId are now non-nullable + AlterColumn(Constants.DatabaseSchema.Tables.ContentVersionCultureVariation, + "name"); + AlterColumn(Constants.DatabaseSchema.Tables.ContentVersionCultureVariation, + "languageId"); + + Create.Table().Do(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddVariationTables2.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddVariationTables2.cs index 263dffd2b94f..fada74aaef6b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddVariationTables2.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddVariationTables2.cs @@ -1,17 +1,17 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class AddVariationTables2 : MigrationBase { - public class AddVariationTables2 : MigrationBase + public AddVariationTables2(IMigrationContext context) + : base(context) { - public AddVariationTables2(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - Create.Table(true).Do(); - Create.Table(true).Do(); - } + protected override void Migrate() + { + Create.Table(true).Do(); + Create.Table(true).Do(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs index 6171c3df1320..eefe985b1520 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs @@ -1,66 +1,62 @@ -using System; -using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.Models; +using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.Models; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class ContentVariationMigration : MigrationBase { - public class ContentVariationMigration : MigrationBase + public ContentVariationMigration(IMigrationContext context) + : base(context) { - public ContentVariationMigration(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() + protected override void Migrate() + { + byte GetNewValue(byte oldValue) { - byte GetNewValue(byte oldValue) - { - switch (oldValue) - { - case 0: // Unknown - case 1: // InvariantNeutral - return 0; // Unknown - case 2: // CultureNeutral - case 3: // CultureNeutral | InvariantNeutral - return 1; // Culture - case 4: // InvariantSegment - case 5: // InvariantSegment | InvariantNeutral - return 2; // Segment - case 6: // InvariantSegment | CultureNeutral - case 7: // InvariantSegment | CultureNeutral | InvariantNeutral - case 8: // CultureSegment - case 9: // CultureSegment | InvariantNeutral - case 10: // CultureSegment | CultureNeutral - case 11: // CultureSegment | CultureNeutral | InvariantNeutral - case 12: // etc - case 13: - case 14: - case 15: - return 3; // Culture | Segment - default: - throw new NotSupportedException($"Invalid value {oldValue}."); - } - } - - var propertyTypes = Database.Fetch(Sql().Select().From()); - foreach (var dto in propertyTypes) - { - dto.Variations = GetNewValue(dto.Variations); - Database.Update(dto); - } - - var contentTypes = Database.Fetch(Sql().Select().From()); - foreach (var dto in contentTypes) + switch (oldValue) { - dto.Variations = GetNewValue(dto.Variations); - Database.Update(dto); + case 0: // Unknown + case 1: // InvariantNeutral + return 0; // Unknown + case 2: // CultureNeutral + case 3: // CultureNeutral | InvariantNeutral + return 1; // Culture + case 4: // InvariantSegment + case 5: // InvariantSegment | InvariantNeutral + return 2; // Segment + case 6: // InvariantSegment | CultureNeutral + case 7: // InvariantSegment | CultureNeutral | InvariantNeutral + case 8: // CultureSegment + case 9: // CultureSegment | InvariantNeutral + case 10: // CultureSegment | CultureNeutral + case 11: // CultureSegment | CultureNeutral | InvariantNeutral + case 12: // etc + case 13: + case 14: + case 15: + return 3; // Culture | Segment + default: + throw new NotSupportedException($"Invalid value {oldValue}."); } } - // we *need* to use these private DTOs here, which does *not* have extra properties, which would kill the migration - - - - + List? propertyTypes = + Database.Fetch(Sql().Select().From()); + foreach (PropertyTypeDto80? dto in propertyTypes) + { + dto.Variations = GetNewValue(dto.Variations); + Database.Update(dto); + } + List? contentTypes = + Database.Fetch(Sql().Select().From()); + foreach (ContentTypeDto80? dto in contentTypes) + { + dto.Variations = GetNewValue(dto.Variations); + Database.Update(dto); + } } + + // we *need* to use these private DTOs here, which does *not* have extra properties, which would kill the migration } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs index a6ff99f2c759..1e6e519466b9 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs @@ -1,146 +1,151 @@ -using System.Collections.Generic; -using System.Globalization; -using System.Linq; +using System.Globalization; using System.Runtime.Serialization; using Newtonsoft.Json; +using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.Models; +using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class ConvertRelatedLinksToMultiUrlPicker : MigrationBase { - public class ConvertRelatedLinksToMultiUrlPicker : MigrationBase + public ConvertRelatedLinksToMultiUrlPicker(IMigrationContext context) : base(context) { - public ConvertRelatedLinksToMultiUrlPicker(IMigrationContext context) : base(context) - { } + } - protected override void Migrate() - { - var sqlDataTypes = Sql() - .Select() - .From() - .Where(x => x.EditorAlias == Cms.Core.Constants.PropertyEditors.Legacy.Aliases.RelatedLinks - || x.EditorAlias == Cms.Core.Constants.PropertyEditors.Legacy.Aliases.RelatedLinks2); + protected override void Migrate() + { + Sql sqlDataTypes = Sql() + .Select() + .From() + .Where(x => x.EditorAlias == Constants.PropertyEditors.Legacy.Aliases.RelatedLinks + || x.EditorAlias == Constants.PropertyEditors.Legacy.Aliases.RelatedLinks2); - var dataTypes = Database.Fetch(sqlDataTypes); - var dataTypeIds = dataTypes.Select(x => x.NodeId).ToList(); + List? dataTypes = Database.Fetch(sqlDataTypes); + var dataTypeIds = dataTypes.Select(x => x.NodeId).ToList(); - if (dataTypeIds.Count == 0) return; + if (dataTypeIds.Count == 0) + { + return; + } - foreach (var dataType in dataTypes) - { - dataType.EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.MultiUrlPicker; - Database.Update(dataType); - } + foreach (DataTypeDto? dataType in dataTypes) + { + dataType.EditorAlias = Constants.PropertyEditors.Aliases.MultiUrlPicker; + Database.Update(dataType); + } - var sqlPropertyTpes = Sql() - .Select() - .From() - .Where(x => dataTypeIds.Contains(x.DataTypeId)); + Sql sqlPropertyTpes = Sql() + .Select() + .From() + .Where(x => dataTypeIds.Contains(x.DataTypeId)); - var propertyTypeIds = Database.Fetch(sqlPropertyTpes).Select(x => x.Id).ToList(); + var propertyTypeIds = Database.Fetch(sqlPropertyTpes).Select(x => x.Id).ToList(); - if (propertyTypeIds.Count == 0) return; + if (propertyTypeIds.Count == 0) + { + return; + } - var sqlPropertyData = Sql() - .Select() - .From() - .Where(x => propertyTypeIds.Contains(x.PropertyTypeId)); + Sql sqlPropertyData = Sql() + .Select() + .From() + .Where(x => propertyTypeIds.Contains(x.PropertyTypeId)); - var properties = Database.Fetch(sqlPropertyData); + List? properties = Database.Fetch(sqlPropertyData); - // Create a Multi URL Picker datatype for the converted RelatedLinks data + // Create a Multi URL Picker datatype for the converted RelatedLinks data - foreach (var property in properties) + foreach (PropertyDataDto? property in properties) + { + var value = property.Value?.ToString(); + if (string.IsNullOrWhiteSpace(value)) { - var value = property.Value?.ToString(); - if (string.IsNullOrWhiteSpace(value)) - continue; + continue; + } - var relatedLinks = JsonConvert.DeserializeObject>(value); - var links = new List(); - if (relatedLinks is null) - { - return; - } + List? relatedLinks = JsonConvert.DeserializeObject>(value); + var links = new List(); + if (relatedLinks is null) + { + return; + } - foreach (var relatedLink in relatedLinks) + foreach (RelatedLink relatedLink in relatedLinks) + { + GuidUdi? udi = null; + if (relatedLink.IsInternal) { - GuidUdi? udi = null; - if (relatedLink.IsInternal) + var linkIsUdi = UdiParser.TryParse(relatedLink.Link, out udi); + if (linkIsUdi == false) { - var linkIsUdi = UdiParser.TryParse(relatedLink.Link, out udi); - if (linkIsUdi == false) + // oh no.. probably an integer, yikes! + if (int.TryParse(relatedLink.Link, NumberStyles.Integer, CultureInfo.InvariantCulture, + out var intId)) { - // oh no.. probably an integer, yikes! - if (int.TryParse(relatedLink.Link, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intId)) + Sql sqlNodeData = Sql() + .Select() + .From() + .Where(x => x.NodeId == intId); + + NodeDto? node = Database.Fetch(sqlNodeData).FirstOrDefault(); + if (node != null) + // Note: RelatedLinks did not allow for picking media items, + // so if there's a value this will be a content item - hence + // the hardcoded "document" here { - var sqlNodeData = Sql() - .Select() - .From() - .Where(x => x.NodeId == intId); - - var node = Database.Fetch(sqlNodeData).FirstOrDefault(); - if (node != null) - // Note: RelatedLinks did not allow for picking media items, - // so if there's a value this will be a content item - hence - // the hardcoded "document" here - udi = new GuidUdi("document", node.UniqueId); + udi = new GuidUdi("document", node.UniqueId); } } } - - var link = new LinkDto - { - Name = relatedLink.Caption, - Target = relatedLink.NewWindow ? "_blank" : null, - Udi = udi, - // Should only have a URL if it's an external link otherwise it wil be a UDI - Url = relatedLink.IsInternal == false ? relatedLink.Link : null - }; - - links.Add(link); } - var json = JsonConvert.SerializeObject(links); - - // Update existing data - property.TextValue = json; - Database.Update(property); + var link = new LinkDto + { + Name = relatedLink.Caption, + Target = relatedLink.NewWindow ? "_blank" : null, + Udi = udi, + // Should only have a URL if it's an external link otherwise it wil be a UDI + Url = relatedLink.IsInternal == false ? relatedLink.Link : null + }; + + links.Add(link); } + var json = JsonConvert.SerializeObject(links); + // Update existing data + property.TextValue = json; + Database.Update(property); } } +} - internal class RelatedLink - { - public int? Id { get; internal set; } - internal bool IsDeleted { get; set; } - [JsonProperty("caption")] - public string? Caption { get; set; } - [JsonProperty("link")] - public string? Link { get; set; } - [JsonProperty("newWindow")] - public bool NewWindow { get; set; } - [JsonProperty("isInternal")] - public bool IsInternal { get; set; } - } +internal class RelatedLink +{ + public int? Id { get; internal set; } + internal bool IsDeleted { get; set; } - [DataContract] - internal class LinkDto - { - [DataMember(Name = "name")] - public string? Name { get; set; } + [JsonProperty("caption")] public string? Caption { get; set; } - [DataMember(Name = "target")] - public string? Target { get; set; } + [JsonProperty("link")] public string? Link { get; set; } - [DataMember(Name = "udi")] - public GuidUdi? Udi { get; set; } + [JsonProperty("newWindow")] public bool NewWindow { get; set; } - [DataMember(Name = "url")] - public string? Url { get; set; } - } + [JsonProperty("isInternal")] public bool IsInternal { get; set; } +} + +[DataContract] +internal class LinkDto +{ + [DataMember(Name = "name")] public string? Name { get; set; } + + [DataMember(Name = "target")] public string? Target { get; set; } + + [DataMember(Name = "udi")] public GuidUdi? Udi { get; set; } + + [DataMember(Name = "url")] public string? Url { get; set; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs index c254ecc8df6a..ae259149838b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs @@ -1,138 +1,142 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; +using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class DataTypeMigration : MigrationBase { + private static readonly ISet LegacyAliases = new HashSet + { + Constants.PropertyEditors.Legacy.Aliases.Date, + Constants.PropertyEditors.Legacy.Aliases.Textbox, + Constants.PropertyEditors.Legacy.Aliases.ContentPicker2, + Constants.PropertyEditors.Legacy.Aliases.MediaPicker2, + Constants.PropertyEditors.Legacy.Aliases.MemberPicker2, + Constants.PropertyEditors.Legacy.Aliases.RelatedLinks2, + Constants.PropertyEditors.Legacy.Aliases.TextboxMultiple, + Constants.PropertyEditors.Legacy.Aliases.MultiNodeTreePicker2 + }; + + private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer; + private readonly ILogger _logger; + private readonly PreValueMigratorCollection _preValueMigrators; + private readonly PropertyEditorCollection _propertyEditors; + + public DataTypeMigration(IMigrationContext context, + PreValueMigratorCollection preValueMigrators, + PropertyEditorCollection propertyEditors, + ILogger logger, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) + : base(context) + { + _preValueMigrators = preValueMigrators; + _propertyEditors = propertyEditors; + _logger = logger; + _configurationEditorJsonSerializer = configurationEditorJsonSerializer; + } - public class DataTypeMigration : MigrationBase + protected override void Migrate() { - private readonly PreValueMigratorCollection _preValueMigrators; - private readonly PropertyEditorCollection _propertyEditors; - private readonly ILogger _logger; - private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer; + // drop and create columns + Delete.Column("pk").FromTable("cmsDataType").Do(); - private static readonly ISet LegacyAliases = new HashSet() - { - Cms.Core.Constants.PropertyEditors.Legacy.Aliases.Date, - Cms.Core.Constants.PropertyEditors.Legacy.Aliases.Textbox, - Cms.Core.Constants.PropertyEditors.Legacy.Aliases.ContentPicker2, - Cms.Core.Constants.PropertyEditors.Legacy.Aliases.MediaPicker2, - Cms.Core.Constants.PropertyEditors.Legacy.Aliases.MemberPicker2, - Cms.Core.Constants.PropertyEditors.Legacy.Aliases.RelatedLinks2, - Cms.Core.Constants.PropertyEditors.Legacy.Aliases.TextboxMultiple, - Cms.Core.Constants.PropertyEditors.Legacy.Aliases.MultiNodeTreePicker2, - }; - - public DataTypeMigration(IMigrationContext context, - PreValueMigratorCollection preValueMigrators, - PropertyEditorCollection propertyEditors, - ILogger logger, - IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) - : base(context) - { - _preValueMigrators = preValueMigrators; - _propertyEditors = propertyEditors; - _logger = logger; - _configurationEditorJsonSerializer = configurationEditorJsonSerializer; - } + // rename the table + Rename.Table("cmsDataType").To(Constants.DatabaseSchema.Tables.DataType).Do(); - protected override void Migrate() - { - // drop and create columns - Delete.Column("pk").FromTable("cmsDataType").Do(); + // create column + AddColumn(Constants.DatabaseSchema.Tables.DataType, "config"); + Execute.Sql(Sql().Update(u => u.Set(x => x.Configuration, string.Empty))).Do(); - // rename the table - Rename.Table("cmsDataType").To(Cms.Core.Constants.DatabaseSchema.Tables.DataType).Do(); + // renames + Execute.Sql(Sql() + .Update(u => u.Set(x => x.EditorAlias, "Umbraco.ColorPicker")) + .Where(x => x.EditorAlias == "Umbraco.ColorPickerAlias")).Do(); - // create column - AddColumn(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "config"); - Execute.Sql(Sql().Update(u => u.Set(x => x.Configuration, string.Empty))).Do(); + // from preValues to configuration... + Sql sql = Sql() + .Select() + .AndSelect(x => x.Id, x => x.Alias, x => x.SortOrder, x => x.Value) + .From() + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .OrderBy(x => x.NodeId) + .AndBy(x => x.SortOrder); - // renames - Execute.Sql(Sql() - .Update(u => u.Set(x => x.EditorAlias, "Umbraco.ColorPicker")) - .Where(x => x.EditorAlias == "Umbraco.ColorPickerAlias")).Do(); + IEnumerable> dtos = Database.Fetch(sql).GroupBy(x => x.NodeId); - // from preValues to configuration... - var sql = Sql() + foreach (IGrouping group in dtos) + { + DataTypeDto? dataType = Database.Fetch(Sql() .Select() - .AndSelect(x => x.Id, x => x.Alias, x => x.SortOrder, x => x.Value) .From() - .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .OrderBy(x => x.NodeId) - .AndBy(x => x.SortOrder); + .Where(x => x.NodeId == group.Key)).First(); - var dtos = Database.Fetch(sql).GroupBy(x => x.NodeId); + // check for duplicate aliases + var aliases = group.Select(x => x.Alias).Where(x => !string.IsNullOrWhiteSpace(x)).ToArray(); + if (aliases.Distinct().Count() != aliases.Length) + { + throw new InvalidOperationException( + $"Cannot migrate prevalues for datatype id={dataType.NodeId}, editor={dataType.EditorAlias}: duplicate alias."); + } - foreach (var group in dtos) + // handle null/empty aliases + var index = 0; + var dictionary = group.ToDictionary(x => string.IsNullOrWhiteSpace(x.Alias) ? index++.ToString() : x.Alias); + + // migrate the preValues to configuration + IPreValueMigrator migrator = + _preValueMigrators.GetMigrator(dataType.EditorAlias) ?? new DefaultPreValueMigrator(); + var config = migrator.GetConfiguration(dataType.NodeId, dataType.EditorAlias, dictionary); + var json = _configurationEditorJsonSerializer.Serialize(config); + + // validate - and kill the migration if it fails + var newAlias = migrator.GetNewAlias(dataType.EditorAlias); + if (newAlias == null) + { + if (!LegacyAliases.Contains(dataType.EditorAlias)) + { + _logger.LogWarning( + "Skipping validation of configuration for data type {NodeId} : {EditorAlias}." + + " Please ensure that the configuration is valid. The site may fail to start and / or load data types and run.", + dataType.NodeId, dataType.EditorAlias); + } + } + else if (!_propertyEditors.TryGet(newAlias, out IDataEditor? propertyEditor)) { - var dataType = Database.Fetch(Sql() - .Select() - .From() - .Where(x => x.NodeId == group.Key)).First(); - - // check for duplicate aliases - var aliases = group.Select(x => x.Alias).Where(x => !string.IsNullOrWhiteSpace(x)).ToArray(); - if (aliases.Distinct().Count() != aliases.Length) - throw new InvalidOperationException($"Cannot migrate prevalues for datatype id={dataType.NodeId}, editor={dataType.EditorAlias}: duplicate alias."); - - // handle null/empty aliases - int index = 0; - var dictionary = group.ToDictionary(x => string.IsNullOrWhiteSpace(x.Alias) ? index++.ToString() : x.Alias); - - // migrate the preValues to configuration - var migrator = _preValueMigrators.GetMigrator(dataType.EditorAlias) ?? new DefaultPreValueMigrator(); - var config = migrator.GetConfiguration(dataType.NodeId, dataType.EditorAlias, dictionary); - var json = _configurationEditorJsonSerializer.Serialize(config); - - // validate - and kill the migration if it fails - var newAlias = migrator.GetNewAlias(dataType.EditorAlias); - if (newAlias == null) + if (!LegacyAliases.Contains(newAlias)) { - if (!LegacyAliases.Contains(dataType.EditorAlias)) - { - _logger.LogWarning( - "Skipping validation of configuration for data type {NodeId} : {EditorAlias}." - + " Please ensure that the configuration is valid. The site may fail to start and / or load data types and run.", - dataType.NodeId, dataType.EditorAlias); - } + _logger.LogWarning( + "Skipping validation of configuration for data type {NodeId} : {NewEditorAlias} (was: {EditorAlias})" + + " because no property editor with that alias was found." + + " Please ensure that the configuration is valid. The site may fail to start and / or load data types and run.", + dataType.NodeId, newAlias, dataType.EditorAlias); } - else if (!_propertyEditors.TryGet(newAlias, out var propertyEditor)) + } + else + { + IConfigurationEditor configEditor = propertyEditor.GetConfigurationEditor(); + try { - if (!LegacyAliases.Contains(newAlias)) - { - _logger.LogWarning("Skipping validation of configuration for data type {NodeId} : {NewEditorAlias} (was: {EditorAlias})" - + " because no property editor with that alias was found." - + " Please ensure that the configuration is valid. The site may fail to start and / or load data types and run.", - dataType.NodeId, newAlias, dataType.EditorAlias); - } + var _ = configEditor.FromDatabase(json, _configurationEditorJsonSerializer); } - else + catch (Exception e) { - var configEditor = propertyEditor.GetConfigurationEditor(); - try - { - var _ = configEditor.FromDatabase(json, _configurationEditorJsonSerializer); - } - catch (Exception e) - { - _logger.LogWarning(e, "Failed to validate configuration for data type {NodeId} : {NewEditorAlias} (was: {EditorAlias})." - + " Please fix the configuration and ensure it is valid. The site may fail to start and / or load data types and run.", - dataType.NodeId, newAlias, dataType.EditorAlias); - } + _logger.LogWarning(e, + "Failed to validate configuration for data type {NodeId} : {NewEditorAlias} (was: {EditorAlias})." + + " Please fix the configuration and ensure it is valid. The site may fail to start and / or load data types and run.", + dataType.NodeId, newAlias, dataType.EditorAlias); } - - // update - dataType.Configuration = _configurationEditorJsonSerializer.Serialize(config); - Database.Update(dataType); } + + // update + dataType.Configuration = _configurationEditorJsonSerializer.Serialize(config); + Database.Update(dataType); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/ContentPickerPreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/ContentPickerPreValueMigrator.cs index 7e1711604aa9..c9d98619b1a6 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/ContentPickerPreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/ContentPickerPreValueMigrator.cs @@ -1,20 +1,23 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +internal class ContentPickerPreValueMigrator : DefaultPreValueMigrator { - class ContentPickerPreValueMigrator : DefaultPreValueMigrator - { - public override bool CanMigrate(string editorAlias) - => editorAlias == Cms.Core.Constants.PropertyEditors.Legacy.Aliases.ContentPicker2; + public override bool CanMigrate(string editorAlias) + => editorAlias == Constants.PropertyEditors.Legacy.Aliases.ContentPicker2; - public override string? GetNewAlias(string editorAlias) - => null; + public override string? GetNewAlias(string editorAlias) + => null; - protected override object? GetPreValueValue(PreValueDto preValue) + protected override object? GetPreValueValue(PreValueDto preValue) + { + if (preValue.Alias == "showOpenButton" || + preValue.Alias == "ignoreUserStartNodes") { - if (preValue.Alias == "showOpenButton" || - preValue.Alias == "ignoreUserStartNodes") - return preValue.Value == "1"; - - return base.GetPreValueValue(preValue); + return preValue.Value == "1"; } + + return base.GetPreValueValue(preValue); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/DecimalPreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/DecimalPreValueMigrator.cs index 0383e7029eec..358a58d03d57 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/DecimalPreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/DecimalPreValueMigrator.cs @@ -1,21 +1,22 @@ using Newtonsoft.Json; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +internal class DecimalPreValueMigrator : DefaultPreValueMigrator { - class DecimalPreValueMigrator : DefaultPreValueMigrator - { - public override bool CanMigrate(string editorAlias) - => editorAlias == "Umbraco.Decimal"; + public override bool CanMigrate(string editorAlias) + => editorAlias == "Umbraco.Decimal"; - protected override object? GetPreValueValue(PreValueDto preValue) + protected override object? GetPreValueValue(PreValueDto preValue) + { + if (preValue.Alias == "min" || + preValue.Alias == "step" || + preValue.Alias == "max") { - if (preValue.Alias == "min" || - preValue.Alias == "step" || - preValue.Alias == "max") - return decimal.TryParse(preValue.Value, out var d) ? (decimal?) d : null; - - return preValue.Value?.DetectIsJson() ?? false ? JsonConvert.DeserializeObject(preValue.Value) : preValue.Value; + return decimal.TryParse(preValue.Value, out var d) ? (decimal?)d : null; } + + return preValue.Value?.DetectIsJson() ?? false ? JsonConvert.DeserializeObject(preValue.Value) : preValue.Value; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/DefaultPreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/DefaultPreValueMigrator.cs index 30507ac3ec66..19be78d37120 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/DefaultPreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/DefaultPreValueMigrator.cs @@ -1,43 +1,44 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json; +using Newtonsoft.Json; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +internal class DefaultPreValueMigrator : IPreValueMigrator { - class DefaultPreValueMigrator : IPreValueMigrator - { - public virtual bool CanMigrate(string editorAlias) - => true; + public virtual bool CanMigrate(string editorAlias) + => true; - public virtual string? GetNewAlias(string editorAlias) - => editorAlias; + public virtual string? GetNewAlias(string editorAlias) + => editorAlias; - public object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) + public object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) + { + var preValuesA = preValues.Values.ToList(); + var aliases = preValuesA.Select(x => x.Alias).Distinct().ToArray(); + if (aliases.Length == 1 && string.IsNullOrWhiteSpace(aliases[0])) { - var preValuesA = preValues.Values.ToList(); - var aliases = preValuesA.Select(x => x.Alias).Distinct().ToArray(); - if (aliases.Length == 1 && string.IsNullOrWhiteSpace(aliases[0])) + // array-based prevalues + return new Dictionary { - // array-based prevalues - return new Dictionary { ["values"] = preValuesA.OrderBy(x => x.SortOrder).Select(x => x.Value).ToArray() }; - } - - // assuming we don't want to fall back to array - if (aliases.Any(string.IsNullOrWhiteSpace)) - throw new InvalidOperationException($"Cannot migrate prevalues for datatype id={dataTypeId}, editor={editorAlias}: null/empty alias."); - - // dictionary-base prevalues - return GetPreValues(preValuesA).ToDictionary(x => x.Alias, GetPreValueValue); + ["values"] = preValuesA.OrderBy(x => x.SortOrder).Select(x => x.Value).ToArray() + }; } - protected virtual IEnumerable GetPreValues(IEnumerable preValues) - => preValues; - - protected virtual object? GetPreValueValue(PreValueDto preValue) + // assuming we don't want to fall back to array + if (aliases.Any(string.IsNullOrWhiteSpace)) { - return preValue.Value?.DetectIsJson() ?? false ? JsonConvert.DeserializeObject(preValue.Value) : preValue.Value; + throw new InvalidOperationException( + $"Cannot migrate prevalues for datatype id={dataTypeId}, editor={editorAlias}: null/empty alias."); } + + // dictionary-base prevalues + return GetPreValues(preValuesA).ToDictionary(x => x.Alias, GetPreValueValue); } + + protected virtual IEnumerable GetPreValues(IEnumerable preValues) + => preValues; + + protected virtual object? GetPreValueValue(PreValueDto preValue) => preValue.Value?.DetectIsJson() ?? false + ? JsonConvert.DeserializeObject(preValue.Value) + : preValue.Value; } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/DropDownFlexiblePreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/DropDownFlexiblePreValueMigrator.cs index 6c0f3d48696e..3ec52e5b8ccb 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/DropDownFlexiblePreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/DropDownFlexiblePreValueMigrator.cs @@ -1,31 +1,30 @@ -using System.Collections.Generic; -using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +internal class DropDownFlexiblePreValueMigrator : IPreValueMigrator { - class DropDownFlexiblePreValueMigrator : IPreValueMigrator - { - public bool CanMigrate(string editorAlias) - => editorAlias == "Umbraco.DropDown.Flexible"; + public bool CanMigrate(string editorAlias) + => editorAlias == "Umbraco.DropDown.Flexible"; - public virtual string? GetNewAlias(string editorAlias) - => null; + public virtual string? GetNewAlias(string editorAlias) + => null; - public object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) + public object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) + { + var config = new DropDownFlexibleConfiguration(); + foreach (PreValueDto preValue in preValues.Values) { - var config = new DropDownFlexibleConfiguration(); - foreach (var preValue in preValues.Values) + if (preValue.Alias == "multiple") { - if (preValue.Alias == "multiple") - { - config.Multiple = (preValue.Value == "1"); - } - else - { - config.Items.Add(new ValueListConfiguration.ValueListItem { Id = preValue.Id, Value = preValue.Value }); - } + config.Multiple = preValue.Value == "1"; + } + else + { + config.Items.Add(new ValueListConfiguration.ValueListItem {Id = preValue.Id, Value = preValue.Value}); } - return config; } + + return config; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/IPreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/IPreValueMigrator.cs index 5489fd626ed0..87f3ddfc181f 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/IPreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/IPreValueMigrator.cs @@ -1,36 +1,35 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +/// +/// Defines a service migrating preValues. +/// +public interface IPreValueMigrator { /// - /// Defines a service migrating preValues. + /// Determines whether this migrator can migrate a data type. /// - public interface IPreValueMigrator - { - /// - /// Determines whether this migrator can migrate a data type. - /// - /// The data type editor alias. - bool CanMigrate(string editorAlias); + /// The data type editor alias. + bool CanMigrate(string editorAlias); - /// - /// Gets the v8 codebase data type editor alias. - /// - /// The original v7 codebase editor alias. - /// - /// This is used to validate that the migrated configuration can be parsed - /// by the new property editor. Return null to bypass this validation, - /// when for instance we know it will fail, and another, later migration will - /// deal with it. - /// - string? GetNewAlias(string editorAlias); + /// + /// Gets the v8 codebase data type editor alias. + /// + /// The original v7 codebase editor alias. + /// + /// + /// This is used to validate that the migrated configuration can be parsed + /// by the new property editor. Return null to bypass this validation, + /// when for instance we know it will fail, and another, later migration will + /// deal with it. + /// + /// + string? GetNewAlias(string editorAlias); - /// - /// Gets the configuration object corresponding to preValue. - /// - /// The data type identifier. - /// The data type editor alias. - /// PreValues. - object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues); - } + /// + /// Gets the configuration object corresponding to preValue. + /// + /// The data type identifier. + /// The data type editor alias. + /// PreValues. + object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/ListViewPreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/ListViewPreValueMigrator.cs index c306e3eef3f8..3b973e899ef4 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/ListViewPreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/ListViewPreValueMigrator.cs @@ -1,29 +1,26 @@ -using System.Collections.Generic; -using System.Globalization; -using System.Linq; +using System.Globalization; using Newtonsoft.Json; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +internal class ListViewPreValueMigrator : DefaultPreValueMigrator { - class ListViewPreValueMigrator : DefaultPreValueMigrator - { - public override bool CanMigrate(string editorAlias) - => editorAlias == "Umbraco.ListView"; + public override bool CanMigrate(string editorAlias) + => editorAlias == "Umbraco.ListView"; - protected override IEnumerable GetPreValues(IEnumerable preValues) - { - return preValues.Where(preValue => preValue.Alias != "displayAtTabNumber"); - } + protected override IEnumerable GetPreValues(IEnumerable preValues) => + preValues.Where(preValue => preValue.Alias != "displayAtTabNumber"); - protected override object? GetPreValueValue(PreValueDto preValue) + protected override object? GetPreValueValue(PreValueDto preValue) + { + if (preValue.Alias == "pageSize") { - if (preValue.Alias == "pageSize") - { - return int.TryParse(preValue.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) ? (int?)i : null; - } - - return preValue.Value?.DetectIsJson() ?? false ? JsonConvert.DeserializeObject(preValue.Value) : preValue.Value; + return int.TryParse(preValue.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) + ? (int?)i + : null; } + + return preValue.Value?.DetectIsJson() ?? false ? JsonConvert.DeserializeObject(preValue.Value) : preValue.Value; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/MarkdownEditorPreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/MarkdownEditorPreValueMigrator.cs index 9f8e7da57a68..98c2b04f33ae 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/MarkdownEditorPreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/MarkdownEditorPreValueMigrator.cs @@ -1,16 +1,19 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +internal class MarkdownEditorPreValueMigrator : DefaultPreValueMigrator //PreValueMigratorBase { - class MarkdownEditorPreValueMigrator : DefaultPreValueMigrator //PreValueMigratorBase - { - public override bool CanMigrate(string editorAlias) - => editorAlias == Cms.Core.Constants.PropertyEditors.Aliases.MarkdownEditor; + public override bool CanMigrate(string editorAlias) + => editorAlias == Constants.PropertyEditors.Aliases.MarkdownEditor; - protected override object? GetPreValueValue(PreValueDto preValue) + protected override object? GetPreValueValue(PreValueDto preValue) + { + if (preValue.Alias == "preview") { - if (preValue.Alias == "preview") - return preValue.Value == "1"; - - return base.GetPreValueValue(preValue); + return preValue.Value == "1"; } + + return base.GetPreValueValue(preValue); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/MediaPickerPreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/MediaPickerPreValueMigrator.cs index 364cc3e86b3d..0b5737f51598 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/MediaPickerPreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/MediaPickerPreValueMigrator.cs @@ -1,37 +1,37 @@ -using System.Linq; +using Umbraco.Cms.Core; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +internal class MediaPickerPreValueMigrator : DefaultPreValueMigrator //PreValueMigratorBase { - class MediaPickerPreValueMigrator : DefaultPreValueMigrator //PreValueMigratorBase + private readonly string[] _editors = { - private readonly string[] _editors = - { - Cms.Core.Constants.PropertyEditors.Legacy.Aliases.MediaPicker2, - Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker - }; + Constants.PropertyEditors.Legacy.Aliases.MediaPicker2, Constants.PropertyEditors.Aliases.MediaPicker + }; - public override bool CanMigrate(string editorAlias) - => _editors.Contains(editorAlias); + public override bool CanMigrate(string editorAlias) + => _editors.Contains(editorAlias); - public override string GetNewAlias(string editorAlias) - => Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker; + public override string GetNewAlias(string editorAlias) + => Constants.PropertyEditors.Aliases.MediaPicker; - // you wish - but MediaPickerConfiguration lives in Umbraco.Web - /* - public override object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) - { - return new MediaPickerConfiguration { ... }; - } - */ + // you wish - but MediaPickerConfiguration lives in Umbraco.Web + /* + public override object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) + { + return new MediaPickerConfiguration { ... }; + } + */ - protected override object? GetPreValueValue(PreValueDto preValue) + protected override object? GetPreValueValue(PreValueDto preValue) + { + if (preValue.Alias == "multiPicker" || + preValue.Alias == "onlyImages" || + preValue.Alias == "disableFolderSelect") { - if (preValue.Alias == "multiPicker" || - preValue.Alias == "onlyImages" || - preValue.Alias == "disableFolderSelect") - return preValue.Value == "1"; - - return base.GetPreValueValue(preValue); + return preValue.Value == "1"; } + + return base.GetPreValueValue(preValue); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/NestedContentPreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/NestedContentPreValueMigrator.cs index 761f55be4ef2..d8c1166febfa 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/NestedContentPreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/NestedContentPreValueMigrator.cs @@ -2,33 +2,38 @@ using Newtonsoft.Json; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +internal class NestedContentPreValueMigrator : DefaultPreValueMigrator //PreValueMigratorBase { - class NestedContentPreValueMigrator : DefaultPreValueMigrator //PreValueMigratorBase + public override bool CanMigrate(string editorAlias) + => editorAlias == "Umbraco.NestedContent"; + + // you wish - but NestedContentConfiguration lives in Umbraco.Web + /* + public override object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) { - public override bool CanMigrate(string editorAlias) - => editorAlias == "Umbraco.NestedContent"; + return new NestedContentConfiguration { ... }; + } + */ - // you wish - but NestedContentConfiguration lives in Umbraco.Web - /* - public override object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) + protected override object? GetPreValueValue(PreValueDto preValue) + { + if (preValue.Alias == "confirmDeletes" || + preValue.Alias == "showIcons" || + preValue.Alias == "hideLabel") { - return new NestedContentConfiguration { ... }; + return preValue.Value == "1"; } - */ - protected override object? GetPreValueValue(PreValueDto preValue) + if (preValue.Alias == "minItems" || + preValue.Alias == "maxItems") { - if (preValue.Alias == "confirmDeletes" || - preValue.Alias == "showIcons" || - preValue.Alias == "hideLabel") - return preValue.Value == "1"; - - if (preValue.Alias == "minItems" || - preValue.Alias == "maxItems") - return int.TryParse(preValue.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) ? (int?)i : null; - - return preValue.Value?.DetectIsJson() ?? false ? JsonConvert.DeserializeObject(preValue.Value) : preValue.Value; + return int.TryParse(preValue.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) + ? (int?)i + : null; } + + return preValue.Value?.DetectIsJson() ?? false ? JsonConvert.DeserializeObject(preValue.Value) : preValue.Value; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueDto.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueDto.cs index d3f4b0673787..34a242b49e05 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueDto.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueDto.cs @@ -1,24 +1,18 @@ using NPoco; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +[TableName("cmsDataTypePreValues")] +[ExplicitColumns] +public class PreValueDto { - [TableName("cmsDataTypePreValues")] - [ExplicitColumns] - public class PreValueDto - { - [Column("id")] - public int Id { get; set; } + [Column("id")] public int Id { get; set; } - [Column("datatypeNodeId")] - public int NodeId { get; set; } + [Column("datatypeNodeId")] public int NodeId { get; set; } - [Column("alias")] - public string Alias { get; set; } = null!; + [Column("alias")] public string Alias { get; set; } = null!; - [Column("sortorder")] - public int SortOrder { get; set; } + [Column("sortorder")] public int SortOrder { get; set; } - [Column("value")] - public string? Value { get; set; } - } + [Column("value")] public string? Value { get; set; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorBase.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorBase.cs index d4f5f4c42523..9f07df5acc38 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorBase.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorBase.cs @@ -1,20 +1,20 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +public abstract class PreValueMigratorBase : IPreValueMigrator { - public abstract class PreValueMigratorBase : IPreValueMigrator - { - public abstract bool CanMigrate(string editorAlias); + public abstract bool CanMigrate(string editorAlias); - public virtual string GetNewAlias(string editorAlias) - => editorAlias; + public virtual string GetNewAlias(string editorAlias) + => editorAlias; - public abstract object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues); + public abstract object GetConfiguration(int dataTypeId, string editorAlias, + Dictionary preValues); - protected bool GetBoolValue(Dictionary preValues, string alias, bool defaultValue = false) - => preValues.TryGetValue(alias, out var preValue) ? preValue.Value == "1" : defaultValue; + protected bool GetBoolValue(Dictionary preValues, string alias, bool defaultValue = false) + => preValues.TryGetValue(alias, out PreValueDto? preValue) ? preValue.Value == "1" : defaultValue; - protected decimal GetDecimalValue(Dictionary preValues, string alias, decimal defaultValue = 0) - => preValues.TryGetValue(alias, out var preValue) && decimal.TryParse(preValue.Value, out var value) ? value : defaultValue; - } + protected decimal GetDecimalValue(Dictionary preValues, string alias, decimal defaultValue = 0) + => preValues.TryGetValue(alias, out PreValueDto? preValue) && decimal.TryParse(preValue.Value, out var value) + ? value + : defaultValue; } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollection.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollection.cs index b304098188d9..aa3c09f80e6b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollection.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollection.cs @@ -1,27 +1,22 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +public class PreValueMigratorCollection : BuilderCollectionBase { - public class PreValueMigratorCollection : BuilderCollectionBase - { - private readonly ILogger _logger; + private readonly ILogger _logger; - public PreValueMigratorCollection(Func> items, ILogger logger) - : base(items) - { - _logger = logger; - } + public PreValueMigratorCollection(Func> items, + ILogger logger) + : base(items) => + _logger = logger; - public IPreValueMigrator? GetMigrator(string editorAlias) - { - var migrator = this.FirstOrDefault(x => x.CanMigrate(editorAlias)); - _logger.LogDebug("Getting migrator for \"{EditorAlias}\" = {MigratorType}", editorAlias, migrator == null ? "" : migrator.GetType().Name); - return migrator; - } + public IPreValueMigrator? GetMigrator(string editorAlias) + { + IPreValueMigrator? migrator = this.FirstOrDefault(x => x.CanMigrate(editorAlias)); + _logger.LogDebug("Getting migrator for \"{EditorAlias}\" = {MigratorType}", editorAlias, + migrator == null ? "" : migrator.GetType().Name); + return migrator; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollectionBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollectionBuilder.cs index 2c90a0d50465..ec98e0659eac 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollectionBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollectionBuilder.cs @@ -1,9 +1,9 @@ using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +public class PreValueMigratorCollectionBuilder : OrderedCollectionBuilderBase { - public class PreValueMigratorCollectionBuilder : OrderedCollectionBuilderBase - { - protected override PreValueMigratorCollectionBuilder This => this; - } + protected override PreValueMigratorCollectionBuilder This => this; } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/RenamingPreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/RenamingPreValueMigrator.cs index 5d05de56c3a4..f144ee405ac9 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/RenamingPreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/RenamingPreValueMigrator.cs @@ -1,27 +1,23 @@ -using System.Linq; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Exceptions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +internal class RenamingPreValueMigrator : DefaultPreValueMigrator { - class RenamingPreValueMigrator : DefaultPreValueMigrator - { - private readonly string[] _editors = - { - "Umbraco.NoEdit" - }; + private readonly string[] _editors = {"Umbraco.NoEdit"}; - public override bool CanMigrate(string editorAlias) - => _editors.Contains(editorAlias); + public override bool CanMigrate(string editorAlias) + => _editors.Contains(editorAlias); - public override string GetNewAlias(string editorAlias) + public override string GetNewAlias(string editorAlias) + { + switch (editorAlias) { - switch (editorAlias) - { - case "Umbraco.NoEdit": - return Cms.Core.Constants.PropertyEditors.Aliases.Label; - default: - throw new PanicException($"The alias {editorAlias} is not supported"); - } + case "Umbraco.NoEdit": + return Constants.PropertyEditors.Aliases.Label; + default: + throw new PanicException($"The alias {editorAlias} is not supported"); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/RichTextPreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/RichTextPreValueMigrator.cs index 0abcd86a962a..63c5792e8b60 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/RichTextPreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/RichTextPreValueMigrator.cs @@ -1,22 +1,24 @@ using Newtonsoft.Json; +using Umbraco.Cms.Core; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +internal class RichTextPreValueMigrator : DefaultPreValueMigrator { - class RichTextPreValueMigrator : DefaultPreValueMigrator - { - public override bool CanMigrate(string editorAlias) - => editorAlias == "Umbraco.TinyMCEv3"; + public override bool CanMigrate(string editorAlias) + => editorAlias == "Umbraco.TinyMCEv3"; - public override string GetNewAlias(string editorAlias) - => Cms.Core.Constants.PropertyEditors.Aliases.TinyMce; + public override string GetNewAlias(string editorAlias) + => Constants.PropertyEditors.Aliases.TinyMce; - protected override object? GetPreValueValue(PreValueDto preValue) + protected override object? GetPreValueValue(PreValueDto preValue) + { + if (preValue.Alias == "hideLabel") { - if (preValue.Alias == "hideLabel") - return preValue.Value == "1"; - - return preValue.Value?.DetectIsJson() ?? false ? JsonConvert.DeserializeObject(preValue.Value) : preValue.Value; + return preValue.Value == "1"; } + + return preValue.Value?.DetectIsJson() ?? false ? JsonConvert.DeserializeObject(preValue.Value) : preValue.Value; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/UmbracoSliderPreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/UmbracoSliderPreValueMigrator.cs index c193f2702847..02431a491131 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/UmbracoSliderPreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/UmbracoSliderPreValueMigrator.cs @@ -1,24 +1,21 @@ -using System.Collections.Generic; -using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +internal class UmbracoSliderPreValueMigrator : PreValueMigratorBase { - class UmbracoSliderPreValueMigrator : PreValueMigratorBase - { - public override bool CanMigrate(string editorAlias) - => editorAlias == "Umbraco.Slider"; + public override bool CanMigrate(string editorAlias) + => editorAlias == "Umbraco.Slider"; - public override object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) + public override object GetConfiguration(int dataTypeId, string editorAlias, + Dictionary preValues) => + new SliderConfiguration { - return new SliderConfiguration - { - EnableRange = GetBoolValue(preValues, "enableRange"), - InitialValue = GetDecimalValue(preValues, "initVal1"), - InitialValue2 = GetDecimalValue(preValues, "initVal2"), - MaximumValue = GetDecimalValue(preValues, "maxVal"), - MinimumValue = GetDecimalValue(preValues, "minVal"), - StepIncrements = GetDecimalValue(preValues, "step") - }; - } - } + EnableRange = GetBoolValue(preValues, "enableRange"), + InitialValue = GetDecimalValue(preValues, "initVal1"), + InitialValue2 = GetDecimalValue(preValues, "initVal2"), + MaximumValue = GetDecimalValue(preValues, "maxVal"), + MinimumValue = GetDecimalValue(preValues, "minVal"), + StepIncrements = GetDecimalValue(preValues, "step") + }; } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs index 44b12addd243..37532928f1f1 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs @@ -1,33 +1,29 @@ -using System.Collections.Generic; -using System.Linq; -using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +internal class ValueListPreValueMigrator : IPreValueMigrator { - class ValueListPreValueMigrator : IPreValueMigrator + private readonly string[] _editors = { - private readonly string[] _editors = - { - "Umbraco.RadioButtonList", - "Umbraco.CheckBoxList", - "Umbraco.DropDown", - "Umbraco.DropdownlistPublishingKeys", - "Umbraco.DropDownMultiple", - "Umbraco.DropdownlistMultiplePublishKeys" - }; + "Umbraco.RadioButtonList", "Umbraco.CheckBoxList", "Umbraco.DropDown", "Umbraco.DropdownlistPublishingKeys", + "Umbraco.DropDownMultiple", "Umbraco.DropdownlistMultiplePublishKeys" + }; - public bool CanMigrate(string editorAlias) - => _editors.Contains(editorAlias); + public bool CanMigrate(string editorAlias) + => _editors.Contains(editorAlias); - public virtual string? GetNewAlias(string editorAlias) - => null; + public virtual string? GetNewAlias(string editorAlias) + => null; - public object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) + public object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) + { + var config = new ValueListConfiguration(); + foreach (PreValueDto preValue in preValues.Values) { - var config = new ValueListConfiguration(); - foreach (var preValue in preValues.Values) - config.Items.Add(new ValueListConfiguration.ValueListItem { Id = preValue.Id, Value = preValue.Value }); - return config; + config.Items.Add(new ValueListConfiguration.ValueListItem {Id = preValue.Id, Value = preValue.Value}); } + + return config; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs index 0d4b6020a99f..acbe474108bd 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs @@ -1,8 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; @@ -13,125 +11,132 @@ using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class DropDownPropertyEditorsMigration : PropertyEditorsMigrationBase { - public class DropDownPropertyEditorsMigration : PropertyEditorsMigrationBase + private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer; + private readonly IEditorConfigurationParser _editorConfigurationParser; + private readonly IIOHelper _ioHelper; + + public DropDownPropertyEditorsMigration(IMigrationContext context, IIOHelper ioHelper, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) + : this(context, ioHelper, configurationEditorJsonSerializer, + StaticServiceProvider.Instance.GetRequiredService()) { - private readonly IIOHelper _ioHelper; - private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer; - private readonly IEditorConfigurationParser _editorConfigurationParser; + } - public DropDownPropertyEditorsMigration(IMigrationContext context, IIOHelper ioHelper, IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) - : this(context, ioHelper, configurationEditorJsonSerializer, StaticServiceProvider.Instance.GetRequiredService()) - { - } + public DropDownPropertyEditorsMigration( + IMigrationContext context, + IIOHelper ioHelper, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer, + IEditorConfigurationParser editorConfigurationParser) + : base(context) + { + _ioHelper = ioHelper; + _configurationEditorJsonSerializer = configurationEditorJsonSerializer; + _editorConfigurationParser = editorConfigurationParser; + } - public DropDownPropertyEditorsMigration( - IMigrationContext context, - IIOHelper ioHelper, - IConfigurationEditorJsonSerializer configurationEditorJsonSerializer, - IEditorConfigurationParser editorConfigurationParser) - : base(context) - { - _ioHelper = ioHelper; - _configurationEditorJsonSerializer = configurationEditorJsonSerializer; - _editorConfigurationParser = editorConfigurationParser; - } + protected override void Migrate() + { + var refreshCache = Migrate(GetDataTypes(".DropDown", false)); - protected override void Migrate() + // if some data types have been updated directly in the database (editing DataTypeDto and/or PropertyDataDto), + // bypassing the services, then we need to rebuild the cache entirely, including the umbracoContentNu table + if (refreshCache) { - var refreshCache = Migrate(GetDataTypes(".DropDown", false)); - - // if some data types have been updated directly in the database (editing DataTypeDto and/or PropertyDataDto), - // bypassing the services, then we need to rebuild the cache entirely, including the umbracoContentNu table - if (refreshCache) - Context.AddPostMigration(); + Context.AddPostMigration(); } + } + + private bool Migrate(IEnumerable dataTypes) + { + var refreshCache = false; + ConfigurationEditor? configurationEditor = null; - private bool Migrate(IEnumerable dataTypes) + foreach (DataTypeDto dataType in dataTypes) { - var refreshCache = false; - ConfigurationEditor? configurationEditor = null; + ValueListConfiguration config; - foreach (var dataType in dataTypes) + if (!dataType.Configuration.IsNullOrWhiteSpace()) { - ValueListConfiguration config; + // parse configuration, and update everything accordingly + if (configurationEditor == null) + { + configurationEditor = new ValueListConfigurationEditor(_ioHelper, _editorConfigurationParser); + } - if (!dataType.Configuration.IsNullOrWhiteSpace()) + try { - // parse configuration, and update everything accordingly - if (configurationEditor == null) - configurationEditor = new ValueListConfigurationEditor(_ioHelper, _editorConfigurationParser); - try - { - config = (ValueListConfiguration) configurationEditor.FromDatabase(dataType.Configuration, _configurationEditorJsonSerializer); - } - catch (Exception ex) - { - Logger.LogError( - ex, "Invalid configuration: \"{Configuration}\", cannot convert editor.", - dataType.Configuration); - - // reset - config = new ValueListConfiguration(); - } - - // get property data dtos - var propertyDataDtos = Database.Fetch(Sql() - .Select() - .From() - .InnerJoin().On((pt, pd) => pt.Id == pd.PropertyTypeId) - .InnerJoin().On((dt, pt) => dt.NodeId == pt.DataTypeId) - .Where(x => x.DataTypeId == dataType.NodeId)); - - // update dtos - var updatedDtos = propertyDataDtos.Where(x => UpdatePropertyDataDto(x, config, true)); - - // persist changes - foreach (var propertyDataDto in updatedDtos) - Database.Update(propertyDataDto); + config = (ValueListConfiguration)configurationEditor.FromDatabase(dataType.Configuration, + _configurationEditorJsonSerializer); } - else + catch (Exception ex) { - // default configuration + Logger.LogError( + ex, "Invalid configuration: \"{Configuration}\", cannot convert editor.", + dataType.Configuration); + + // reset config = new ValueListConfiguration(); } - switch (dataType.EditorAlias) + // get property data dtos + List? propertyDataDtos = Database.Fetch(Sql() + .Select() + .From() + .InnerJoin() + .On((pt, pd) => pt.Id == pd.PropertyTypeId) + .InnerJoin().On((dt, pt) => dt.NodeId == pt.DataTypeId) + .Where(x => x.DataTypeId == dataType.NodeId)); + + // update dtos + IEnumerable updatedDtos = + propertyDataDtos.Where(x => UpdatePropertyDataDto(x, config, true)); + + // persist changes + foreach (PropertyDataDto? propertyDataDto in updatedDtos) { - case string ea when ea.InvariantEquals("Umbraco.DropDown"): - UpdateDataType(dataType, config, false); - break; - case string ea when ea.InvariantEquals("Umbraco.DropdownlistPublishingKeys"): - UpdateDataType(dataType, config, false); - break; - case string ea when ea.InvariantEquals("Umbraco.DropDownMultiple"): - UpdateDataType(dataType, config, true); - break; - case string ea when ea.InvariantEquals("Umbraco.DropdownlistMultiplePublishKeys"): - UpdateDataType(dataType, config, true); - break; + Database.Update(propertyDataDto); } + } + else + { + // default configuration + config = new ValueListConfiguration(); + } - refreshCache = true; + switch (dataType.EditorAlias) + { + case string ea when ea.InvariantEquals("Umbraco.DropDown"): + UpdateDataType(dataType, config, false); + break; + case string ea when ea.InvariantEquals("Umbraco.DropdownlistPublishingKeys"): + UpdateDataType(dataType, config, false); + break; + case string ea when ea.InvariantEquals("Umbraco.DropDownMultiple"): + UpdateDataType(dataType, config, true); + break; + case string ea when ea.InvariantEquals("Umbraco.DropdownlistMultiplePublishKeys"): + UpdateDataType(dataType, config, true); + break; } - return refreshCache; + refreshCache = true; } - private void UpdateDataType(DataTypeDto dataType, ValueListConfiguration config, bool isMultiple) - { - dataType.DbType = ValueStorageType.Nvarchar.ToString(); - dataType.EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.DropDownListFlexible; + return refreshCache; + } - var flexConfig = new DropDownFlexibleConfiguration - { - Items = config.Items, - Multiple = isMultiple - }; - dataType.Configuration = ConfigurationEditor.ToDatabase(flexConfig, _configurationEditorJsonSerializer); + private void UpdateDataType(DataTypeDto dataType, ValueListConfiguration config, bool isMultiple) + { + dataType.DbType = ValueStorageType.Nvarchar.ToString(); + dataType.EditorAlias = Constants.PropertyEditors.Aliases.DropDownListFlexible; - Database.Update(dataType); - } + var flexConfig = new DropDownFlexibleConfiguration {Items = config.Items, Multiple = isMultiple}; + dataType.Configuration = ConfigurationEditor.ToDatabase(flexConfig, _configurationEditorJsonSerializer); + + Database.Update(dataType); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropMigrationsTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropMigrationsTable.cs index 0d1e0506cbe6..1452b010376d 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropMigrationsTable.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropMigrationsTable.cs @@ -1,15 +1,17 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class DropMigrationsTable : MigrationBase { - public class DropMigrationsTable : MigrationBase + public DropMigrationsTable(IMigrationContext context) + : base(context) { - public DropMigrationsTable(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() + protected override void Migrate() + { + if (TableExists("umbracoMigration")) { - if (TableExists("umbracoMigration")) - Delete.Table("umbracoMigration").Do(); + Delete.Table("umbracoMigration").Do(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropPreValueTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropPreValueTable.cs index 0195e51e6e82..59e79c42938a 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropPreValueTable.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropPreValueTable.cs @@ -1,15 +1,17 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class DropPreValueTable : MigrationBase { - public class DropPreValueTable : MigrationBase + public DropPreValueTable(IMigrationContext context) : base(context) { - public DropPreValueTable(IMigrationContext context) : base(context) - { } + } - protected override void Migrate() + protected override void Migrate() + { + // drop preValues table + if (TableExists("cmsDataTypePreValues")) { - // drop preValues table - if (TableExists("cmsDataTypePreValues")) - Delete.Table("cmsDataTypePreValues").Do(); + Delete.Table("cmsDataTypePreValues").Do(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropTaskTables.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropTaskTables.cs index b4004c1c8290..ddf3ea7fff59 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropTaskTables.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropTaskTables.cs @@ -1,17 +1,22 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class DropTaskTables : MigrationBase { - public class DropTaskTables : MigrationBase + public DropTaskTables(IMigrationContext context) + : base(context) + { + } + + protected override void Migrate() { - public DropTaskTables(IMigrationContext context) - : base(context) - { } + if (TableExists("cmsTask")) + { + Delete.Table("cmsTask").Do(); + } - protected override void Migrate() + if (TableExists("cmsTaskType")) { - if (TableExists("cmsTask")) - Delete.Table("cmsTask").Do(); - if (TableExists("cmsTaskType")) - Delete.Table("cmsTaskType").Do(); + Delete.Table("cmsTaskType").Do(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropTemplateDesignColumn.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropTemplateDesignColumn.cs index 9f65689a59e5..c3e5a58727d5 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropTemplateDesignColumn.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropTemplateDesignColumn.cs @@ -1,15 +1,17 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class DropTemplateDesignColumn : MigrationBase { - public class DropTemplateDesignColumn : MigrationBase + public DropTemplateDesignColumn(IMigrationContext context) + : base(context) { - public DropTemplateDesignColumn(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() + protected override void Migrate() + { + if (ColumnExists("cmsTemplate", "design")) { - if(ColumnExists("cmsTemplate", "design")) - Delete.Column("design").FromTable("cmsTemplate").Do(); + Delete.Column("design").FromTable("cmsTemplate").Do(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropXmlTables.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropXmlTables.cs index 3e86e142aabd..b16b85012ea2 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropXmlTables.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropXmlTables.cs @@ -1,17 +1,22 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class DropXmlTables : MigrationBase { - public class DropXmlTables : MigrationBase + public DropXmlTables(IMigrationContext context) + : base(context) + { + } + + protected override void Migrate() { - public DropXmlTables(IMigrationContext context) - : base(context) - { } + if (TableExists("cmsContentXml")) + { + Delete.Table("cmsContentXml").Do(); + } - protected override void Migrate() + if (TableExists("cmsPreviewXml")) { - if (TableExists("cmsContentXml")) - Delete.Table("cmsContentXml").Do(); - if (TableExists("cmsPreviewXml")) - Delete.Table("cmsPreviewXml").Do(); + Delete.Table("cmsPreviewXml").Do(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/FallbackLanguage.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/FallbackLanguage.cs index 48e00df2ffd2..861ecf5e906c 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/FallbackLanguage.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/FallbackLanguage.cs @@ -1,25 +1,30 @@ -using System.Linq; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +/// +/// Adds a new, self-joined field to umbracoLanguages to hold the fall-back language for +/// a given language. +/// +public class FallbackLanguage : MigrationBase { - /// - /// Adds a new, self-joined field to umbracoLanguages to hold the fall-back language for - /// a given language. - /// - public class FallbackLanguage : MigrationBase + public FallbackLanguage(IMigrationContext context) + : base(context) { - public FallbackLanguage(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray(); + protected override void Migrate() + { + ColumnInfo[] columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray(); - if (columns.Any(x => x.TableName.InvariantEquals(Cms.Core.Constants.DatabaseSchema.Tables.Language) && x.ColumnName.InvariantEquals("fallbackLanguageId")) == false) - AddColumn("fallbackLanguageId"); + if (columns.Any(x => + x.TableName.InvariantEquals(Constants.DatabaseSchema.Tables.Language) && + x.ColumnName.InvariantEquals("fallbackLanguageId")) == false) + { + AddColumn("fallbackLanguageId"); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/FixLanguageIsoCodeLength.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/FixLanguageIsoCodeLength.cs index 7a35dc12ed26..780dc3d02152 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/FixLanguageIsoCodeLength.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/FixLanguageIsoCodeLength.cs @@ -1,21 +1,18 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class FixLanguageIsoCodeLength : MigrationBase { - public class FixLanguageIsoCodeLength : MigrationBase + public FixLanguageIsoCodeLength(IMigrationContext context) + : base(context) { - public FixLanguageIsoCodeLength(IMigrationContext context) - : base(context) - { } - - protected override void Migrate() - { - // there is some confusion here when upgrading from v7 - // it should be 14 already but that's not always the case - - Alter.Table("umbracoLanguage") - .AlterColumn("languageISOCode") - .AsString(14) - .Nullable() - .Do(); - } } + + protected override void Migrate() => + // there is some confusion here when upgrading from v7 + // it should be 14 already but that's not always the case + Alter.Table("umbracoLanguage") + .AlterColumn("languageISOCode") + .AsString(14) + .Nullable() + .Do(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/LanguageColumns.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/LanguageColumns.cs index f6aa86259fc3..2d562db62757 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/LanguageColumns.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/LanguageColumns.cs @@ -1,17 +1,18 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class LanguageColumns : MigrationBase { - public class LanguageColumns : MigrationBase + public LanguageColumns(IMigrationContext context) + : base(context) { - public LanguageColumns(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - AddColumn(Cms.Core.Constants.DatabaseSchema.Tables.Language, "isDefaultVariantLang"); - AddColumn(Cms.Core.Constants.DatabaseSchema.Tables.Language, "mandatory"); - } + protected override void Migrate() + { + AddColumn(Constants.DatabaseSchema.Tables.Language, "isDefaultVariantLang"); + AddColumn(Constants.DatabaseSchema.Tables.Language, "mandatory"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MakeRedirectUrlVariant.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MakeRedirectUrlVariant.cs index 7958f4fbf83a..25dd67b4cf91 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MakeRedirectUrlVariant.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MakeRedirectUrlVariant.cs @@ -1,16 +1,13 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class MakeRedirectUrlVariant : MigrationBase { - public class MakeRedirectUrlVariant : MigrationBase + public MakeRedirectUrlVariant(IMigrationContext context) + : base(context) { - public MakeRedirectUrlVariant(IMigrationContext context) - : base(context) - { } - - protected override void Migrate() - { - AddColumn("culture"); - } } + + protected override void Migrate() => AddColumn("culture"); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MakeTagsVariant.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MakeTagsVariant.cs index 74cdd88357fc..41a6f04eb2a9 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MakeTagsVariant.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MakeTagsVariant.cs @@ -1,16 +1,13 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class MakeTagsVariant : MigrationBase { - public class MakeTagsVariant : MigrationBase + public MakeTagsVariant(IMigrationContext context) + : base(context) { - public MakeTagsVariant(IMigrationContext context) - : base(context) - { } - - protected override void Migrate() - { - AddColumn("languageId"); - } } + + protected override void Migrate() => AddColumn("languageId"); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs index db7766213c55..062db56600f6 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs @@ -1,7 +1,6 @@ -using System; -using System.Collections.Generic; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Serialization; @@ -10,87 +9,90 @@ using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class MergeDateAndDateTimePropertyEditor : MigrationBase { - public class MergeDateAndDateTimePropertyEditor : MigrationBase + private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer; + private readonly IEditorConfigurationParser _editorConfigurationParser; + private readonly IIOHelper _ioHelper; + + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public MergeDateAndDateTimePropertyEditor(IMigrationContext context, IIOHelper ioHelper, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) + : this(context, ioHelper, configurationEditorJsonSerializer, + StaticServiceProvider.Instance.GetRequiredService()) { - private readonly IIOHelper _ioHelper; - private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer; - private readonly IEditorConfigurationParser _editorConfigurationParser; + } - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public MergeDateAndDateTimePropertyEditor(IMigrationContext context, IIOHelper ioHelper, IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) - : this(context, ioHelper, configurationEditorJsonSerializer, StaticServiceProvider.Instance.GetRequiredService()) - { - } + public MergeDateAndDateTimePropertyEditor(IMigrationContext context, IIOHelper ioHelper, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer, + IEditorConfigurationParser editorConfigurationParser) + : base(context) + { + _ioHelper = ioHelper; + _configurationEditorJsonSerializer = configurationEditorJsonSerializer; + _editorConfigurationParser = editorConfigurationParser; + } - public MergeDateAndDateTimePropertyEditor(IMigrationContext context, IIOHelper ioHelper, IConfigurationEditorJsonSerializer configurationEditorJsonSerializer, IEditorConfigurationParser editorConfigurationParser) - : base(context) - { - _ioHelper = ioHelper; - _configurationEditorJsonSerializer = configurationEditorJsonSerializer; - _editorConfigurationParser = editorConfigurationParser; - } + protected override void Migrate() + { + List dataTypes = GetDataTypes(Constants.PropertyEditors.Legacy.Aliases.Date); - protected override void Migrate() + foreach (DataTypeDto dataType in dataTypes) { - var dataTypes = GetDataTypes(Cms.Core.Constants.PropertyEditors.Legacy.Aliases.Date); - - foreach (var dataType in dataTypes) + DateTimeConfiguration config; + try { - DateTimeConfiguration config; - try + config = (DateTimeConfiguration)new CustomDateTimeConfigurationEditor(_ioHelper, + _editorConfigurationParser).FromDatabase( + dataType.Configuration, _configurationEditorJsonSerializer); + + // If the Umbraco.Date type is the default from V7 and it has never been updated, then the + // configuration is empty, and the format stuff is handled by in JS by moment.js. - We can't do that + // after the migration, so we force the format to the default from V7. + if (string.IsNullOrEmpty(dataType.Configuration)) { - config = (DateTimeConfiguration) new CustomDateTimeConfigurationEditor(_ioHelper, _editorConfigurationParser).FromDatabase( - dataType.Configuration, _configurationEditorJsonSerializer); - - // If the Umbraco.Date type is the default from V7 and it has never been updated, then the - // configuration is empty, and the format stuff is handled by in JS by moment.js. - We can't do that - // after the migration, so we force the format to the default from V7. - if (string.IsNullOrEmpty(dataType.Configuration)) - { - config.Format = "YYYY-MM-DD"; - } - } - catch (Exception ex) - { - Logger.LogError( - ex, - "Invalid property editor configuration detected: \"{Configuration}\", cannot convert editor, values will be cleared", - dataType.Configuration); - - continue; + config.Format = "YYYY-MM-DD"; } + } + catch (Exception ex) + { + Logger.LogError( + ex, + "Invalid property editor configuration detected: \"{Configuration}\", cannot convert editor, values will be cleared", + dataType.Configuration); - config.OffsetTime = false; - - dataType.EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.DateTime; - dataType.Configuration = ConfigurationEditor.ToDatabase(config, _configurationEditorJsonSerializer); - - Database.Update(dataType); + continue; } - } + config.OffsetTime = false; + dataType.EditorAlias = Constants.PropertyEditors.Aliases.DateTime; + dataType.Configuration = ConfigurationEditor.ToDatabase(config, _configurationEditorJsonSerializer); - private List GetDataTypes(string editorAlias) - { - //need to convert the old drop down data types to use the new one - var dataTypes = Database.Fetch(Sql() - .Select() - .From() - .Where(x => x.EditorAlias == editorAlias)); - return dataTypes; + Database.Update(dataType); } + } + private List GetDataTypes(string editorAlias) + { + //need to convert the old drop down data types to use the new one + List? dataTypes = Database.Fetch(Sql() + .Select() + .From() + .Where(x => x.EditorAlias == editorAlias)); + return dataTypes; + } + - private class CustomDateTimeConfigurationEditor : ConfigurationEditor + private class CustomDateTimeConfigurationEditor : ConfigurationEditor + { + public CustomDateTimeConfigurationEditor(IIOHelper ioHelper, + IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) { - public CustomDateTimeConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - } } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/Models/ContentTypeDto80.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/Models/ContentTypeDto80.cs index bbd1646ad529..7baabbcb67f1 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/Models/ContentTypeDto80.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/Models/ContentTypeDto80.cs @@ -1,63 +1,62 @@ using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.Models +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.Models; + +/// +/// Snapshot of the as it was at version 8.0 +/// +/// +/// This is required during migrations the schema of this table changed and running SQL against the new table would +/// result in errors +/// +[TableName(TableName)] +[PrimaryKey("pk")] +[ExplicitColumns] +internal class ContentTypeDto80 { + public const string TableName = Constants.DatabaseSchema.Tables.ContentType; - /// - /// Snapshot of the as it was at version 8.0 - /// - /// - /// This is required during migrations the schema of this table changed and running SQL against the new table would result in errors - /// - [TableName(TableName)] - [PrimaryKey("pk")] - [ExplicitColumns] - internal class ContentTypeDto80 - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.ContentType; - - [Column("pk")] - [PrimaryKeyColumn(IdentitySeed = 535)] - public int PrimaryKey { get; set; } - - [Column("nodeId")] - [ForeignKey(typeof(NodeDto))] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsContentType")] - public int NodeId { get; set; } - - [Column("alias")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Alias { get; set; } - - [Column("icon")] - [Index(IndexTypes.NonClustered)] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Icon { get; set; } - - [Column("thumbnail")] - [Constraint(Default = "folder.png")] - public string? Thumbnail { get; set; } - - [Column("description")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(1500)] - public string? Description { get; set; } - - [Column("isContainer")] - [Constraint(Default = "0")] - public bool IsContainer { get; set; } - - [Column("allowAtRoot")] - [Constraint(Default = "0")] - public bool AllowAtRoot { get; set; } - - [Column("variations")] - [Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)] - public byte Variations { get; set; } - - [ResultColumn] - public NodeDto? NodeDto { get; set; } - } + [Column("pk")] + [PrimaryKeyColumn(IdentitySeed = 535)] + public int PrimaryKey { get; set; } + + [Column("nodeId")] + [ForeignKey(typeof(NodeDto))] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsContentType")] + public int NodeId { get; set; } + + [Column("alias")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Alias { get; set; } + + [Column("icon")] + [Index(IndexTypes.NonClustered)] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Icon { get; set; } + + [Column("thumbnail")] + [Constraint(Default = "folder.png")] + public string? Thumbnail { get; set; } + + [Column("description")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(1500)] + public string? Description { get; set; } + + [Column("isContainer")] + [Constraint(Default = "0")] + public bool IsContainer { get; set; } + + [Column("allowAtRoot")] + [Constraint(Default = "0")] + public bool AllowAtRoot { get; set; } + + [Column("variations")] + [Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)] + public byte Variations { get; set; } + + [ResultColumn] public NodeDto? NodeDto { get; set; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/Models/PropertyDataDto80.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/Models/PropertyDataDto80.cs index 1e9e93aa5313..4c537472db66 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/Models/PropertyDataDto80.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/Models/PropertyDataDto80.cs @@ -1,143 +1,142 @@ -using System; -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.Models +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.Models; + +/// +/// Snapshot of the as it was at version 8.0 +/// +/// +/// This is required during migrations the schema of this table changed and running SQL against the new table would +/// result in errors +/// +[TableName(TableName)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class PropertyDataDto80 { - /// - /// Snapshot of the as it was at version 8.0 - /// - /// - /// This is required during migrations the schema of this table changed and running SQL against the new table would result in errors - /// - [TableName(TableName)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class PropertyDataDto80 + public const string TableName = Constants.DatabaseSchema.Tables.PropertyData; + public const int VarcharLength = 512; + public const int SegmentLength = 256; + + private decimal? _decimalValue; + + // pk, not used at the moment (never updating) + [Column("id")] [PrimaryKeyColumn] public int Id { get; set; } + + [Column("versionId")] + [ForeignKey(typeof(ContentVersionDto))] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_VersionId", + ForColumns = "versionId,propertyTypeId,languageId,segment")] + public int VersionId { get; set; } + + [Column("propertyTypeId")] + [ForeignKey(typeof(PropertyTypeDto80))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_PropertyTypeId")] + public int PropertyTypeId { get; set; } + + [Column("languageId")] + [ForeignKey(typeof(LanguageDto))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? LanguageId { get; set; } + + [Column("segment")] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Segment")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(SegmentLength)] + public string? Segment { get; set; } + + [Column("intValue")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? IntegerValue { get; set; } + + [Column("decimalValue")] + [NullSetting(NullSetting = NullSettings.Null)] + public decimal? DecimalValue { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.PropertyData; - public const int VarcharLength = 512; - public const int SegmentLength = 256; - - private decimal? _decimalValue; - - // pk, not used at the moment (never updating) - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } - - [Column("versionId")] - [ForeignKey(typeof(ContentVersionDto))] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_VersionId", ForColumns = "versionId,propertyTypeId,languageId,segment")] - public int VersionId { get; set; } - - [Column("propertyTypeId")] - [ForeignKey(typeof(PropertyTypeDto80))] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_PropertyTypeId")] - public int PropertyTypeId { get; set; } - - [Column("languageId")] - [ForeignKey(typeof(LanguageDto))] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] - [NullSetting(NullSetting = NullSettings.Null)] - public int? LanguageId { get; set; } - - [Column("segment")] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Segment")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(SegmentLength)] - public string? Segment { get; set; } - - [Column("intValue")] - [NullSetting(NullSetting = NullSettings.Null)] - public int? IntegerValue { get; set; } - - [Column("decimalValue")] - [NullSetting(NullSetting = NullSettings.Null)] - public decimal? DecimalValue - { - get => _decimalValue; - set => _decimalValue = value?.Normalize(); - } + get => _decimalValue; + set => _decimalValue = value?.Normalize(); + } - [Column("dateValue")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? DateValue { get; set; } + [Column("dateValue")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? DateValue { get; set; } - [Column("varcharValue")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(VarcharLength)] - public string? VarcharValue { get; set; } + [Column("varcharValue")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(VarcharLength)] + public string? VarcharValue { get; set; } - [Column("textValue")] - [NullSetting(NullSetting = NullSettings.Null)] - [SpecialDbType(SpecialDbTypes.NTEXT)] - public string? TextValue { get; set; } + [Column("textValue")] + [NullSetting(NullSetting = NullSettings.Null)] + [SpecialDbType(SpecialDbTypes.NTEXT)] + public string? TextValue { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne, ColumnName = "PropertyTypeId")] - public PropertyTypeDto80? PropertyTypeDto { get; set; } + [ResultColumn] + [Reference(ReferenceType.OneToOne, ColumnName = "PropertyTypeId")] + public PropertyTypeDto80? PropertyTypeDto { get; set; } - [Ignore] - public object? Value + [Ignore] + public object? Value + { + get { - get + if (IntegerValue.HasValue) { - if (IntegerValue.HasValue) - return IntegerValue.Value; - - if (DecimalValue.HasValue) - return DecimalValue.Value; - - if (DateValue.HasValue) - return DateValue.Value; - - if (!string.IsNullOrEmpty(VarcharValue)) - return VarcharValue; + return IntegerValue.Value; + } - if (!string.IsNullOrEmpty(TextValue)) - return TextValue; + if (DecimalValue.HasValue) + { + return DecimalValue.Value; + } - return null; + if (DateValue.HasValue) + { + return DateValue.Value; } - } - public PropertyDataDto80 Clone(int versionId) - { - return new PropertyDataDto80 + if (!string.IsNullOrEmpty(VarcharValue)) { - VersionId = versionId, - PropertyTypeId = PropertyTypeId, - LanguageId = LanguageId, - Segment = Segment, - IntegerValue = IntegerValue, - DecimalValue = DecimalValue, - DateValue = DateValue, - VarcharValue = VarcharValue, - TextValue = TextValue, - PropertyTypeDto = PropertyTypeDto - }; - } + return VarcharValue; + } - protected bool Equals(PropertyDataDto other) - { - return Id == other.Id; - } + if (!string.IsNullOrEmpty(TextValue)) + { + return TextValue; + } - public override bool Equals(object? other) - { - return - !ReferenceEquals(null, other) // other is not null - && (ReferenceEquals(this, other) // and either ref-equals, or same id - || other is PropertyDataDto pdata && pdata.Id == Id); + return null; } + } - public override int GetHashCode() + public PropertyDataDto80 Clone(int versionId) => + new PropertyDataDto80 { - // ReSharper disable once NonReadonlyMemberInGetHashCode - return Id; - } - } + VersionId = versionId, + PropertyTypeId = PropertyTypeId, + LanguageId = LanguageId, + Segment = Segment, + IntegerValue = IntegerValue, + DecimalValue = DecimalValue, + DateValue = DateValue, + VarcharValue = VarcharValue, + TextValue = TextValue, + PropertyTypeDto = PropertyTypeDto + }; + + protected bool Equals(PropertyDataDto other) => Id == other.Id; + + public override bool Equals(object? other) => + !ReferenceEquals(null, other) // other is not null + && (ReferenceEquals(this, other) // and either ref-equals, or same id + || (other is PropertyDataDto pdata && pdata.Id == Id)); + + public override int GetHashCode() => + // ReSharper disable once NonReadonlyMemberInGetHashCode + Id; } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/Models/PropertyTypeDto80.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/Models/PropertyTypeDto80.cs index 4d61521d00ca..c1dae094ea8b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/Models/PropertyTypeDto80.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/Models/PropertyTypeDto80.cs @@ -1,76 +1,76 @@ -using System; -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.Models +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.Models; + +/// +/// Snapshot of the as it was at version 8.0 +/// +/// +/// This is required during migrations before 8.6 since the schema has changed and running SQL against the new table +/// would result in errors +/// +[TableName(Constants.DatabaseSchema.Tables.PropertyType)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class PropertyTypeDto80 { - /// - /// Snapshot of the as it was at version 8.0 - /// - /// - /// This is required during migrations before 8.6 since the schema has changed and running SQL against the new table would result in errors - /// - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class PropertyTypeDto80 - { - [Column("id")] - [PrimaryKeyColumn(IdentitySeed = 50)] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn(IdentitySeed = 50)] + public int Id { get; set; } - [Column("dataTypeId")] - [ForeignKey(typeof(DataTypeDto), Column = "nodeId")] - public int DataTypeId { get; set; } + [Column("dataTypeId")] + [ForeignKey(typeof(DataTypeDto), Column = "nodeId")] + public int DataTypeId { get; set; } - [Column("contentTypeId")] - [ForeignKey(typeof(ContentTypeDto), Column = "nodeId")] - public int ContentTypeId { get; set; } + [Column("contentTypeId")] + [ForeignKey(typeof(ContentTypeDto), Column = "nodeId")] + public int ContentTypeId { get; set; } - [Column("propertyTypeGroupId")] - [NullSetting(NullSetting = NullSettings.Null)] - [ForeignKey(typeof(PropertyTypeGroupDto))] - public int? PropertyTypeGroupId { get; set; } + [Column("propertyTypeGroupId")] + [NullSetting(NullSetting = NullSettings.Null)] + [ForeignKey(typeof(PropertyTypeGroupDto))] + public int? PropertyTypeGroupId { get; set; } - [Index(IndexTypes.NonClustered, Name = "IX_cmsPropertyTypeAlias")] - [Column("Alias")] - public string Alias { get; set; } = null!; + [Index(IndexTypes.NonClustered, Name = "IX_cmsPropertyTypeAlias")] + [Column("Alias")] + public string Alias { get; set; } = null!; - [Column("Name")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Name { get; set; } + [Column("Name")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Name { get; set; } - [Column("sortOrder")] - [Constraint(Default = "0")] - public int SortOrder { get; set; } + [Column("sortOrder")] + [Constraint(Default = "0")] + public int SortOrder { get; set; } - [Column("mandatory")] - [Constraint(Default = "0")] - public bool Mandatory { get; set; } + [Column("mandatory")] + [Constraint(Default = "0")] + public bool Mandatory { get; set; } - [Column("validationRegExp")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? ValidationRegExp { get; set; } + [Column("validationRegExp")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? ValidationRegExp { get; set; } - [Column("Description")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(2000)] - public string? Description { get; set; } + [Column("Description")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(2000)] + public string? Description { get; set; } - [Column("variations")] - [Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)] - public byte Variations { get; set; } + [Column("variations")] + [Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)] + public byte Variations { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne, ColumnName = "DataTypeId")] - public DataTypeDto? DataTypeDto { get; set; } + [ResultColumn] + [Reference(ReferenceType.OneToOne, ColumnName = "DataTypeId")] + public DataTypeDto? DataTypeDto { get; set; } - [Column("UniqueID")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Constraint(Default = SystemMethods.NewGuid)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsPropertyTypeUniqueID")] - public Guid UniqueId { get; set; } - } + [Column("UniqueID")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.NewGuid)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsPropertyTypeUniqueID")] + public Guid UniqueId { get; set; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs index 935d51dacde5..0cc036f90628 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs @@ -1,54 +1,59 @@ -using System; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class PropertyEditorsMigration : MigrationBase { - public class PropertyEditorsMigration : MigrationBase + public PropertyEditorsMigration(IMigrationContext context) + : base(context) { - public PropertyEditorsMigration(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - RenameDataType(Cms.Core.Constants.PropertyEditors.Legacy.Aliases.ContentPicker2, Cms.Core.Constants.PropertyEditors.Aliases.ContentPicker); - RenameDataType(Cms.Core.Constants.PropertyEditors.Legacy.Aliases.MediaPicker2, Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker); - RenameDataType(Cms.Core.Constants.PropertyEditors.Legacy.Aliases.MemberPicker2, Cms.Core.Constants.PropertyEditors.Aliases.MemberPicker); - RenameDataType(Cms.Core.Constants.PropertyEditors.Legacy.Aliases.MultiNodeTreePicker2, Cms.Core.Constants.PropertyEditors.Aliases.MultiNodeTreePicker); - RenameDataType(Cms.Core.Constants.PropertyEditors.Legacy.Aliases.TextboxMultiple, Cms.Core.Constants.PropertyEditors.Aliases.TextArea, false); - RenameDataType(Cms.Core.Constants.PropertyEditors.Legacy.Aliases.Textbox, Cms.Core.Constants.PropertyEditors.Aliases.TextBox, false); - } + protected override void Migrate() + { + RenameDataType(Constants.PropertyEditors.Legacy.Aliases.ContentPicker2, + Constants.PropertyEditors.Aliases.ContentPicker); + RenameDataType(Constants.PropertyEditors.Legacy.Aliases.MediaPicker2, + Constants.PropertyEditors.Aliases.MediaPicker); + RenameDataType(Constants.PropertyEditors.Legacy.Aliases.MemberPicker2, + Constants.PropertyEditors.Aliases.MemberPicker); + RenameDataType(Constants.PropertyEditors.Legacy.Aliases.MultiNodeTreePicker2, + Constants.PropertyEditors.Aliases.MultiNodeTreePicker); + RenameDataType(Constants.PropertyEditors.Legacy.Aliases.TextboxMultiple, + Constants.PropertyEditors.Aliases.TextArea, false); + RenameDataType(Constants.PropertyEditors.Legacy.Aliases.Textbox, Constants.PropertyEditors.Aliases.TextBox, + false); + } - private void RenameDataType(string fromAlias, string toAlias, bool checkCollision = true) + private void RenameDataType(string fromAlias, string toAlias, bool checkCollision = true) + { + if (checkCollision) { - if (checkCollision) - { - var oldCount = Database.ExecuteScalar(Sql() - .SelectCount() - .From() - .Where(x => x.EditorAlias == toAlias)); - - if (oldCount > 0) - { - // If we throw it means that the upgrade will exit and cannot continue. - // This will occur if a v7 site has the old "Obsolete" property editors that are already named with the `toAlias` name. - // TODO: We should have an additional upgrade step when going from 7 -> 8 like we did with 6 -> 7 that shows a compatibility report, - // this would include this check and then we can provide users with information on what they should do (i.e. before upgrading to v8 they will - // need to migrate these old obsolete editors to non-obsolete editors) + var oldCount = Database.ExecuteScalar(Sql() + .SelectCount() + .From() + .Where(x => x.EditorAlias == toAlias)); - throw new InvalidOperationException( - $"Cannot rename datatype alias \"{fromAlias}\" to \"{toAlias}\" because the target alias is already used." + - $"This is generally because when upgrading from a v7 to v8 site, the v7 site contains Data Types that reference old and already Obsolete " + - $"Property Editors. Before upgrading to v8, any Data Types using property editors that are named with the prefix '(Obsolete)' must be migrated " + - $"to the non-obsolete v7 property editors of the same type."); - } + if (oldCount > 0) + { + // If we throw it means that the upgrade will exit and cannot continue. + // This will occur if a v7 site has the old "Obsolete" property editors that are already named with the `toAlias` name. + // TODO: We should have an additional upgrade step when going from 7 -> 8 like we did with 6 -> 7 that shows a compatibility report, + // this would include this check and then we can provide users with information on what they should do (i.e. before upgrading to v8 they will + // need to migrate these old obsolete editors to non-obsolete editors) + throw new InvalidOperationException( + $"Cannot rename datatype alias \"{fromAlias}\" to \"{toAlias}\" because the target alias is already used." + + "This is generally because when upgrading from a v7 to v8 site, the v7 site contains Data Types that reference old and already Obsolete " + + "Property Editors. Before upgrading to v8, any Data Types using property editors that are named with the prefix '(Obsolete)' must be migrated " + + "to the non-obsolete v7 property editors of the same type."); } - - Database.Execute(Sql() - .Update(u => u.Set(x => x.EditorAlias, toAlias)) - .Where(x => x.EditorAlias == fromAlias)); } + + Database.Execute(Sql() + .Update(u => u.Set(x => x.EditorAlias, toAlias)) + .Where(x => x.EditorAlias == fromAlias)); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigrationBase.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigrationBase.cs index 321da13df89b..440f66cf5ee9 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigrationBase.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigrationBase.cs @@ -1,105 +1,115 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; +using System.Globalization; using Microsoft.Extensions.Logging; using Newtonsoft.Json; +using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public abstract class PropertyEditorsMigrationBase : MigrationBase { - public abstract class PropertyEditorsMigrationBase : MigrationBase + protected PropertyEditorsMigrationBase(IMigrationContext context) + : base(context) { - protected PropertyEditorsMigrationBase(IMigrationContext context) - : base(context) - { } + } - internal List GetDataTypes(string editorAlias, bool strict = true) - { - var sql = Sql() - .Select() - .From(); + internal List GetDataTypes(string editorAlias, bool strict = true) + { + Sql sql = Sql() + .Select() + .From(); - sql = strict - ? sql.Where(x => x.EditorAlias == editorAlias) - : sql.Where(x => x.EditorAlias.Contains(editorAlias)); + sql = strict + ? sql.Where(x => x.EditorAlias == editorAlias) + : sql.Where(x => x.EditorAlias.Contains(editorAlias)); - return Database.Fetch(sql); - } - - protected int[]? ConvertStringValues(string? val) - { - var splitVals = val?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); + return Database.Fetch(sql); + } - var intVals = splitVals? - .Select(x => int.TryParse(x, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) ? i : int.MinValue) - .Where(x => x != int.MinValue) - .ToArray(); + protected int[]? ConvertStringValues(string? val) + { + var splitVals = val?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); - //only return if the number of values are the same (i.e. All INTs) - if (splitVals?.Length == intVals?.Length) - return intVals; + var intVals = splitVals? + .Select(x => + int.TryParse(x, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) ? i : int.MinValue) + .Where(x => x != int.MinValue) + .ToArray(); - return null; + //only return if the number of values are the same (i.e. All INTs) + if (splitVals?.Length == intVals?.Length) + { + return intVals; } - internal bool UpdatePropertyDataDto(PropertyDataDto propData, ValueListConfiguration config, bool isMultiple) + return null; + } + + internal bool UpdatePropertyDataDto(PropertyDataDto propData, ValueListConfiguration config, bool isMultiple) + { + //Get the INT ids stored for this property/drop down + int[]? ids = null; + if (!propData.VarcharValue.IsNullOrWhiteSpace()) { - //Get the INT ids stored for this property/drop down - int[]? ids = null; - if (!propData.VarcharValue.IsNullOrWhiteSpace()) - { - ids = ConvertStringValues(propData.VarcharValue); - } - else if (!propData.TextValue.IsNullOrWhiteSpace()) - { - ids = ConvertStringValues(propData.TextValue); - } - else if (propData.IntegerValue.HasValue) - { - ids = new[] { propData.IntegerValue.Value }; - } + ids = ConvertStringValues(propData.VarcharValue); + } + else if (!propData.TextValue.IsNullOrWhiteSpace()) + { + ids = ConvertStringValues(propData.TextValue); + } + else if (propData.IntegerValue.HasValue) + { + ids = new[] {propData.IntegerValue.Value}; + } - // if there are INT ids, convert them to values based on the configuration - if (ids == null || ids.Length <= 0) return false; + // if there are INT ids, convert them to values based on the configuration + if (ids == null || ids.Length <= 0) + { + return false; + } - // map ids to values - var values = new List(); - var canConvert = true; + // map ids to values + var values = new List(); + var canConvert = true; - foreach (var id in ids) + foreach (var id in ids) + { + ValueListConfiguration.ValueListItem? val = config.Items.FirstOrDefault(x => x.Id == id); + if (val?.Value != null) { - var val = config.Items.FirstOrDefault(x => x.Id == id); - if (val?.Value != null) - { - values.Add(val.Value); - continue; - } - - Logger.LogWarning("Could not find PropertyData {PropertyDataId} value '{PropertyValue}' in the datatype configuration: {Values}.", - propData.Id, id, string.Join(", ", config.Items.Select(x => x.Id + ":" + x.Value))); - canConvert = false; + values.Add(val.Value); + continue; } - if (!canConvert) return false; + Logger.LogWarning( + "Could not find PropertyData {PropertyDataId} value '{PropertyValue}' in the datatype configuration: {Values}.", + propData.Id, id, string.Join(", ", config.Items.Select(x => x.Id + ":" + x.Value))); + canConvert = false; + } - propData.VarcharValue = isMultiple ? JsonConvert.SerializeObject(values) : values[0]; - propData.TextValue = null; - propData.IntegerValue = null; - return true; + if (!canConvert) + { + return false; } - // dummy editor for deserialization - protected class ValueListConfigurationEditor : ConfigurationEditor + propData.VarcharValue = isMultiple ? JsonConvert.SerializeObject(values) : values[0]; + propData.TextValue = null; + propData.IntegerValue = null; + return true; + } + + // dummy editor for deserialization + protected class ValueListConfigurationEditor : ConfigurationEditor + { + public ValueListConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : + base(ioHelper, editorConfigurationParser) { - public ValueListConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - } } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RadioAndCheckboxPropertyEditorsMigration.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RadioAndCheckboxPropertyEditorsMigration.cs index f7114fb0bd1e..8463047fdd86 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RadioAndCheckboxPropertyEditorsMigration.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RadioAndCheckboxPropertyEditorsMigration.cs @@ -1,8 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; @@ -13,101 +11,113 @@ using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class RadioAndCheckboxPropertyEditorsMigration : PropertyEditorsMigrationBase { - public class RadioAndCheckboxPropertyEditorsMigration : PropertyEditorsMigrationBase + private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer; + private readonly IEditorConfigurationParser _editorConfigurationParser; + private readonly IIOHelper _ioHelper; + + public RadioAndCheckboxPropertyEditorsMigration( + IMigrationContext context, + IIOHelper ioHelper, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) + : this(context, ioHelper, configurationEditorJsonSerializer, + StaticServiceProvider.Instance.GetRequiredService()) { - private readonly IIOHelper _ioHelper; - private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer; - private readonly IEditorConfigurationParser _editorConfigurationParser; - - public RadioAndCheckboxPropertyEditorsMigration( - IMigrationContext context, - IIOHelper ioHelper, - IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) - : this(context, ioHelper, configurationEditorJsonSerializer, StaticServiceProvider.Instance.GetRequiredService()) - { - } + } - public RadioAndCheckboxPropertyEditorsMigration( - IMigrationContext context, - IIOHelper ioHelper, - IConfigurationEditorJsonSerializer configurationEditorJsonSerializer, - IEditorConfigurationParser editorConfigurationParser) - : base(context) + public RadioAndCheckboxPropertyEditorsMigration( + IMigrationContext context, + IIOHelper ioHelper, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer, + IEditorConfigurationParser editorConfigurationParser) + : base(context) + { + _ioHelper = ioHelper; + _configurationEditorJsonSerializer = configurationEditorJsonSerializer; + _editorConfigurationParser = editorConfigurationParser; + } + + protected override void Migrate() + { + var refreshCache = false; + + refreshCache |= Migrate(GetDataTypes(Constants.PropertyEditors.Aliases.RadioButtonList), false); + refreshCache |= Migrate(GetDataTypes(Constants.PropertyEditors.Aliases.CheckBoxList), true); + + // if some data types have been updated directly in the database (editing DataTypeDto and/or PropertyDataDto), + // bypassing the services, then we need to rebuild the cache entirely, including the umbracoContentNu table + if (refreshCache) { - _ioHelper = ioHelper; - _configurationEditorJsonSerializer = configurationEditorJsonSerializer; - _editorConfigurationParser = editorConfigurationParser; + Context.AddPostMigration(); } + } - protected override void Migrate() + private bool Migrate(IEnumerable dataTypes, bool isMultiple) + { + var refreshCache = false; + ConfigurationEditor? configurationEditor = null; + + foreach (DataTypeDto dataType in dataTypes) { - var refreshCache = false; + ValueListConfiguration config; - refreshCache |= Migrate(GetDataTypes(Cms.Core.Constants.PropertyEditors.Aliases.RadioButtonList), false); - refreshCache |= Migrate(GetDataTypes(Cms.Core.Constants.PropertyEditors.Aliases.CheckBoxList), true); + if (dataType.Configuration.IsNullOrWhiteSpace()) + { + continue; + } - // if some data types have been updated directly in the database (editing DataTypeDto and/or PropertyDataDto), - // bypassing the services, then we need to rebuild the cache entirely, including the umbracoContentNu table - if (refreshCache) - Context.AddPostMigration(); - } + // parse configuration, and update everything accordingly + if (configurationEditor == null) + { + configurationEditor = new ValueListConfigurationEditor(_ioHelper, _editorConfigurationParser); + } - private bool Migrate(IEnumerable dataTypes, bool isMultiple) - { - var refreshCache = false; - ConfigurationEditor? configurationEditor = null; + try + { + config = (ValueListConfiguration)configurationEditor.FromDatabase(dataType.Configuration, + _configurationEditorJsonSerializer); + } + catch (Exception ex) + { + Logger.LogError( + ex, "Invalid configuration: \"{Configuration}\", cannot convert editor.", + dataType.Configuration); - foreach (var dataType in dataTypes) + continue; + } + + // get property data dtos + List? propertyDataDtos = Database.Fetch(Sql() + .Select() + .From() + .InnerJoin() + .On((pt, pd) => pt.Id == pd.PropertyTypeId) + .InnerJoin().On((dt, pt) => dt.NodeId == pt.DataTypeId) + .Where(x => x.DataTypeId == dataType.NodeId)); + + // update dtos + IEnumerable updatedDtos = + propertyDataDtos.Where(x => UpdatePropertyDataDto(x, config, isMultiple)); + + // persist changes + foreach (PropertyDataDto? propertyDataDto in updatedDtos) { - ValueListConfiguration config; - - if (dataType.Configuration.IsNullOrWhiteSpace()) - continue; - - // parse configuration, and update everything accordingly - if (configurationEditor == null) - configurationEditor = new ValueListConfigurationEditor(_ioHelper, _editorConfigurationParser); - try - { - config = (ValueListConfiguration) configurationEditor.FromDatabase(dataType.Configuration, _configurationEditorJsonSerializer); - } - catch (Exception ex) - { - Logger.LogError( - ex, "Invalid configuration: \"{Configuration}\", cannot convert editor.", - dataType.Configuration); - - continue; - } - - // get property data dtos - var propertyDataDtos = Database.Fetch(Sql() - .Select() - .From() - .InnerJoin().On((pt, pd) => pt.Id == pd.PropertyTypeId) - .InnerJoin().On((dt, pt) => dt.NodeId == pt.DataTypeId) - .Where(x => x.DataTypeId == dataType.NodeId)); - - // update dtos - var updatedDtos = propertyDataDtos.Where(x => UpdatePropertyDataDto(x, config, isMultiple)); - - // persist changes - foreach (var propertyDataDto in updatedDtos) - Database.Update(propertyDataDto); - - UpdateDataType(dataType); - refreshCache = true; + Database.Update(propertyDataDto); } - return refreshCache; + UpdateDataType(dataType); + refreshCache = true; } - private void UpdateDataType(DataTypeDto dataType) - { - dataType.DbType = ValueStorageType.Nvarchar.ToString(); - Database.Update(dataType); - } + return refreshCache; + } + + private void UpdateDataType(DataTypeDto dataType) + { + dataType.DbType = ValueStorageType.Nvarchar.ToString(); + Database.Update(dataType); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RefactorMacroColumns.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RefactorMacroColumns.cs index 005a2ef46415..1e6a4fd90c60 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RefactorMacroColumns.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RefactorMacroColumns.cs @@ -1,39 +1,55 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class RefactorMacroColumns : MigrationBase { - public class RefactorMacroColumns : MigrationBase + public RefactorMacroColumns(IMigrationContext context) + : base(context) { - public RefactorMacroColumns(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() + protected override void Migrate() + { + if (ColumnExists(Constants.DatabaseSchema.Tables.Macro, "macroXSLT")) { - if (ColumnExists(Cms.Core.Constants.DatabaseSchema.Tables.Macro, "macroXSLT")) - { - //special trick to add the column without constraints and return the sql to add them later - AddColumn("macroType", out var sqls1); - AddColumn("macroSource", out var sqls2); + //special trick to add the column without constraints and return the sql to add them later + AddColumn("macroType", out IEnumerable sqls1); + AddColumn("macroSource", out IEnumerable sqls2); - //populate the new columns with legacy data - //when the macro type is PartialView, it corresponds to 7, else it is 4 for Unknown - Execute.Sql($"UPDATE {Cms.Core.Constants.DatabaseSchema.Tables.Macro} SET macroSource = '', macroType = 4").Do(); - Execute.Sql($"UPDATE {Cms.Core.Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroXSLT, macroType = 4 WHERE macroXSLT != '' AND macroXSLT IS NOT NULL").Do(); - Execute.Sql($"UPDATE {Cms.Core.Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroScriptAssembly, macroType = 4 WHERE macroScriptAssembly != '' AND macroScriptAssembly IS NOT NULL").Do(); - Execute.Sql($"UPDATE {Cms.Core.Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroScriptType, macroType = 4 WHERE macroScriptType != '' AND macroScriptType IS NOT NULL").Do(); - Execute.Sql($"UPDATE {Cms.Core.Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroPython, macroType = 7 WHERE macroPython != '' AND macroPython IS NOT NULL").Do(); + //populate the new columns with legacy data + //when the macro type is PartialView, it corresponds to 7, else it is 4 for Unknown + Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = '', macroType = 4").Do(); + Execute.Sql( + $"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroXSLT, macroType = 4 WHERE macroXSLT != '' AND macroXSLT IS NOT NULL") + .Do(); + Execute.Sql( + $"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroScriptAssembly, macroType = 4 WHERE macroScriptAssembly != '' AND macroScriptAssembly IS NOT NULL") + .Do(); + Execute.Sql( + $"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroScriptType, macroType = 4 WHERE macroScriptType != '' AND macroScriptType IS NOT NULL") + .Do(); + Execute.Sql( + $"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroPython, macroType = 7 WHERE macroPython != '' AND macroPython IS NOT NULL") + .Do(); - //now apply constraints (NOT NULL) to new table - foreach (var sql in sqls1) Execute.Sql(sql).Do(); - foreach (var sql in sqls2) Execute.Sql(sql).Do(); + //now apply constraints (NOT NULL) to new table + foreach (var sql in sqls1) + { + Execute.Sql(sql).Do(); + } - //now remove these old columns - Delete.Column("macroXSLT").FromTable(Cms.Core.Constants.DatabaseSchema.Tables.Macro).Do(); - Delete.Column("macroScriptAssembly").FromTable(Cms.Core.Constants.DatabaseSchema.Tables.Macro).Do(); - Delete.Column("macroScriptType").FromTable(Cms.Core.Constants.DatabaseSchema.Tables.Macro).Do(); - Delete.Column("macroPython").FromTable(Cms.Core.Constants.DatabaseSchema.Tables.Macro).Do(); + foreach (var sql in sqls2) + { + Execute.Sql(sql).Do(); } + + //now remove these old columns + Delete.Column("macroXSLT").FromTable(Constants.DatabaseSchema.Tables.Macro).Do(); + Delete.Column("macroScriptAssembly").FromTable(Constants.DatabaseSchema.Tables.Macro).Do(); + Delete.Column("macroScriptType").FromTable(Constants.DatabaseSchema.Tables.Macro).Do(); + Delete.Column("macroPython").FromTable(Constants.DatabaseSchema.Tables.Macro).Do(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RefactorVariantsModel.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RefactorVariantsModel.cs index 1ff19e0698cd..0328a93c59ae 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RefactorVariantsModel.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RefactorVariantsModel.cs @@ -1,80 +1,99 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class RefactorVariantsModel : MigrationBase { - public class RefactorVariantsModel : MigrationBase + public RefactorVariantsModel(IMigrationContext context) + : base(context) { - public RefactorVariantsModel(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() + protected override void Migrate() + { + if (ColumnExists(Constants.DatabaseSchema.Tables.ContentVersionCultureVariation, "edited")) { - if (ColumnExists(Cms.Core.Constants.DatabaseSchema.Tables.ContentVersionCultureVariation, "edited")) - Delete.Column("edited").FromTable(Cms.Core.Constants.DatabaseSchema.Tables.ContentVersionCultureVariation).Do(); - + Delete.Column("edited").FromTable(Constants.DatabaseSchema.Tables.ContentVersionCultureVariation).Do(); + } - // add available column - AddColumn("available", out var sqls); - // so far, only those cultures that were available had records in the table - Update.Table(DocumentCultureVariationDto.TableName).Set(new { available = true }).AllRows().Do(); + // add available column + AddColumn("available", out IEnumerable sqls); - foreach (var sql in sqls) Execute.Sql(sql).Do(); + // so far, only those cultures that were available had records in the table + Update.Table(DocumentCultureVariationDto.TableName).Set(new {available = true}).AllRows().Do(); + foreach (var sql in sqls) + { + Execute.Sql(sql).Do(); + } - // add published column - AddColumn("published", out sqls); - // make it false by default - Update.Table(DocumentCultureVariationDto.TableName).Set(new { published = false }).AllRows().Do(); + // add published column + AddColumn("published", out sqls); - // now figure out whether these available cultures are published, too - var getPublished = Sql() - .Select(x => x.NodeId) - .AndSelect(x => x.LanguageId) - .From() - .InnerJoin().On((node, cv) => node.NodeId == cv.NodeId) - .InnerJoin().On((cv, dv) => cv.Id == dv.Id && dv.Published) - .InnerJoin().On((cv, ccv) => cv.Id == ccv.VersionId); + // make it false by default + Update.Table(DocumentCultureVariationDto.TableName).Set(new {published = false}).AllRows().Do(); - foreach (var dto in Database.Fetch(getPublished)) - Database.Execute(Sql() - .Update(u => u.Set(x => x.Published, true)) - .Where(x => x.NodeId == dto.NodeId && x.LanguageId == dto.LanguageId)); + // now figure out whether these available cultures are published, too + Sql getPublished = Sql() + .Select(x => x.NodeId) + .AndSelect(x => x.LanguageId) + .From() + .InnerJoin().On((node, cv) => node.NodeId == cv.NodeId) + .InnerJoin() + .On((cv, dv) => cv.Id == dv.Id && dv.Published) + .InnerJoin() + .On((cv, ccv) => cv.Id == ccv.VersionId); - foreach (var sql in sqls) Execute.Sql(sql).Do(); + foreach (TempDto? dto in Database.Fetch(getPublished)) + { + Database.Execute(Sql() + .Update(u => u.Set(x => x.Published, true)) + .Where(x => x.NodeId == dto.NodeId && x.LanguageId == dto.LanguageId)); + } - // so far, it was kinda impossible to make a culture unavailable again, - // so we *should* not have anything published but not available - ignore + foreach (var sql in sqls) + { + Execute.Sql(sql).Do(); + } + // so far, it was kinda impossible to make a culture unavailable again, + // so we *should* not have anything published but not available - ignore - // add name column - AddColumn("name"); - // so far, every record in the table mapped to an available culture - var getNames = Sql() - .Select(x => x.NodeId) - .AndSelect(x => x.LanguageId, x => x.Name) - .From() - .InnerJoin().On((node, cv) => node.NodeId == cv.NodeId && cv.Current) - .InnerJoin().On((cv, ccv) => cv.Id == ccv.VersionId); + // add name column + AddColumn("name"); - foreach (var dto in Database.Fetch(getNames)) - Database.Execute(Sql() - .Update(u => u.Set(x => x.Name, dto.Name)) - .Where(x => x.NodeId == dto.NodeId && x.LanguageId == dto.LanguageId)); - } + // so far, every record in the table mapped to an available culture + Sql getNames = Sql() + .Select(x => x.NodeId) + .AndSelect(x => x.LanguageId, x => x.Name) + .From() + .InnerJoin() + .On((node, cv) => node.NodeId == cv.NodeId && cv.Current) + .InnerJoin() + .On((cv, ccv) => cv.Id == ccv.VersionId); - // ReSharper disable once ClassNeverInstantiated.Local - // ReSharper disable UnusedAutoPropertyAccessor.Local - private class TempDto + foreach (TempDto? dto in Database.Fetch(getNames)) { - public int NodeId { get; set; } - public int LanguageId { get; set; } - public string? Name { get; set; } + Database.Execute(Sql() + .Update(u => u.Set(x => x.Name, dto.Name)) + .Where(x => x.NodeId == dto.NodeId && x.LanguageId == dto.LanguageId)); } - // ReSharper restore UnusedAutoPropertyAccessor.Local } + + // ReSharper disable once ClassNeverInstantiated.Local + // ReSharper disable UnusedAutoPropertyAccessor.Local + private class TempDto + { + public int NodeId { get; set; } + public int LanguageId { get; set; } + public string? Name { get; set; } + } + // ReSharper restore UnusedAutoPropertyAccessor.Local } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameLabelAndRichTextPropertyEditorAliases.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameLabelAndRichTextPropertyEditorAliases.cs index c3fdf2d0fc60..a5fc9b4ece4c 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameLabelAndRichTextPropertyEditorAliases.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameLabelAndRichTextPropertyEditorAliases.cs @@ -1,41 +1,39 @@ -using System.Collections.Generic; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class RenameLabelAndRichTextPropertyEditorAliases : MigrationBase { - public class RenameLabelAndRichTextPropertyEditorAliases : MigrationBase + public RenameLabelAndRichTextPropertyEditorAliases(IMigrationContext context) + : base(context) { - public RenameLabelAndRichTextPropertyEditorAliases(IMigrationContext context) - : base(context) - { - } - - protected override void Migrate() - { - MigratePropertyEditorAlias("Umbraco.TinyMCEv3", Cms.Core.Constants.PropertyEditors.Aliases.TinyMce); - MigratePropertyEditorAlias("Umbraco.NoEdit", Cms.Core.Constants.PropertyEditors.Aliases.Label); - } + } - private void MigratePropertyEditorAlias(string oldAlias, string newAlias) - { - var dataTypes = GetDataTypes(oldAlias); + protected override void Migrate() + { + MigratePropertyEditorAlias("Umbraco.TinyMCEv3", Constants.PropertyEditors.Aliases.TinyMce); + MigratePropertyEditorAlias("Umbraco.NoEdit", Constants.PropertyEditors.Aliases.Label); + } - foreach (var dataType in dataTypes) - { - dataType.EditorAlias = newAlias; - Database.Update(dataType); - } - } + private void MigratePropertyEditorAlias(string oldAlias, string newAlias) + { + List dataTypes = GetDataTypes(oldAlias); - private List GetDataTypes(string editorAlias) + foreach (DataTypeDto dataType in dataTypes) { - var dataTypes = Database.Fetch(Sql() - .Select() - .From() - .Where(x => x.EditorAlias == editorAlias)); - return dataTypes; + dataType.EditorAlias = newAlias; + Database.Update(dataType); } + } + private List GetDataTypes(string editorAlias) + { + List? dataTypes = Database.Fetch(Sql() + .Select() + .From() + .Where(x => x.EditorAlias == editorAlias)); + return dataTypes; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameMediaVersionTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameMediaVersionTable.cs index fa88f17422d1..1bc621c7cbec 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameMediaVersionTable.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameMediaVersionTable.cs @@ -1,45 +1,49 @@ -using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class RenameMediaVersionTable : MigrationBase { - public class RenameMediaVersionTable : MigrationBase + public RenameMediaVersionTable(IMigrationContext context) + : base(context) { - public RenameMediaVersionTable(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - Rename.Table("cmsMedia").To(Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion).Do(); + protected override void Migrate() + { + Rename.Table("cmsMedia").To(Constants.DatabaseSchema.Tables.MediaVersion).Do(); - // that is not supported on SqlCE - //Rename.Column("versionId").OnTable(Constants.DatabaseSchema.Tables.MediaVersion).To("id").Do(); + // that is not supported on SqlCE + //Rename.Column("versionId").OnTable(Constants.DatabaseSchema.Tables.MediaVersion).To("id").Do(); - AddColumn("id", out var sqls); + AddColumn("id", out IEnumerable sqls); - Database.Execute($@"UPDATE {Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion} SET id=v.id -FROM {Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion} m + Database.Execute($@"UPDATE {Constants.DatabaseSchema.Tables.MediaVersion} SET id=v.id +FROM {Constants.DatabaseSchema.Tables.MediaVersion} m JOIN cmsContentVersion v on m.versionId = v.versionId JOIN umbracoNode n on v.contentId=n.id -WHERE n.nodeObjectType='{Cms.Core.Constants.ObjectTypes.Media}'"); +WHERE n.nodeObjectType='{Constants.ObjectTypes.Media}'"); - foreach (var sql in sqls) - Execute.Sql(sql).Do(); + foreach (var sql in sqls) + { + Execute.Sql(sql).Do(); + } - AddColumn("path", out sqls); + AddColumn("path", out sqls); - Execute.Sql($"UPDATE {Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion} SET path=mediaPath").Do(); + Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.MediaVersion} SET path=mediaPath").Do(); - foreach (var sql in sqls) - Execute.Sql(sql).Do(); + foreach (var sql in sqls) + { + Execute.Sql(sql).Do(); + } - // we had to run sqls to get the NULL constraints, but we need to get rid of most - Delete.KeysAndIndexes(Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion).Do(); + // we had to run sqls to get the NULL constraints, but we need to get rid of most + Delete.KeysAndIndexes(Constants.DatabaseSchema.Tables.MediaVersion).Do(); - Delete.Column("mediaPath").FromTable(Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion).Do(); - Delete.Column("versionId").FromTable(Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion).Do(); - Delete.Column("nodeId").FromTable(Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion).Do(); - } + Delete.Column("mediaPath").FromTable(Constants.DatabaseSchema.Tables.MediaVersion).Do(); + Delete.Column("versionId").FromTable(Constants.DatabaseSchema.Tables.MediaVersion).Do(); + Delete.Column("nodeId").FromTable(Constants.DatabaseSchema.Tables.MediaVersion).Do(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameUmbracoDomainsTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameUmbracoDomainsTable.cs index 8bb2a8c14cf7..52fe8a15d453 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameUmbracoDomainsTable.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameUmbracoDomainsTable.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class RenameUmbracoDomainsTable : MigrationBase { - public class RenameUmbracoDomainsTable : MigrationBase + public RenameUmbracoDomainsTable(IMigrationContext context) + : base(context) { - public RenameUmbracoDomainsTable(IMigrationContext context) - : base(context) - { } - - protected override void Migrate() - { - Rename.Table("umbracoDomains").To(Cms.Core.Constants.DatabaseSchema.Tables.Domain).Do(); - } } + + protected override void Migrate() => Rename.Table("umbracoDomains").To(Constants.DatabaseSchema.Tables.Domain).Do(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/SuperZero.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/SuperZero.cs index 4daab6996239..502295b87523 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/SuperZero.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/SuperZero.cs @@ -1,20 +1,26 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class SuperZero : MigrationBase { - public class SuperZero : MigrationBase + public SuperZero(IMigrationContext context) + : base(context) { - public SuperZero(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() + protected override void Migrate() + { + var exists = Database.Fetch("select id from umbracoUser where id=-1;").Count > 0; + if (exists) { - var exists = Database.Fetch("select id from umbracoUser where id=-1;").Count > 0; - if (exists) return; + return; + } - Database.Execute("update umbracoUser set userLogin = userLogin + '__' where id=0"); + Database.Execute("update umbracoUser set userLogin = userLogin + '__' where id=0"); - Database.Execute("set identity_insert umbracoUser on;"); - Database.Execute(@" + Database.Execute("set identity_insert umbracoUser on;"); + Database.Execute(@" insert into umbracoUser (id, userDisabled, userNoConsole, userName, userLogin, userPassword, passwordConfig, userEmail, userLanguage, securityStampToken, failedLoginAttempts, lastLockoutDate, @@ -27,14 +33,13 @@ insert into umbracoUser (id, lastPasswordChangeDate, lastLoginDate, emailConfirmedDate, invitedDate, createDate, updateDate, avatar, tourData from umbracoUser where id=0;"); - Database.Execute("set identity_insert umbracoUser off;"); + Database.Execute("set identity_insert umbracoUser off;"); - Database.Execute("update umbracoUser2UserGroup set userId=-1 where userId=0;"); - Database.Execute("update umbracoUser2NodeNotify set userId=-1 where userId=0;"); - Database.Execute("update umbracoNode set nodeUser=-1 where nodeUser=0;"); - Database.Execute("update umbracoUserLogin set userId=-1 where userId=0;"); - Database.Execute($"update {Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion} set userId=-1 where userId=0;"); - Database.Execute("delete from umbracoUser where id=0;"); - } + Database.Execute("update umbracoUser2UserGroup set userId=-1 where userId=0;"); + Database.Execute("update umbracoUser2NodeNotify set userId=-1 where userId=0;"); + Database.Execute("update umbracoNode set nodeUser=-1 where nodeUser=0;"); + Database.Execute("update umbracoUserLogin set userId=-1 where userId=0;"); + Database.Execute($"update {Constants.DatabaseSchema.Tables.ContentVersion} set userId=-1 where userId=0;"); + Database.Execute("delete from umbracoUser where id=0;"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs index 531e7a06ccd2..8cc0791e8a21 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs @@ -1,58 +1,57 @@ -using System; -using NPoco; +using NPoco; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class TablesForScheduledPublishing : MigrationBase { - public class TablesForScheduledPublishing : MigrationBase + public TablesForScheduledPublishing(IMigrationContext context) + : base(context) + { + } + + protected override void Migrate() { - public TablesForScheduledPublishing(IMigrationContext context) - : base(context) - { } + //Get anything currently scheduled + Sql? releaseSql = new Sql() + .Select("nodeId", "releaseDate") + .From("umbracoDocument") + .Where("releaseDate IS NOT NULL"); + Dictionary? releases = Database.Dictionary(releaseSql); + + Sql? expireSql = new Sql() + .Select("nodeId", "expireDate") + .From("umbracoDocument") + .Where("expireDate IS NOT NULL"); + Dictionary? expires = Database.Dictionary(expireSql); + + + //drop old cols + Delete.Column("releaseDate").FromTable("umbracoDocument").Do(); + Delete.Column("expireDate").FromTable("umbracoDocument").Do(); + //add new table + Create.Table(true).Do(); + + //migrate the schedule + foreach (KeyValuePair s in releases) + { + DateTime date = s.Value; + var action = ContentScheduleAction.Release.ToString(); + + Insert.IntoTable(ContentScheduleDto.TableName) + .Row(new {id = Guid.NewGuid(), nodeId = s.Key, date, action}) + .Do(); + } - protected override void Migrate() + foreach (KeyValuePair s in expires) { - //Get anything currently scheduled - var releaseSql = new Sql() - .Select("nodeId", "releaseDate") - .From("umbracoDocument") - .Where("releaseDate IS NOT NULL"); - var releases = Database.Dictionary (releaseSql); - - var expireSql = new Sql() - .Select("nodeId", "expireDate") - .From("umbracoDocument") - .Where("expireDate IS NOT NULL"); - var expires = Database.Dictionary(expireSql); - - - //drop old cols - Delete.Column("releaseDate").FromTable("umbracoDocument").Do(); - Delete.Column("expireDate").FromTable("umbracoDocument").Do(); - //add new table - Create.Table(true).Do(); - - //migrate the schedule - foreach(var s in releases) - { - var date = s.Value; - var action = ContentScheduleAction.Release.ToString(); - - Insert.IntoTable(ContentScheduleDto.TableName) - .Row(new { id = Guid.NewGuid(), nodeId = s.Key, date = date, action = action }) - .Do(); - } - - foreach (var s in expires) - { - var date = s.Value; - var action = ContentScheduleAction.Expire.ToString(); - - Insert.IntoTable(ContentScheduleDto.TableName) - .Row(new { id = Guid.NewGuid(), nodeId = s.Key, date = date, action = action }) - .Do(); - } + DateTime date = s.Value; + var action = ContentScheduleAction.Expire.ToString(); + + Insert.IntoTable(ContentScheduleDto.TableName) + .Row(new {id = Guid.NewGuid(), nodeId = s.Key, date, action}) + .Do(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/TagsMigration.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/TagsMigration.cs index 35c32bddb9a3..6f84d4db7155 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/TagsMigration.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/TagsMigration.cs @@ -1,21 +1,22 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class TagsMigration : MigrationBase { - public class TagsMigration : MigrationBase + public TagsMigration(IMigrationContext context) + : base(context) { - public TagsMigration(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - // alter columns => non-null - AlterColumn(Cms.Core.Constants.DatabaseSchema.Tables.Tag, "group"); - AlterColumn(Cms.Core.Constants.DatabaseSchema.Tables.Tag, "tag"); + protected override void Migrate() + { + // alter columns => non-null + AlterColumn(Constants.DatabaseSchema.Tables.Tag, "group"); + AlterColumn(Constants.DatabaseSchema.Tables.Tag, "tag"); - // kill unused parentId column - Delete.Column("ParentId").FromTable(Cms.Core.Constants.DatabaseSchema.Tables.Tag).Do(); - } + // kill unused parentId column + Delete.Column("ParentId").FromTable(Constants.DatabaseSchema.Tables.Tag).Do(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/TagsMigrationFix.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/TagsMigrationFix.cs index 63ffd563a91e..a950b525b999 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/TagsMigrationFix.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/TagsMigrationFix.cs @@ -1,16 +1,20 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class TagsMigrationFix : MigrationBase { - public class TagsMigrationFix : MigrationBase + public TagsMigrationFix(IMigrationContext context) + : base(context) { - public TagsMigrationFix(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() + protected override void Migrate() + { + // kill unused parentId column, if it still exists + if (ColumnExists(Constants.DatabaseSchema.Tables.Tag, "ParentId")) { - // kill unused parentId column, if it still exists - if (ColumnExists(Cms.Core.Constants.DatabaseSchema.Tables.Tag, "ParentId")) - Delete.Column("ParentId").FromTable(Cms.Core.Constants.DatabaseSchema.Tables.Tag).Do(); + Delete.Column("ParentId").FromTable(Constants.DatabaseSchema.Tables.Tag).Do(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/UpdateDefaultMandatoryLanguage.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/UpdateDefaultMandatoryLanguage.cs index e3251dc6edc5..841630dbc542 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/UpdateDefaultMandatoryLanguage.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/UpdateDefaultMandatoryLanguage.cs @@ -1,48 +1,54 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class UpdateDefaultMandatoryLanguage : MigrationBase { - public class UpdateDefaultMandatoryLanguage : MigrationBase + public UpdateDefaultMandatoryLanguage(IMigrationContext context) + : base(context) { - public UpdateDefaultMandatoryLanguage(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - // add the new languages lock object - AddLockObjects.EnsureLockObject(Database, Cms.Core.Constants.Locks.Languages, "Languages"); + protected override void Migrate() + { + // add the new languages lock object + AddLockObjects.EnsureLockObject(Database, Constants.Locks.Languages, "Languages"); - // get all existing languages - var selectDtos = Sql() - .Select() - .From(); + // get all existing languages + Sql selectDtos = Sql() + .Select() + .From(); - var dtos = Database.Fetch(selectDtos); + List? dtos = Database.Fetch(selectDtos); - // get the id of the language which is already the default one, if any, - // else get the lowest language id, which will become the default language - var defaultId = int.MaxValue; - foreach (var dto in dtos) + // get the id of the language which is already the default one, if any, + // else get the lowest language id, which will become the default language + var defaultId = int.MaxValue; + foreach (LanguageDto? dto in dtos) + { + if (dto.IsDefault) { - if (dto.IsDefault) - { - defaultId = dto.Id; - break; - } + defaultId = dto.Id; + break; + } - if (dto.Id < defaultId) defaultId = dto.Id; + if (dto.Id < defaultId) + { + defaultId = dto.Id; } + } - // update, so that language with that id is now default and mandatory - var updateDefault = Sql() - .Update(u => u - .Set(x => x.IsDefault, true) - .Set(x => x.IsMandatory, true)) - .Where(x => x.Id == defaultId); + // update, so that language with that id is now default and mandatory + Sql updateDefault = Sql() + .Update(u => u + .Set(x => x.IsDefault, true) + .Set(x => x.IsMandatory, true)) + .Where(x => x.Id == defaultId); - Database.Execute(updateDefault); - } + Database.Execute(updateDefault); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/UpdatePickerIntegerValuesToUdi.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/UpdatePickerIntegerValuesToUdi.cs index 7fe50b215924..2d920fd9385c 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/UpdatePickerIntegerValuesToUdi.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/UpdatePickerIntegerValuesToUdi.cs @@ -1,110 +1,117 @@ -using System; -using System.Globalization; -using System.Linq; +using System.Globalization; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using NPoco; using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class UpdatePickerIntegerValuesToUdi : MigrationBase { - public class UpdatePickerIntegerValuesToUdi : MigrationBase + public UpdatePickerIntegerValuesToUdi(IMigrationContext context) : base(context) { - public UpdatePickerIntegerValuesToUdi(IMigrationContext context) : base(context) - { } + } - protected override void Migrate() - { - var sqlDataTypes = Sql() - .Select() - .From() - .Where(x => x.EditorAlias == Cms.Core.Constants.PropertyEditors.Aliases.ContentPicker - || x.EditorAlias == Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker - || x.EditorAlias == Cms.Core.Constants.PropertyEditors.Aliases.MultiNodeTreePicker); + protected override void Migrate() + { + Sql sqlDataTypes = Sql() + .Select() + .From() + .Where(x => x.EditorAlias == Constants.PropertyEditors.Aliases.ContentPicker + || x.EditorAlias == Constants.PropertyEditors.Aliases.MediaPicker + || x.EditorAlias == Constants.PropertyEditors.Aliases.MultiNodeTreePicker); - var dataTypes = Database.Fetch(sqlDataTypes).ToList(); + var dataTypes = Database.Fetch(sqlDataTypes).ToList(); - foreach (var datatype in dataTypes.Where(x => !x.Configuration.IsNullOrWhiteSpace())) + foreach (DataTypeDto? datatype in dataTypes.Where(x => !x.Configuration.IsNullOrWhiteSpace())) + { + switch (datatype.EditorAlias) { - switch (datatype.EditorAlias) + case Constants.PropertyEditors.Aliases.ContentPicker: + case Constants.PropertyEditors.Aliases.MediaPicker: { - case Cms.Core.Constants.PropertyEditors.Aliases.ContentPicker: - case Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker: + JObject? config = JsonConvert.DeserializeObject(datatype.Configuration!); + var startNodeId = config!.Value("startNodeId"); + if (!startNodeId.IsNullOrWhiteSpace() && int.TryParse(startNodeId, NumberStyles.Integer, + CultureInfo.InvariantCulture, out var intStartNode)) + { + Guid? guid = intStartNode <= 0 + ? null + : Context.Database.ExecuteScalar( + Sql().Select(x => x.UniqueId).From() + .Where(x => x.NodeId == intStartNode)); + if (guid.HasValue) { - var config = JsonConvert.DeserializeObject(datatype.Configuration!); - var startNodeId = config!.Value("startNodeId"); - if (!startNodeId.IsNullOrWhiteSpace() && int.TryParse(startNodeId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intStartNode)) - { - var guid = intStartNode <= 0 - ? null - : Context.Database.ExecuteScalar( - Sql().Select(x => x.UniqueId).From().Where(x => x.NodeId == intStartNode)); - if (guid.HasValue) - { - var udi = new GuidUdi(datatype.EditorAlias == Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker - ? Cms.Core.Constants.UdiEntityType.Media - : Cms.Core.Constants.UdiEntityType.Document, guid.Value); - config!["startNodeId"] = new JValue(udi.ToString()); - } - else - config!.Remove("startNodeId"); - - datatype.Configuration = JsonConvert.SerializeObject(config); - Database.Update(datatype); - } - - break; + var udi = new GuidUdi(datatype.EditorAlias == Constants.PropertyEditors.Aliases.MediaPicker + ? Constants.UdiEntityType.Media + : Constants.UdiEntityType.Document, guid.Value); + config!["startNodeId"] = new JValue(udi.ToString()); } - case Cms.Core.Constants.PropertyEditors.Aliases.MultiNodeTreePicker: + else { - var config = JsonConvert.DeserializeObject(datatype.Configuration!); - var startNodeConfig = config!.Value("startNode"); - if (startNodeConfig != null) - { - var startNodeId = startNodeConfig.Value("id"); - var objectType = startNodeConfig.Value("type"); - if (!objectType.IsNullOrWhiteSpace() - && !startNodeId.IsNullOrWhiteSpace() - && int.TryParse(startNodeId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intStartNode)) - { - var guid = intStartNode <= 0 - ? null - : Context.Database.ExecuteScalar( - Sql().Select(x => x.UniqueId).From().Where(x => x.NodeId == intStartNode)); + config!.Remove("startNodeId"); + } - string? entityType = null; - switch (objectType?.ToLowerInvariant()) - { - case "content": - entityType = Cms.Core.Constants.UdiEntityType.Document; - break; - case "media": - entityType = Cms.Core.Constants.UdiEntityType.Media; - break; - case "member": - entityType = Cms.Core.Constants.UdiEntityType.Member; - break; - } + datatype.Configuration = JsonConvert.SerializeObject(config); + Database.Update(datatype); + } - if (entityType != null && guid.HasValue) - { - var udi = new GuidUdi(entityType, guid.Value); - startNodeConfig["id"] = new JValue(udi.ToString()); - } - else - startNodeConfig.Remove("id"); + break; + } + case Constants.PropertyEditors.Aliases.MultiNodeTreePicker: + { + JObject? config = JsonConvert.DeserializeObject(datatype.Configuration!); + JObject? startNodeConfig = config!.Value("startNode"); + if (startNodeConfig != null) + { + var startNodeId = startNodeConfig.Value("id"); + var objectType = startNodeConfig.Value("type"); + if (!objectType.IsNullOrWhiteSpace() + && !startNodeId.IsNullOrWhiteSpace() + && int.TryParse(startNodeId, NumberStyles.Integer, CultureInfo.InvariantCulture, + out var intStartNode)) + { + Guid? guid = intStartNode <= 0 + ? null + : Context.Database.ExecuteScalar( + Sql().Select(x => x.UniqueId).From() + .Where(x => x.NodeId == intStartNode)); - datatype.Configuration = JsonConvert.SerializeObject(config); - Database.Update(datatype); - } + string? entityType = null; + switch (objectType?.ToLowerInvariant()) + { + case "content": + entityType = Constants.UdiEntityType.Document; + break; + case "media": + entityType = Constants.UdiEntityType.Media; + break; + case "member": + entityType = Constants.UdiEntityType.Member; + break; } - break; + if (entityType != null && guid.HasValue) + { + var udi = new GuidUdi(entityType, guid.Value); + startNodeConfig["id"] = new JValue(udi.ToString()); + } + else + { + startNodeConfig.Remove("id"); + } + + datatype.Configuration = JsonConvert.SerializeObject(config); + Database.Update(datatype); } + } + + break; } } - } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/UserForeignKeys.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/UserForeignKeys.cs index 03c3529f59d1..8266d9e82b32 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/UserForeignKeys.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/UserForeignKeys.cs @@ -1,31 +1,41 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +/// +/// Creates/Updates non mandatory FK columns to the user table +/// +public class UserForeignKeys : MigrationBase { - /// - /// Creates/Updates non mandatory FK columns to the user table - /// - public class UserForeignKeys : MigrationBase + public UserForeignKeys(IMigrationContext context) + : base(context) { - public UserForeignKeys(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - // first allow NULL-able - Alter.Table(ContentVersionCultureVariationDto.TableName).AlterColumn("availableUserId").AsInt32().Nullable().Do(); - Alter.Table(ContentVersionDto.TableName).AlterColumn("userId").AsInt32().Nullable().Do(); - Alter.Table(Cms.Core.Constants.DatabaseSchema.Tables.Log).AlterColumn("userId").AsInt32().Nullable().Do(); - Alter.Table(NodeDto.TableName).AlterColumn("nodeUser").AsInt32().Nullable().Do(); + protected override void Migrate() + { + // first allow NULL-able + Alter.Table(ContentVersionCultureVariationDto.TableName).AlterColumn("availableUserId").AsInt32().Nullable() + .Do(); + Alter.Table(ContentVersionDto.TableName).AlterColumn("userId").AsInt32().Nullable().Do(); + Alter.Table(Constants.DatabaseSchema.Tables.Log).AlterColumn("userId").AsInt32().Nullable().Do(); + Alter.Table(NodeDto.TableName).AlterColumn("nodeUser").AsInt32().Nullable().Do(); - // then we can update any non existing users to NULL - Execute.Sql($"UPDATE {ContentVersionCultureVariationDto.TableName} SET availableUserId = NULL WHERE availableUserId NOT IN (SELECT id FROM {UserDto.TableName})").Do(); - Execute.Sql($"UPDATE {ContentVersionDto.TableName} SET userId = NULL WHERE userId NOT IN (SELECT id FROM {UserDto.TableName})").Do(); - Execute.Sql($"UPDATE {Cms.Core.Constants.DatabaseSchema.Tables.Log} SET userId = NULL WHERE userId NOT IN (SELECT id FROM {UserDto.TableName})").Do(); - Execute.Sql($"UPDATE {NodeDto.TableName} SET nodeUser = NULL WHERE nodeUser NOT IN (SELECT id FROM {UserDto.TableName})").Do(); + // then we can update any non existing users to NULL + Execute.Sql( + $"UPDATE {ContentVersionCultureVariationDto.TableName} SET availableUserId = NULL WHERE availableUserId NOT IN (SELECT id FROM {UserDto.TableName})") + .Do(); + Execute.Sql( + $"UPDATE {ContentVersionDto.TableName} SET userId = NULL WHERE userId NOT IN (SELECT id FROM {UserDto.TableName})") + .Do(); + Execute.Sql( + $"UPDATE {Constants.DatabaseSchema.Tables.Log} SET userId = NULL WHERE userId NOT IN (SELECT id FROM {UserDto.TableName})") + .Do(); + Execute.Sql( + $"UPDATE {NodeDto.TableName} SET nodeUser = NULL WHERE nodeUser NOT IN (SELECT id FROM {UserDto.TableName})") + .Do(); - // FKs will be created after migrations - } + // FKs will be created after migrations } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs index f0fbb637290a..80f80c93f6a8 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs @@ -1,215 +1,270 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using Umbraco.Cms.Infrastructure.Persistence; +using System.Diagnostics; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class VariantsMigration : MigrationBase { - public class VariantsMigration : MigrationBase + public VariantsMigration(IMigrationContext context) + : base(context) { - public VariantsMigration(IMigrationContext context) - : base(context) - { } + } - // notes - // do NOT use Rename.Column as it's borked on SQLCE - use ReplaceColumn instead + // notes + // do NOT use Rename.Column as it's borked on SQLCE - use ReplaceColumn instead - protected override void Migrate() + protected override void Migrate() + { + MigratePropertyData(); + CreatePropertyDataIndexes(); + MigrateContentAndPropertyTypes(); + MigrateContent(); + MigrateVersions(); + + if (Database.Fetch( + $@"SELECT {Constants.DatabaseSchema.Tables.ContentVersion}.nodeId, COUNT({Constants.DatabaseSchema.Tables.ContentVersion}.id) +FROM {Constants.DatabaseSchema.Tables.ContentVersion} +JOIN {Constants.DatabaseSchema.Tables.DocumentVersion} ON {Constants.DatabaseSchema.Tables.ContentVersion}.id={Constants.DatabaseSchema.Tables.DocumentVersion}.id +WHERE {Constants.DatabaseSchema.Tables.DocumentVersion}.published=1 +GROUP BY {Constants.DatabaseSchema.Tables.ContentVersion}.nodeId +HAVING COUNT({Constants.DatabaseSchema.Tables.ContentVersion}.id) > 1").Any()) { - MigratePropertyData(); - CreatePropertyDataIndexes(); - MigrateContentAndPropertyTypes(); - MigrateContent(); - MigrateVersions(); - - if (Database.Fetch($@"SELECT {Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion}.nodeId, COUNT({Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion}.id) -FROM {Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion} -JOIN {Cms.Core.Constants.DatabaseSchema.Tables.DocumentVersion} ON {Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion}.id={Cms.Core.Constants.DatabaseSchema.Tables.DocumentVersion}.id -WHERE {Cms.Core.Constants.DatabaseSchema.Tables.DocumentVersion}.published=1 -GROUP BY {Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion}.nodeId -HAVING COUNT({Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion}.id) > 1").Any()) - { - Debugger.Break(); - throw new Exception("Migration failed: duplicate 'published' document versions."); - } + Debugger.Break(); + throw new Exception("Migration failed: duplicate 'published' document versions."); + } - if (Database.Fetch($@"SELECT v1.nodeId, v1.id, COUNT(v2.id) -FROM {Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion} v1 -LEFT JOIN {Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion} v2 ON v1.nodeId=v2.nodeId AND v2.[current]=1 + if (Database.Fetch($@"SELECT v1.nodeId, v1.id, COUNT(v2.id) +FROM {Constants.DatabaseSchema.Tables.ContentVersion} v1 +LEFT JOIN {Constants.DatabaseSchema.Tables.ContentVersion} v2 ON v1.nodeId=v2.nodeId AND v2.[current]=1 GROUP BY v1.nodeId, v1.id HAVING COUNT(v2.id) <> 1").Any()) - { - Debugger.Break(); - throw new Exception("Migration failed: missing or duplicate 'current' content versions."); - } + { + Debugger.Break(); + throw new Exception("Migration failed: missing or duplicate 'current' content versions."); + } + } + + private void MigratePropertyData() + { + // if the table has already been renamed, we're done + if (TableExists(Constants.DatabaseSchema.Tables.PropertyData)) + { + return; } - private void MigratePropertyData() + // add columns + if (!ColumnExists(PreTables.PropertyData, "languageId")) { - // if the table has already been renamed, we're done - if (TableExists(Cms.Core.Constants.DatabaseSchema.Tables.PropertyData)) - return; - - // add columns - if (!ColumnExists(PreTables.PropertyData, "languageId")) - AddColumn(PreTables.PropertyData, "languageId"); - if (!ColumnExists(PreTables.PropertyData, "segment")) - AddColumn(PreTables.PropertyData, "segment"); - - // rename columns - if (ColumnExists(PreTables.PropertyData, "dataNtext")) - ReplaceColumn(PreTables.PropertyData, "dataNtext", "textValue"); - if (ColumnExists(PreTables.PropertyData, "dataNvarchar")) - ReplaceColumn(PreTables.PropertyData, "dataNvarchar", "varcharValue"); - if (ColumnExists(PreTables.PropertyData, "dataDecimal")) - ReplaceColumn(PreTables.PropertyData, "dataDecimal", "decimalValue"); - if (ColumnExists(PreTables.PropertyData, "dataInt")) - ReplaceColumn(PreTables.PropertyData, "dataInt", "intValue"); - if (ColumnExists(PreTables.PropertyData, "dataDate")) - ReplaceColumn(PreTables.PropertyData, "dataDate", "dateValue"); - - // transform column versionId from guid to integer (contentVersion.id) - if (ColumnType(PreTables.PropertyData, "versionId") == "uniqueidentifier") - { - Alter.Table(PreTables.PropertyData).AddColumn("versionId2").AsInt32().Nullable().Do(); + AddColumn(PreTables.PropertyData, "languageId"); + } - Database.Execute($@"UPDATE {PreTables.PropertyData} SET versionId2={PreTables.ContentVersion}.id + if (!ColumnExists(PreTables.PropertyData, "segment")) + { + AddColumn(PreTables.PropertyData, "segment"); + } + + // rename columns + if (ColumnExists(PreTables.PropertyData, "dataNtext")) + { + ReplaceColumn(PreTables.PropertyData, "dataNtext", "textValue"); + } + + if (ColumnExists(PreTables.PropertyData, "dataNvarchar")) + { + ReplaceColumn(PreTables.PropertyData, "dataNvarchar", "varcharValue"); + } + + if (ColumnExists(PreTables.PropertyData, "dataDecimal")) + { + ReplaceColumn(PreTables.PropertyData, "dataDecimal", "decimalValue"); + } + + if (ColumnExists(PreTables.PropertyData, "dataInt")) + { + ReplaceColumn(PreTables.PropertyData, "dataInt", "intValue"); + } + + if (ColumnExists(PreTables.PropertyData, "dataDate")) + { + ReplaceColumn(PreTables.PropertyData, "dataDate", "dateValue"); + } + + // transform column versionId from guid to integer (contentVersion.id) + if (ColumnType(PreTables.PropertyData, "versionId") == "uniqueidentifier") + { + Alter.Table(PreTables.PropertyData).AddColumn("versionId2").AsInt32().Nullable().Do(); + + Database.Execute($@"UPDATE {PreTables.PropertyData} SET versionId2={PreTables.ContentVersion}.id FROM {PreTables.ContentVersion} INNER JOIN {PreTables.PropertyData} ON {PreTables.ContentVersion}.versionId = {PreTables.PropertyData}.versionId"); - Delete.Column("versionId").FromTable(PreTables.PropertyData).Do(); - ReplaceColumn(PreTables.PropertyData, "versionId2", "versionId"); - } + Delete.Column("versionId").FromTable(PreTables.PropertyData).Do(); + ReplaceColumn(PreTables.PropertyData, "versionId2", "versionId"); + } + + // drop column + if (ColumnExists(PreTables.PropertyData, "contentNodeId")) + { + Delete.Column("contentNodeId").FromTable(PreTables.PropertyData).Do(); + } - // drop column - if (ColumnExists(PreTables.PropertyData, "contentNodeId")) - Delete.Column("contentNodeId").FromTable(PreTables.PropertyData).Do(); + // rename table + Rename.Table(PreTables.PropertyData).To(Constants.DatabaseSchema.Tables.PropertyData).Do(); + } + + private void CreatePropertyDataIndexes() + { + // Creates a temporary index on umbracoPropertyData to speed up other migrations which update property values. + // It will be removed in CreateKeysAndIndexes before the normal indexes for the table are created + TableDefinition tableDefinition = DefinitionFactory.GetTableDefinition(typeof(PropertyDataDto), SqlSyntax); + Execute.Sql(SqlSyntax.FormatPrimaryKey(tableDefinition)).Do(); + Create.Index("IX_umbracoPropertyData_Temp").OnTable(PropertyDataDto.TableName) + .WithOptions().Unique() + .WithOptions().NonClustered() + .OnColumn("versionId").Ascending() + .OnColumn("propertyTypeId").Ascending() + .OnColumn("languageId").Ascending() + .OnColumn("segment").Ascending() + .Do(); + } - // rename table - Rename.Table(PreTables.PropertyData).To(Cms.Core.Constants.DatabaseSchema.Tables.PropertyData).Do(); + private void MigrateContentAndPropertyTypes() + { + if (!ColumnExists(PreTables.ContentType, "variations")) + { + AddColumn(PreTables.ContentType, "variations"); } - private void CreatePropertyDataIndexes() + if (!ColumnExists(PreTables.PropertyType, "variations")) { - // Creates a temporary index on umbracoPropertyData to speed up other migrations which update property values. - // It will be removed in CreateKeysAndIndexes before the normal indexes for the table are created - var tableDefinition = DefinitionFactory.GetTableDefinition(typeof(PropertyDataDto), SqlSyntax); - Execute.Sql(SqlSyntax.FormatPrimaryKey(tableDefinition)).Do(); - Create.Index("IX_umbracoPropertyData_Temp").OnTable(PropertyDataDto.TableName) - .WithOptions().Unique() - .WithOptions().NonClustered() - .OnColumn("versionId").Ascending() - .OnColumn("propertyTypeId").Ascending() - .OnColumn("languageId").Ascending() - .OnColumn("segment").Ascending() - .Do(); + AddColumn(PreTables.PropertyType, "variations"); } + } - private void MigrateContentAndPropertyTypes() + private void MigrateContent() + { + // if the table has already been renamed, we're done + if (TableExists(Constants.DatabaseSchema.Tables.Content)) { - if (!ColumnExists(PreTables.ContentType, "variations")) - AddColumn(PreTables.ContentType, "variations"); - if (!ColumnExists(PreTables.PropertyType, "variations")) - AddColumn(PreTables.PropertyType, "variations"); + return; } - private void MigrateContent() + // rename columns + if (ColumnExists(PreTables.Content, "contentType")) { - // if the table has already been renamed, we're done - if (TableExists(Cms.Core.Constants.DatabaseSchema.Tables.Content)) - return; + ReplaceColumn(PreTables.Content, "contentType", "contentTypeId"); + } - // rename columns - if (ColumnExists(PreTables.Content, "contentType")) - ReplaceColumn(PreTables.Content, "contentType", "contentTypeId"); + // drop columns + if (ColumnExists(PreTables.Content, "pk")) + { + Delete.Column("pk").FromTable(PreTables.Content).Do(); + } - // drop columns - if (ColumnExists(PreTables.Content, "pk")) - Delete.Column("pk").FromTable(PreTables.Content).Do(); + // rename table + Rename.Table(PreTables.Content).To(Constants.DatabaseSchema.Tables.Content).Do(); + } - // rename table - Rename.Table(PreTables.Content).To(Cms.Core.Constants.DatabaseSchema.Tables.Content).Do(); + private void MigrateVersions() + { + // if the table has already been renamed, we're done + if (TableExists(Constants.DatabaseSchema.Tables.ContentVersion)) + { + return; } - private void MigrateVersions() + // if the table already exists, we're done + if (TableExists(Constants.DatabaseSchema.Tables.DocumentVersion)) { - // if the table has already been renamed, we're done - if (TableExists(Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion)) - return; + return; + } - // if the table already exists, we're done - if (TableExists(Cms.Core.Constants.DatabaseSchema.Tables.DocumentVersion)) - return; + // if the table has already been renamed, we're done + if (TableExists(Constants.DatabaseSchema.Tables.Document)) + { + return; + } - // if the table has already been renamed, we're done - if (TableExists(Cms.Core.Constants.DatabaseSchema.Tables.Document)) - return; + // do it all at once - // do it all at once + // add contentVersion columns + if (!ColumnExists(PreTables.ContentVersion, "text")) + { + AddColumn(PreTables.ContentVersion, "text"); + } - // add contentVersion columns - if (!ColumnExists(PreTables.ContentVersion, "text")) - AddColumn(PreTables.ContentVersion, "text"); - if (!ColumnExists(PreTables.ContentVersion, "current")) + if (!ColumnExists(PreTables.ContentVersion, "current")) + { + AddColumn(PreTables.ContentVersion, "current", out IEnumerable sqls); + Database.Execute( + $@"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} SET {SqlSyntax.GetQuotedColumnName("current")}=0"); + foreach (var sql in sqls) { - AddColumn(PreTables.ContentVersion, "current", out var sqls); - Database.Execute($@"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} SET {SqlSyntax.GetQuotedColumnName("current")}=0"); - foreach (var sql in sqls) Database.Execute(sql); + Database.Execute(sql); } - if (!ColumnExists(PreTables.ContentVersion, "userId")) + } + + if (!ColumnExists(PreTables.ContentVersion, "userId")) + { + AddColumn(PreTables.ContentVersion, "userId", out IEnumerable sqls); + Database.Execute($@"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} SET userId=0"); + foreach (var sql in sqls) { - AddColumn(PreTables.ContentVersion, "userId", out var sqls); - Database.Execute($@"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} SET userId=0"); - foreach (var sql in sqls) Database.Execute(sql); + Database.Execute(sql); } + } - // rename contentVersion contentId column - if (ColumnExists(PreTables.ContentVersion, "ContentId")) - ReplaceColumn(PreTables.ContentVersion, "ContentId", "nodeId"); + // rename contentVersion contentId column + if (ColumnExists(PreTables.ContentVersion, "ContentId")) + { + ReplaceColumn(PreTables.ContentVersion, "ContentId", "nodeId"); + } - // populate contentVersion text, current and userId columns for documents - Database.Execute($@"UPDATE {PreTables.ContentVersion} SET text=d.text, {SqlSyntax.GetQuotedColumnName("current")}=(d.newest & ~d.published), userId=d.documentUser + // populate contentVersion text, current and userId columns for documents + Database.Execute( + $@"UPDATE {PreTables.ContentVersion} SET text=d.text, {SqlSyntax.GetQuotedColumnName("current")}=(d.newest & ~d.published), userId=d.documentUser FROM {PreTables.ContentVersion} v INNER JOIN {PreTables.Document} d ON d.versionId = v.versionId"); - // populate contentVersion text and current columns for non-documents, userId is default - Database.Execute($@"UPDATE {PreTables.ContentVersion} SET text=n.text, {SqlSyntax.GetQuotedColumnName("current")}=1, userId=0 + // populate contentVersion text and current columns for non-documents, userId is default + Database.Execute( + $@"UPDATE {PreTables.ContentVersion} SET text=n.text, {SqlSyntax.GetQuotedColumnName("current")}=1, userId=0 FROM {PreTables.ContentVersion} cver -JOIN {SqlSyntax.GetQuotedTableName(Cms.Core.Constants.DatabaseSchema.Tables.Node)} n ON cver.nodeId=n.id +JOIN {SqlSyntax.GetQuotedTableName(Constants.DatabaseSchema.Tables.Node)} n ON cver.nodeId=n.id WHERE cver.versionId NOT IN (SELECT versionId FROM {SqlSyntax.GetQuotedTableName(PreTables.Document)})"); - // create table - Create.Table(withoutKeysAndIndexes: true).Do(); + // create table + Create.Table(true).Do(); - // every document row becomes a document version - Database.Execute($@"INSERT INTO {SqlSyntax.GetQuotedTableName(Cms.Core.Constants.DatabaseSchema.Tables.DocumentVersion)} (id, templateId, published) + // every document row becomes a document version + Database.Execute( + $@"INSERT INTO {SqlSyntax.GetQuotedTableName(Constants.DatabaseSchema.Tables.DocumentVersion)} (id, templateId, published) SELECT cver.id, doc.templateId, doc.published FROM {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} cver JOIN {SqlSyntax.GetQuotedTableName(PreTables.Document)} doc ON doc.nodeId=cver.nodeId AND doc.versionId=cver.versionId"); - // need to add extra rows for where published=newest - // 'cos INSERT above has inserted the 'published' document version - // and v8 always has a 'edited' document version too - Database.Execute($@" + // need to add extra rows for where published=newest + // 'cos INSERT above has inserted the 'published' document version + // and v8 always has a 'edited' document version too + Database.Execute($@" INSERT INTO {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} (nodeId, versionId, versionDate, userId, {SqlSyntax.GetQuotedColumnName("current")}, text) SELECT doc.nodeId, NEWID(), doc.updateDate, doc.documentUser, 1, doc.text FROM {SqlSyntax.GetQuotedTableName(PreTables.Document)} doc JOIN {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} cver ON doc.nodeId=cver.nodeId AND doc.versionId=cver.versionId WHERE doc.newest=1 AND doc.published=1"); - Database.Execute($@" -INSERT INTO {SqlSyntax.GetQuotedTableName(Cms.Core.Constants.DatabaseSchema.Tables.DocumentVersion)} (id, templateId, published) + Database.Execute($@" +INSERT INTO {SqlSyntax.GetQuotedTableName(Constants.DatabaseSchema.Tables.DocumentVersion)} (id, templateId, published) SELECT cverNew.id, doc.templateId, 0 FROM {SqlSyntax.GetQuotedTableName(PreTables.Document)} doc JOIN {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} cverNew ON doc.nodeId = cverNew.nodeId WHERE doc.newest=1 AND doc.published=1 AND cverNew.{SqlSyntax.GetQuotedColumnName("current")} = 1"); - Database.Execute($@" + Database.Execute($@" INSERT INTO {SqlSyntax.GetQuotedTableName(PropertyDataDto.TableName)} (propertytypeid,languageId,segment,textValue,varcharValue,decimalValue,intValue,dateValue,versionId) SELECT propertytypeid,languageId,segment,textValue,varcharValue,decimalValue,intValue,dateValue,cverNew.id FROM {SqlSyntax.GetQuotedTableName(PreTables.Document)} doc @@ -219,126 +274,137 @@ private void MigrateVersions() WHERE doc.newest=1 AND doc.published=1 AND cverNew.{SqlSyntax.GetQuotedColumnName("current")} = 1"); - // reduce document to 1 row per content - Database.Execute($@"DELETE FROM {PreTables.Document} + // reduce document to 1 row per content + Database.Execute($@"DELETE FROM {PreTables.Document} WHERE versionId NOT IN (SELECT (versionId) FROM {PreTables.ContentVersion} WHERE {SqlSyntax.GetQuotedColumnName("current")} = 1) AND (published<>1 OR newest<>1)"); - // ensure that documents with a published version are marked as published - Database.Execute($@"UPDATE {PreTables.Document} SET published=1 WHERE nodeId IN ( -SELECT nodeId FROM {PreTables.ContentVersion} cv INNER JOIN {Cms.Core.Constants.DatabaseSchema.Tables.DocumentVersion} dv ON dv.id = cv.id WHERE dv.published=1)"); - - // drop some document columns - Delete.Column("text").FromTable(PreTables.Document).Do(); - Delete.Column("templateId").FromTable(PreTables.Document).Do(); - Delete.Column("documentUser").FromTable(PreTables.Document).Do(); - Delete.DefaultConstraint().OnTable(PreTables.Document).OnColumn("updateDate").Do(); - Delete.Column("updateDate").FromTable(PreTables.Document).Do(); - Delete.Column("versionId").FromTable(PreTables.Document).Do(); - Delete.DefaultConstraint().OnTable(PreTables.Document).OnColumn("newest").Do(); - Delete.Column("newest").FromTable(PreTables.Document).Do(); - - // add and populate edited column - if (!ColumnExists(PreTables.Document, "edited")) + // ensure that documents with a published version are marked as published + Database.Execute($@"UPDATE {PreTables.Document} SET published=1 WHERE nodeId IN ( +SELECT nodeId FROM {PreTables.ContentVersion} cv INNER JOIN {Constants.DatabaseSchema.Tables.DocumentVersion} dv ON dv.id = cv.id WHERE dv.published=1)"); + + // drop some document columns + Delete.Column("text").FromTable(PreTables.Document).Do(); + Delete.Column("templateId").FromTable(PreTables.Document).Do(); + Delete.Column("documentUser").FromTable(PreTables.Document).Do(); + Delete.DefaultConstraint().OnTable(PreTables.Document).OnColumn("updateDate").Do(); + Delete.Column("updateDate").FromTable(PreTables.Document).Do(); + Delete.Column("versionId").FromTable(PreTables.Document).Do(); + Delete.DefaultConstraint().OnTable(PreTables.Document).OnColumn("newest").Do(); + Delete.Column("newest").FromTable(PreTables.Document).Do(); + + // add and populate edited column + if (!ColumnExists(PreTables.Document, "edited")) + { + AddColumn(PreTables.Document, "edited", out IEnumerable sqls); + Database.Execute($"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.Document)} SET edited=~published"); + foreach (var sql in sqls) { - AddColumn(PreTables.Document, "edited", out var sqls); - Database.Execute($"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.Document)} SET edited=~published"); - foreach (var sql in sqls) Database.Execute(sql); + Database.Execute(sql); } + } - // set 'edited' to true whenever a 'non-published' property data is != a published one - // cannot compare NTEXT values in TSQL - // cannot cast NTEXT to NVARCHAR(MAX) in SQLCE - // ... bah ... - var temp = Database.Fetch($@"SELECT n.id, + // set 'edited' to true whenever a 'non-published' property data is != a published one + // cannot compare NTEXT values in TSQL + // cannot cast NTEXT to NVARCHAR(MAX) in SQLCE + // ... bah ... + List? temp = Database.Fetch($@"SELECT n.id, v1.intValue intValue1, v1.decimalValue decimalValue1, v1.dateValue dateValue1, v1.varcharValue varcharValue1, v1.textValue textValue1, v2.intValue intValue2, v2.decimalValue decimalValue2, v2.dateValue dateValue2, v2.varcharValue varcharValue2, v2.textValue textValue2 -FROM {Cms.Core.Constants.DatabaseSchema.Tables.Node} n +FROM {Constants.DatabaseSchema.Tables.Node} n JOIN {PreTables.ContentVersion} cv1 ON n.id=cv1.nodeId AND cv1.{SqlSyntax.GetQuotedColumnName("current")}=1 -JOIN {Cms.Core.Constants.DatabaseSchema.Tables.PropertyData} v1 ON cv1.id=v1.versionId +JOIN {Constants.DatabaseSchema.Tables.PropertyData} v1 ON cv1.id=v1.versionId JOIN {PreTables.ContentVersion} cv2 ON n.id=cv2.nodeId -JOIN {Cms.Core.Constants.DatabaseSchema.Tables.DocumentVersion} dv ON cv2.id=dv.id AND dv.published=1 -JOIN {Cms.Core.Constants.DatabaseSchema.Tables.PropertyData} v2 ON cv2.id=v2.versionId +JOIN {Constants.DatabaseSchema.Tables.DocumentVersion} dv ON cv2.id=dv.id AND dv.published=1 +JOIN {Constants.DatabaseSchema.Tables.PropertyData} v2 ON cv2.id=v2.versionId WHERE v1.propertyTypeId=v2.propertyTypeId AND (v1.languageId=v2.languageId OR (v1.languageId IS NULL AND v2.languageId IS NULL)) AND (v1.segment=v2.segment OR (v1.segment IS NULL AND v2.segment IS NULL))"); - var updatedIds = new HashSet(); - foreach (var t in temp) - if (t.intValue1 != t.intValue2 || t.decimalValue1 != t.decimalValue2 || t.dateValue1 != t.dateValue2 || t.varcharValue1 != t.varcharValue2 || t.textValue1 != t.textValue2) - if (updatedIds.Add((int)t.id)) - Database.Execute($"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.Document)} SET edited=1 WHERE nodeId=@nodeId", new { nodeId = t.id }); + var updatedIds = new HashSet(); + foreach (var t in temp) + { + if (t.intValue1 != t.intValue2 || t.decimalValue1 != t.decimalValue2 || t.dateValue1 != t.dateValue2 || + t.varcharValue1 != t.varcharValue2 || t.textValue1 != t.textValue2) + { + if (updatedIds.Add((int)t.id)) + { + Database.Execute( + $"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.Document)} SET edited=1 WHERE nodeId=@nodeId", + new {nodeId = t.id}); + } + } + } - // drop more columns - Delete.Column("versionId").FromTable(PreTables.ContentVersion).Do(); + // drop more columns + Delete.Column("versionId").FromTable(PreTables.ContentVersion).Do(); - // rename tables - Rename.Table(PreTables.ContentVersion).To(Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion).Do(); - Rename.Table(PreTables.Document).To(Cms.Core.Constants.DatabaseSchema.Tables.Document).Do(); - } + // rename tables + Rename.Table(PreTables.ContentVersion).To(Constants.DatabaseSchema.Tables.ContentVersion).Do(); + Rename.Table(PreTables.Document).To(Constants.DatabaseSchema.Tables.Document).Do(); + } - private static class PreTables - { - // ReSharper disable UnusedMember.Local - public const string Lock = "umbracoLock"; - public const string Log = "umbracoLog"; - - public const string Node = "umbracoNode"; - public const string NodeData = "cmsContentNu"; - public const string NodeXml = "cmsContentXml"; - public const string NodePreviewXml = "cmsPreviewXml"; - - public const string ContentType = "cmsContentType"; - public const string ContentChildType = "cmsContentTypeAllowedContentType"; - public const string DocumentType = "cmsDocumentType"; - public const string ElementTypeTree = "cmsContentType2ContentType"; - public const string DataType = "cmsDataType"; - public const string DataTypePreValue = "cmsDataTypePreValues"; - public const string Template = "cmsTemplate"; - - public const string Content = "cmsContent"; - public const string ContentVersion = "cmsContentVersion"; - public const string Document = "cmsDocument"; - - public const string PropertyType = "cmsPropertyType"; - public const string PropertyTypeGroup = "cmsPropertyTypeGroup"; - public const string PropertyData = "cmsPropertyData"; - - public const string RelationType = "umbracoRelationType"; - public const string Relation = "umbracoRelation"; - - public const string Domain = "umbracoDomains"; - public const string Language = "umbracoLanguage"; - public const string DictionaryEntry = "cmsDictionary"; - public const string DictionaryValue = "cmsLanguageText"; - - public const string User = "umbracoUser"; - public const string UserGroup = "umbracoUserGroup"; - public const string UserStartNode = "umbracoUserStartNode"; - public const string User2UserGroup = "umbracoUser2UserGroup"; - public const string User2NodeNotify = "umbracoUser2NodeNotify"; - public const string UserGroup2App = "umbracoUserGroup2App"; - public const string UserGroup2NodePermission = "umbracoUserGroup2NodePermission"; - public const string ExternalLogin = "umbracoExternalLogin"; - - public const string Macro = "cmsMacro"; - public const string MacroProperty = "cmsMacroProperty"; - - public const string Member = "cmsMember"; - public const string MemberType = "cmsMemberType"; - public const string Member2MemberGroup = "cmsMember2MemberGroup"; - - public const string Access = "umbracoAccess"; - public const string AccessRule = "umbracoAccessRule"; - public const string RedirectUrl = "umbracoRedirectUrl"; - - public const string CacheInstruction = "umbracoCacheInstruction"; - public const string Migration = "umbracoMigration"; - public const string Server = "umbracoServer"; - - public const string Tag = "cmsTags"; - public const string TagRelationship = "cmsTagRelationship"; - - // ReSharper restore UnusedMember.Local - } + private static class PreTables + { + // ReSharper disable UnusedMember.Local + public const string Lock = "umbracoLock"; + public const string Log = "umbracoLog"; + + public const string Node = "umbracoNode"; + public const string NodeData = "cmsContentNu"; + public const string NodeXml = "cmsContentXml"; + public const string NodePreviewXml = "cmsPreviewXml"; + + public const string ContentType = "cmsContentType"; + public const string ContentChildType = "cmsContentTypeAllowedContentType"; + public const string DocumentType = "cmsDocumentType"; + public const string ElementTypeTree = "cmsContentType2ContentType"; + public const string DataType = "cmsDataType"; + public const string DataTypePreValue = "cmsDataTypePreValues"; + public const string Template = "cmsTemplate"; + + public const string Content = "cmsContent"; + public const string ContentVersion = "cmsContentVersion"; + public const string Document = "cmsDocument"; + + public const string PropertyType = "cmsPropertyType"; + public const string PropertyTypeGroup = "cmsPropertyTypeGroup"; + public const string PropertyData = "cmsPropertyData"; + + public const string RelationType = "umbracoRelationType"; + public const string Relation = "umbracoRelation"; + + public const string Domain = "umbracoDomains"; + public const string Language = "umbracoLanguage"; + public const string DictionaryEntry = "cmsDictionary"; + public const string DictionaryValue = "cmsLanguageText"; + + public const string User = "umbracoUser"; + public const string UserGroup = "umbracoUserGroup"; + public const string UserStartNode = "umbracoUserStartNode"; + public const string User2UserGroup = "umbracoUser2UserGroup"; + public const string User2NodeNotify = "umbracoUser2NodeNotify"; + public const string UserGroup2App = "umbracoUserGroup2App"; + public const string UserGroup2NodePermission = "umbracoUserGroup2NodePermission"; + public const string ExternalLogin = "umbracoExternalLogin"; + + public const string Macro = "cmsMacro"; + public const string MacroProperty = "cmsMacroProperty"; + + public const string Member = "cmsMember"; + public const string MemberType = "cmsMemberType"; + public const string Member2MemberGroup = "cmsMember2MemberGroup"; + + public const string Access = "umbracoAccess"; + public const string AccessRule = "umbracoAccessRule"; + public const string RedirectUrl = "umbracoRedirectUrl"; + + public const string CacheInstruction = "umbracoCacheInstruction"; + public const string Migration = "umbracoMigration"; + public const string Server = "umbracoServer"; + + public const string Tag = "cmsTags"; + public const string TagRelationship = "cmsTagRelationship"; + + // ReSharper restore UnusedMember.Local } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_1/ChangeNuCacheJsonFormat.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_1/ChangeNuCacheJsonFormat.cs index 74445d268d62..dc315eed9f4d 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_1/ChangeNuCacheJsonFormat.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_1/ChangeNuCacheJsonFormat.cs @@ -1,16 +1,14 @@ using Umbraco.Cms.Infrastructure.Migrations.PostMigrations; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_1 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_1; + +public class ChangeNuCacheJsonFormat : MigrationBase { - public class ChangeNuCacheJsonFormat : MigrationBase + public ChangeNuCacheJsonFormat(IMigrationContext context) : base(context) { - public ChangeNuCacheJsonFormat(IMigrationContext context) : base(context) - { } - - protected override void Migrate() - { - // nothing - just adding the post-migration - Context.AddPostMigration(); - } } + + protected override void Migrate() => + // nothing - just adding the post-migration + Context.AddPostMigration(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_10_0/AddPropertyTypeLabelOnTopColumn.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_10_0/AddPropertyTypeLabelOnTopColumn.cs index 6c6fd6166cd0..002cdfa6512c 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_10_0/AddPropertyTypeLabelOnTopColumn.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_10_0/AddPropertyTypeLabelOnTopColumn.cs @@ -1,20 +1,18 @@ -using System.Linq; -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_10_0 -{ +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_10_0; - public class AddPropertyTypeLabelOnTopColumn : MigrationBase +public class AddPropertyTypeLabelOnTopColumn : MigrationBase +{ + public AddPropertyTypeLabelOnTopColumn(IMigrationContext context) + : base(context) { - public AddPropertyTypeLabelOnTopColumn(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); + protected override void Migrate() + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); - AddColumnIfNotExists(columns, "labelOnTop"); - } + AddColumnIfNotExists(columns, "labelOnTop"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs index 23bb979dd982..596c730e6b61 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs @@ -1,47 +1,40 @@ using NPoco; -using System.Linq; using Umbraco.Cms.Core; -using Umbraco.Cms.Infrastructure.Persistence.Dtos; -using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_15_0 -{ - public class AddCmsContentNuByteColumn : MigrationBase - { - public AddCmsContentNuByteColumn(IMigrationContext context) - : base(context) - { +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_15_0; - } +public class AddCmsContentNuByteColumn : MigrationBase +{ + private const string TempTableName = Constants.DatabaseSchema.TableNamePrefix + "cms" + "ContentNuTEMP"; - protected override void Migrate() - { - AlterColumn(Constants.DatabaseSchema.Tables.NodeData, "data"); + public AddCmsContentNuByteColumn(IMigrationContext context) + : base(context) + { + } - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); - AddColumnIfNotExists(columns, "dataRaw"); - } + protected override void Migrate() + { + AlterColumn(Constants.DatabaseSchema.Tables.NodeData, "data"); - private const string TempTableName = Constants.DatabaseSchema.TableNamePrefix + "cms" + "ContentNuTEMP"; + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); + AddColumnIfNotExists(columns, "dataRaw"); + } - [TableName(TempTableName)] - [ExplicitColumns] - private class ContentNuDtoTemp - { - [Column("nodeId")] - public int NodeId { get; set; } + [TableName(TempTableName)] + [ExplicitColumns] + private class ContentNuDtoTemp + { + [Column("nodeId")] public int NodeId { get; set; } - [Column("published")] - public bool Published { get; set; } + [Column("published")] public bool Published { get; set; } - [Column("data")] - [SpecialDbType(SpecialDbTypes.NTEXT)] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Data { get; set; } + [Column("data")] + [SpecialDbType(SpecialDbTypes.NTEXT)] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Data { get; set; } - [Column("rv")] - public long Rv { get; set; } - } + [Column("rv")] public long Rv { get; set; } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/UpdateCmsPropertyGroupIdSeed.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/UpdateCmsPropertyGroupIdSeed.cs index 496e12a1fa71..13308f845011 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/UpdateCmsPropertyGroupIdSeed.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/UpdateCmsPropertyGroupIdSeed.cs @@ -1,16 +1,13 @@ -using Umbraco.Cms.Infrastructure.Persistence; +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_15_0; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_15_0 +public class UpdateCmsPropertyGroupIdSeed : MigrationBase { - public class UpdateCmsPropertyGroupIdSeed : MigrationBase + public UpdateCmsPropertyGroupIdSeed(IMigrationContext context) : base(context) { - public UpdateCmsPropertyGroupIdSeed(IMigrationContext context) : base(context) - { - } + } - protected override void Migrate() - { - // NOOP - was sql ce only - } + protected override void Migrate() + { + // NOOP - was sql ce only } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/UpgradedIncludeIndexes.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/UpgradedIncludeIndexes.cs index 9bdce9bfbf0d..aa22c8203cfa 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/UpgradedIncludeIndexes.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/UpgradedIncludeIndexes.cs @@ -1,66 +1,73 @@ -using System.Linq; using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Execute.Expressions; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_15_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_15_0; + +public class UpgradedIncludeIndexes : MigrationBase { - public class UpgradedIncludeIndexes : MigrationBase + public UpgradedIncludeIndexes(IMigrationContext context) + : base(context) { - public UpgradedIncludeIndexes(IMigrationContext context) - : base(context) - { - - } + } - protected override void Migrate() + protected override void Migrate() + { + // Need to drop the FK for the redirect table before modifying the unique id index + Delete.ForeignKey() + .FromTable(Constants.DatabaseSchema.Tables.RedirectUrl) + .ForeignColumn("contentKey") + .ToTable(NodeDto.TableName) + .PrimaryColumn("uniqueID") + .Do(); + var nodeDtoIndexes = new[] { - // Need to drop the FK for the redirect table before modifying the unique id index - Delete.ForeignKey() - .FromTable(Constants.DatabaseSchema.Tables.RedirectUrl) - .ForeignColumn("contentKey") - .ToTable(NodeDto.TableName) - .PrimaryColumn("uniqueID") - .Do(); - var nodeDtoIndexes = new[] { $"IX_{NodeDto.TableName}_UniqueId", $"IX_{NodeDto.TableName}_ObjectType", $"IX_{NodeDto.TableName}_Level" }; - DeleteIndexes(nodeDtoIndexes); // delete existing ones - CreateIndexes(nodeDtoIndexes); // update/add - // Now re-create the FK for the redirect table - Create.ForeignKey() - .FromTable(Constants.DatabaseSchema.Tables.RedirectUrl) - .ForeignColumn("contentKey") - .ToTable(NodeDto.TableName) - .PrimaryColumn("uniqueID") - .Do(); - + $"IX_{NodeDto.TableName}_UniqueId", $"IX_{NodeDto.TableName}_ObjectType", + $"IX_{NodeDto.TableName}_Level" + }; + DeleteIndexes(nodeDtoIndexes); // delete existing ones + CreateIndexes(nodeDtoIndexes); // update/add + // Now re-create the FK for the redirect table + Create.ForeignKey() + .FromTable(Constants.DatabaseSchema.Tables.RedirectUrl) + .ForeignColumn("contentKey") + .ToTable(NodeDto.TableName) + .PrimaryColumn("uniqueID") + .Do(); - var contentVersionIndexes = new[] { $"IX_{ContentVersionDto.TableName}_NodeId", $"IX_{ContentVersionDto.TableName}_Current" }; - DeleteIndexes(contentVersionIndexes); // delete existing ones - CreateIndexes(contentVersionIndexes); // update/add - } - private void DeleteIndexes(params string[] toDelete) + var contentVersionIndexes = new[] { - var tableDef = DefinitionFactory.GetTableDefinition(typeof(T), Context.SqlContext.SqlSyntax); - - foreach (var i in toDelete) - if (IndexExists(i)) - Delete.Index(i).OnTable(tableDef.Name).Do(); + $"IX_{ContentVersionDto.TableName}_NodeId", $"IX_{ContentVersionDto.TableName}_Current" + }; + DeleteIndexes(contentVersionIndexes); // delete existing ones + CreateIndexes(contentVersionIndexes); // update/add + } - } + private void DeleteIndexes(params string[] toDelete) + { + TableDefinition tableDef = DefinitionFactory.GetTableDefinition(typeof(T), Context.SqlContext.SqlSyntax); - private void CreateIndexes(params string[] toCreate) + foreach (var i in toDelete) { - var tableDef = DefinitionFactory.GetTableDefinition(typeof(T), Context.SqlContext.SqlSyntax); - - foreach (var c in toCreate) + if (IndexExists(i)) { - // get the definition by name - var index = tableDef.Indexes.First(x => x.Name == c); - new ExecuteSqlStatementExpression(Context) { SqlStatement = Context.SqlContext.SqlSyntax.Format(index) }.Execute(); + Delete.Index(i).OnTable(tableDef.Name).Do(); } + } + } + + private void CreateIndexes(params string[] toCreate) + { + TableDefinition tableDef = DefinitionFactory.GetTableDefinition(typeof(T), Context.SqlContext.SqlSyntax); + foreach (var c in toCreate) + { + // get the definition by name + IndexDefinition index = tableDef.Indexes.First(x => x.Name == c); + new ExecuteSqlStatementExpression(Context) {SqlStatement = Context.SqlContext.SqlSyntax.Format(index)} + .Execute(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_17_0/AddPropertyTypeGroupColumns.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_17_0/AddPropertyTypeGroupColumns.cs index feedc56d9af8..259c29c79e1b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_17_0/AddPropertyTypeGroupColumns.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_17_0/AddPropertyTypeGroupColumns.cs @@ -1,64 +1,69 @@ -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_17_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_17_0; + +public class AddPropertyTypeGroupColumns : MigrationBase { - public class AddPropertyTypeGroupColumns : MigrationBase - { - private readonly IShortStringHelper _shortStringHelper; + private readonly IShortStringHelper _shortStringHelper; - public AddPropertyTypeGroupColumns(IMigrationContext context, IShortStringHelper shortStringHelper) - : base(context) => _shortStringHelper = shortStringHelper; + public AddPropertyTypeGroupColumns(IMigrationContext context, IShortStringHelper shortStringHelper) + : base(context) => _shortStringHelper = shortStringHelper; - protected override void Migrate() - { - AddColumn("type"); + protected override void Migrate() + { + AddColumn("type"); - // Add column without constraints - AddColumn("alias", out var sqls); + // Add column without constraints + AddColumn("alias", out IEnumerable sqls); - // Populate non-null alias column - var dtos = Database.Fetch(); - foreach (var dto in PopulateAliases(dtos)) - Database.Update(dto, x => new { x.Alias }); + // Populate non-null alias column + List? dtos = Database.Fetch(); + foreach (PropertyTypeGroupDto dto in PopulateAliases(dtos)) + { + Database.Update(dto, x => new {x.Alias}); + } - // Finally add the constraints - foreach (var sql in sqls) - Database.Execute(sql); + // Finally add the constraints + foreach (var sql in sqls) + { + Database.Execute(sql); } + } - internal IEnumerable PopulateAliases(IEnumerable dtos) + internal IEnumerable PopulateAliases(IEnumerable dtos) + { + foreach (IGrouping dtosPerAlias in dtos.GroupBy(x => + x.Text?.ToSafeAlias(_shortStringHelper, true))) { - foreach (var dtosPerAlias in dtos.GroupBy(x => x.Text?.ToSafeAlias(_shortStringHelper, true))) + IEnumerable> dtosPerAliasAndText = + dtosPerAlias.GroupBy(x => x.Text); + var numberSuffix = 1; + foreach (IGrouping dtosPerText in dtosPerAliasAndText) { - var dtosPerAliasAndText = dtosPerAlias.GroupBy(x => x.Text); - var numberSuffix = 1; - foreach (var dtosPerText in dtosPerAliasAndText) + foreach (PropertyTypeGroupDto dto in dtosPerText) { - foreach (var dto in dtosPerText) - { - dto.Alias = dtosPerAlias.Key ?? string.Empty; - - if (numberSuffix > 1) - { - // More than 1 name found for the alias, so add a suffix - dto.Alias += numberSuffix; - } + dto.Alias = dtosPerAlias.Key ?? string.Empty; - yield return dto; + if (numberSuffix > 1) + { + // More than 1 name found for the alias, so add a suffix + dto.Alias += numberSuffix; } - numberSuffix++; + yield return dto; } - if (numberSuffix > 2) - { - Logger.LogError("Detected the same alias {Alias} for different property group names {Names}, the migration added suffixes, but this might break backwards compatibility.", dtosPerAlias.Key, dtosPerAliasAndText.Select(x => x.Key)); - } + numberSuffix++; + } + + if (numberSuffix > 2) + { + Logger.LogError( + "Detected the same alias {Alias} for different property group names {Names}, the migration added suffixes, but this might break backwards compatibility.", + dtosPerAlias.Key, dtosPerAliasAndText.Select(x => x.Key)); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs index 96d60a30e5bd..5ed1f810a920 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs @@ -1,127 +1,133 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Migrations.PostMigrations; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.Models; +using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_1_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_1_0; + +public class ConvertTinyMceAndGridMediaUrlsToLocalLink : MigrationBase { - public class ConvertTinyMceAndGridMediaUrlsToLocalLink : MigrationBase - { - private readonly IMediaService _mediaService; + private readonly IMediaService _mediaService; - public ConvertTinyMceAndGridMediaUrlsToLocalLink(IMigrationContext context, IMediaService mediaService) : base(context) - { - _mediaService = mediaService ?? throw new ArgumentNullException(nameof(mediaService)); - } + public ConvertTinyMceAndGridMediaUrlsToLocalLink(IMigrationContext context, IMediaService mediaService) : + base(context) => _mediaService = mediaService ?? throw new ArgumentNullException(nameof(mediaService)); - protected override void Migrate() + protected override void Migrate() + { + var mediaLinkPattern = new Regex( + @"(]*href="")(\/ media[^""\?]*)([^>]*>)", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + + Sql sqlPropertyData = Sql() + .Select(r => r.Select(x => x.PropertyTypeDto, r1 => r1.Select(x => x!.DataTypeDto))) + .From() + .InnerJoin() + .On((left, right) => left.PropertyTypeId == right.Id) + .InnerJoin() + .On((left, right) => left.DataTypeId == right.NodeId) + .Where(x => + x.EditorAlias == Constants.PropertyEditors.Aliases.TinyMce || + x.EditorAlias == Constants.PropertyEditors.Aliases.Grid); + + List? properties = Database.Fetch(sqlPropertyData); + + var exceptions = new List(); + foreach (PropertyDataDto80? property in properties) { - var mediaLinkPattern = new Regex( - @"(]*href="")(\/ media[^""\?]*)([^>]*>)", - RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - - var sqlPropertyData = Sql() - .Select(r => r.Select(x => x.PropertyTypeDto, r1 => r1.Select(x => x!.DataTypeDto))) - .From() - .InnerJoin().On((left, right) => left.PropertyTypeId == right.Id) - .InnerJoin().On((left, right) => left.DataTypeId == right.NodeId) - .Where(x => - x.EditorAlias == Cms.Core.Constants.PropertyEditors.Aliases.TinyMce || - x.EditorAlias == Cms.Core.Constants.PropertyEditors.Aliases.Grid); - - var properties = Database.Fetch(sqlPropertyData); - - var exceptions = new List(); - foreach (var property in properties) + var value = property.TextValue; + if (string.IsNullOrWhiteSpace(value)) { - var value = property.TextValue; - if (string.IsNullOrWhiteSpace(value)) continue; + continue; + } - bool propertyChanged = false; - if (property.PropertyTypeDto?.DataTypeDto?.EditorAlias == Cms.Core.Constants.PropertyEditors.Aliases.Grid) + var propertyChanged = false; + if (property.PropertyTypeDto?.DataTypeDto?.EditorAlias == Constants.PropertyEditors.Aliases.Grid) + { + try { - try - { - var obj = JsonConvert.DeserializeObject(value); - var allControls = obj?.SelectTokens("$.sections..rows..areas..controls"); + JObject? obj = JsonConvert.DeserializeObject(value); + IEnumerable? allControls = obj?.SelectTokens("$.sections..rows..areas..controls"); - if (allControls is not null) + if (allControls is not null) + { + foreach (JObject control in allControls.SelectMany(c => c).OfType()) { - foreach (var control in allControls.SelectMany(c => c).OfType()) + JToken? controlValue = control["value"]; + if (controlValue?.Type == JTokenType.String) { - var controlValue = control["value"]; - if (controlValue?.Type == JTokenType.String) - { - control["value"] = UpdateMediaUrls(mediaLinkPattern, controlValue.Value()!, out var controlChanged); - propertyChanged |= controlChanged; - } + control["value"] = UpdateMediaUrls(mediaLinkPattern, controlValue.Value()!, + out var controlChanged); + propertyChanged |= controlChanged; } } - - property.TextValue = JsonConvert.SerializeObject(obj); - } - catch (JsonException e) - { - exceptions.Add(new InvalidOperationException( - "Cannot deserialize the value as json. This can be because the property editor " + - "type is changed from another type into a grid. Old versions of the value in this " + - "property can have the structure from the old property editor type. This needs to be " + - "changed manually before updating the database.\n" + - $"Property info: Id = {property.Id}, LanguageId = {property.LanguageId}, VersionId = {property.VersionId}, Value = {property.Value}" - , e)); - continue; } + property.TextValue = JsonConvert.SerializeObject(obj); } - else + catch (JsonException e) { - property.TextValue = UpdateMediaUrls(mediaLinkPattern, value, out propertyChanged); + exceptions.Add(new InvalidOperationException( + "Cannot deserialize the value as json. This can be because the property editor " + + "type is changed from another type into a grid. Old versions of the value in this " + + "property can have the structure from the old property editor type. This needs to be " + + "changed manually before updating the database.\n" + + $"Property info: Id = {property.Id}, LanguageId = {property.LanguageId}, VersionId = {property.VersionId}, Value = {property.Value}" + , e)); + continue; } - - if (propertyChanged) - Database.Update(property); } - - - if (exceptions.Any()) + else { - throw new AggregateException("One or more errors related to unexpected data in grid values occurred.", exceptions); + property.TextValue = UpdateMediaUrls(mediaLinkPattern, value, out propertyChanged); } - Context.AddPostMigration(); + if (propertyChanged) + { + Database.Update(property); + } } - private string UpdateMediaUrls(Regex mediaLinkPattern, string value, out bool changed) + + if (exceptions.Any()) { - bool matched = false; + throw new AggregateException("One or more errors related to unexpected data in grid values occurred.", + exceptions); + } - var result = mediaLinkPattern.Replace(value, match => - { - matched = true; + Context.AddPostMigration(); + } + + private string UpdateMediaUrls(Regex mediaLinkPattern, string value, out bool changed) + { + var matched = false; - // match groups: - // - 1 = from the beginning of the a tag until href attribute value begins - // - 2 = the href attribute value excluding the querystring (if present) - // - 3 = anything after group 2 until the a tag is closed - var href = match.Groups[2].Value; + var result = mediaLinkPattern.Replace(value, match => + { + matched = true; - var media = _mediaService.GetMediaByPath(href); - return media == null - ? match.Value - : $"{match.Groups[1].Value}/{{localLink:{media.GetUdi()}}}{match.Groups[3].Value}"; - }); + // match groups: + // - 1 = from the beginning of the a tag until href attribute value begins + // - 2 = the href attribute value excluding the querystring (if present) + // - 3 = anything after group 2 until the a tag is closed + var href = match.Groups[2].Value; - changed = matched; + IMedia? media = _mediaService.GetMediaByPath(href); + return media == null + ? match.Value + : $"{match.Groups[1].Value}/{{localLink:{media.GetUdi()}}}{match.Groups[3].Value}"; + }); - return result; - } + changed = matched; + + return result; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_1_0/FixContentNuCascade.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_1_0/FixContentNuCascade.cs index 00389c547edb..1b4f39b506df 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_1_0/FixContentNuCascade.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_1_0/FixContentNuCascade.cs @@ -1,17 +1,17 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_1_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_1_0; + +public class FixContentNuCascade : MigrationBase { - public class FixContentNuCascade : MigrationBase + public FixContentNuCascade(IMigrationContext context) + : base(context) { - public FixContentNuCascade(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - Delete.KeysAndIndexes().Do(); - Create.KeysAndIndexes().Do(); - } + protected override void Migrate() + { + Delete.KeysAndIndexes().Do(); + Create.KeysAndIndexes().Do(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_1_0/RenameUserLoginDtoDateIndex.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_1_0/RenameUserLoginDtoDateIndex.cs index f06477579a7b..672a474ed88f 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_1_0/RenameUserLoginDtoDateIndex.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_1_0/RenameUserLoginDtoDateIndex.cs @@ -1,36 +1,40 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_1_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_1_0; + +public class RenameUserLoginDtoDateIndex : MigrationBase { - public class RenameUserLoginDtoDateIndex : MigrationBase + public RenameUserLoginDtoDateIndex(IMigrationContext context) + : base(context) { - public RenameUserLoginDtoDateIndex(IMigrationContext context) - : base(context) - { } - - protected override void Migrate() - { - // there has been some confusion with an index name, resulting in - // different names depending on which migration path was followed, - // and discrepancies between an upgraded or an installed database. - // better normalize + } - if (IndexExists("IX_umbracoUserLogin_lastValidatedUtc")) - return; + protected override void Migrate() + { + // there has been some confusion with an index name, resulting in + // different names depending on which migration path was followed, + // and discrepancies between an upgraded or an installed database. + // better normalize - if (IndexExists("IX_userLoginDto_lastValidatedUtc")) - Delete - .Index("IX_userLoginDto_lastValidatedUtc") - .OnTable(UserLoginDto.TableName) - .Do(); + if (IndexExists("IX_umbracoUserLogin_lastValidatedUtc")) + { + return; + } - Create - .Index("IX_umbracoUserLogin_lastValidatedUtc") + if (IndexExists("IX_userLoginDto_lastValidatedUtc")) + { + Delete + .Index("IX_userLoginDto_lastValidatedUtc") .OnTable(UserLoginDto.TableName) - .OnColumn("lastValidatedUtc") - .Ascending() - .WithOptions().NonClustered() .Do(); } + + Create + .Index("IX_umbracoUserLogin_lastValidatedUtc") + .OnTable(UserLoginDto.TableName) + .OnColumn("lastValidatedUtc") + .Ascending() + .WithOptions().NonClustered() + .Do(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddMainDomLock.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddMainDomLock.cs index 9fe257fafeee..f6611b52cca0 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddMainDomLock.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddMainDomLock.cs @@ -1,16 +1,15 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_6_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_6_0; + +public class AddMainDomLock : MigrationBase { - public class AddMainDomLock : MigrationBase + public AddMainDomLock(IMigrationContext context) + : base(context) { - public AddMainDomLock(IMigrationContext context) - : base(context) - { } - - protected override void Migrate() - { - Database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.MainDom, Name = "MainDom" }); - } } + + protected override void Migrate() => Database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto {Id = Constants.Locks.MainDom, Name = "MainDom"}); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddNewRelationTypes.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddNewRelationTypes.cs index 9c770adf1599..7e2572782165 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddNewRelationTypes.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddNewRelationTypes.cs @@ -1,33 +1,37 @@ -using Umbraco.Cms.Infrastructure.Migrations.Install; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Migrations.Install; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_6_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_6_0; + +/// +/// Ensures the new relation types are created +/// +public class AddNewRelationTypes : MigrationBase { - /// - /// Ensures the new relation types are created - /// - public class AddNewRelationTypes : MigrationBase + public AddNewRelationTypes(IMigrationContext context) + : base(context) { - public AddNewRelationTypes(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - CreateRelation( - Cms.Core.Constants.Conventions.RelationTypes.RelatedMediaAlias, - Cms.Core.Constants.Conventions.RelationTypes.RelatedMediaName); + protected override void Migrate() + { + CreateRelation( + Constants.Conventions.RelationTypes.RelatedMediaAlias, + Constants.Conventions.RelationTypes.RelatedMediaName); - CreateRelation( - Cms.Core.Constants.Conventions.RelationTypes.RelatedDocumentAlias, - Cms.Core.Constants.Conventions.RelationTypes.RelatedDocumentName); - } + CreateRelation( + Constants.Conventions.RelationTypes.RelatedDocumentAlias, + Constants.Conventions.RelationTypes.RelatedDocumentName); + } - private void CreateRelation(string alias, string name) - { - var uniqueId = DatabaseDataCreator.CreateUniqueRelationTypeId(alias ,name); //this is the same as how it installs so everything is consistent - Insert.IntoTable(Cms.Core.Constants.DatabaseSchema.Tables.RelationType) - .Row(new { typeUniqueId = uniqueId, dual = 0, name, alias }) - .Do(); - } + private void CreateRelation(string alias, string name) + { + Guid uniqueId = + DatabaseDataCreator + .CreateUniqueRelationTypeId(alias, + name); //this is the same as how it installs so everything is consistent + Insert.IntoTable(Constants.DatabaseSchema.Tables.RelationType) + .Row(new {typeUniqueId = uniqueId, dual = 0, name, alias}) + .Do(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddPropertyTypeValidationMessageColumns.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddPropertyTypeValidationMessageColumns.cs index 9a9e2b5e77e6..d8c2fb23f0d2 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddPropertyTypeValidationMessageColumns.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddPropertyTypeValidationMessageColumns.cs @@ -1,21 +1,19 @@ -using System.Linq; -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_6_0 -{ +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_6_0; - public class AddPropertyTypeValidationMessageColumns : MigrationBase +public class AddPropertyTypeValidationMessageColumns : MigrationBase +{ + public AddPropertyTypeValidationMessageColumns(IMigrationContext context) + : base(context) { - public AddPropertyTypeValidationMessageColumns(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); + protected override void Migrate() + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); - AddColumnIfNotExists(columns, "mandatoryMessage"); - AddColumnIfNotExists(columns, "validationRegExpMessage"); - } + AddColumnIfNotExists(columns, "mandatoryMessage"); + AddColumnIfNotExists(columns, "validationRegExpMessage"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/MissingContentVersionsIndexes.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/MissingContentVersionsIndexes.cs index 2d4b227249e0..f337052d9ef5 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/MissingContentVersionsIndexes.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/MissingContentVersionsIndexes.cs @@ -1,33 +1,31 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_6_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_6_0; + +public class MissingContentVersionsIndexes : MigrationBase { - public class MissingContentVersionsIndexes : MigrationBase + private const string IndexName = "IX_" + ContentVersionDto.TableName + "_NodeId"; + + public MissingContentVersionsIndexes(IMigrationContext context) : base(context) { - private const string IndexName = "IX_" + ContentVersionDto.TableName + "_NodeId"; + } - public MissingContentVersionsIndexes(IMigrationContext context) : base(context) - { - } + protected override void Migrate() + { + // We must check before we create an index because if we are upgrading from v7 we force re-create all + // indexes in the whole DB and then this would throw - protected override void Migrate() + if (!IndexExists(IndexName)) { - // We must check before we create an index because if we are upgrading from v7 we force re-create all - // indexes in the whole DB and then this would throw - - if (!IndexExists(IndexName)) - { - Create - .Index(IndexName) - .OnTable(ContentVersionDto.TableName) - .OnColumn("nodeId") - .Ascending() - .OnColumn("current") - .Ascending() - .WithOptions().NonClustered() - .Do(); - } - + Create + .Index(IndexName) + .OnTable(ContentVersionDto.TableName) + .OnColumn("nodeId") + .Ascending() + .OnColumn("current") + .Ascending() + .WithOptions().NonClustered() + .Do(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/UpdateRelationTypeTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/UpdateRelationTypeTable.cs index bc3757eaad2f..2328159fdb5f 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/UpdateRelationTypeTable.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/UpdateRelationTypeTable.cs @@ -1,36 +1,41 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_6_0 -{ +using Umbraco.Cms.Core; - public class UpdateRelationTypeTable : MigrationBase - { - public UpdateRelationTypeTable(IMigrationContext context) - : base(context) - { } +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_6_0; - protected override void Migrate() - { +public class UpdateRelationTypeTable : MigrationBase +{ + public UpdateRelationTypeTable(IMigrationContext context) + : base(context) + { + } - Alter.Table(Cms.Core.Constants.DatabaseSchema.Tables.RelationType).AlterColumn("parentObjectType").AsGuid().Nullable().Do(); - Alter.Table(Cms.Core.Constants.DatabaseSchema.Tables.RelationType).AlterColumn("childObjectType").AsGuid().Nullable().Do(); + protected override void Migrate() + { + Alter.Table(Constants.DatabaseSchema.Tables.RelationType).AlterColumn("parentObjectType").AsGuid().Nullable() + .Do(); + Alter.Table(Constants.DatabaseSchema.Tables.RelationType).AlterColumn("childObjectType").AsGuid().Nullable() + .Do(); - //TODO: We have to update this field to ensure it's not null, we can just copy across the name since that is not nullable + //TODO: We have to update this field to ensure it's not null, we can just copy across the name since that is not nullable - //drop index before we can alter the column - if (IndexExists("IX_umbracoRelationType_alias")) - Delete - .Index("IX_umbracoRelationType_alias") - .OnTable(Cms.Core.Constants.DatabaseSchema.Tables.RelationType) - .Do(); - //change the column to non nullable - Alter.Table(Cms.Core.Constants.DatabaseSchema.Tables.RelationType).AlterColumn("alias").AsString(100).NotNullable().Do(); - //re-create the index - Create + //drop index before we can alter the column + if (IndexExists("IX_umbracoRelationType_alias")) + { + Delete .Index("IX_umbracoRelationType_alias") - .OnTable(Cms.Core.Constants.DatabaseSchema.Tables.RelationType) - .OnColumn("alias") - .Ascending() - .WithOptions().Unique().WithOptions().NonClustered() + .OnTable(Constants.DatabaseSchema.Tables.RelationType) .Do(); } + + //change the column to non nullable + Alter.Table(Constants.DatabaseSchema.Tables.RelationType).AlterColumn("alias").AsString(100).NotNullable().Do(); + //re-create the index + Create + .Index("IX_umbracoRelationType_alias") + .OnTable(Constants.DatabaseSchema.Tables.RelationType) + .OnColumn("alias") + .Ascending() + .WithOptions().Unique().WithOptions().NonClustered() + .Do(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_7_0/MissingDictionaryIndex.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_7_0/MissingDictionaryIndex.cs index 69e4a7423c48..e36e3f3706a0 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_7_0/MissingDictionaryIndex.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_7_0/MissingDictionaryIndex.cs @@ -1,33 +1,31 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_7_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_7_0; + +public class MissingDictionaryIndex : MigrationBase { - public class MissingDictionaryIndex : MigrationBase + public MissingDictionaryIndex(IMigrationContext context) + : base(context) { - public MissingDictionaryIndex(IMigrationContext context) - : base(context) - { + } - } + /// + /// Adds an index to the foreign key column parent on DictionaryDto's table + /// if it doesn't already exist + /// + protected override void Migrate() + { + var indexName = "IX_" + DictionaryDto.TableName + "_Parent"; - /// - /// Adds an index to the foreign key column parent on DictionaryDto's table - /// if it doesn't already exist - /// - protected override void Migrate() + if (!IndexExists(indexName)) { - var indexName = "IX_" + DictionaryDto.TableName + "_Parent"; - - if (!IndexExists(indexName)) - { - Create - .Index(indexName) - .OnTable(DictionaryDto.TableName) - .OnColumn("parent") - .Ascending() - .WithOptions().NonClustered() - .Do(); - } + Create + .Index(indexName) + .OnTable(DictionaryDto.TableName) + .OnColumn("parent") + .Ascending() + .WithOptions().NonClustered() + .Do(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_9_0/ExternalLoginTableUserData.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_9_0/ExternalLoginTableUserData.cs index 7f75bde572c6..70c0d65dd0b3 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_9_0/ExternalLoginTableUserData.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_9_0/ExternalLoginTableUserData.cs @@ -1,20 +1,18 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_9_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_9_0; + +public class ExternalLoginTableUserData : MigrationBase { - public class ExternalLoginTableUserData : MigrationBase + public ExternalLoginTableUserData(IMigrationContext context) + : base(context) { - public ExternalLoginTableUserData(IMigrationContext context) - : base(context) - { - } - - /// - /// Adds new column to the External Login table - /// - protected override void Migrate() - { - AddColumn(Cms.Core.Constants.DatabaseSchema.Tables.ExternalLogin, "userData"); - } } + + /// + /// Adds new column to the External Login table + /// + protected override void Migrate() => + AddColumn(Constants.DatabaseSchema.Tables.ExternalLogin, "userData"); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/AddPasswordConfigToMemberTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/AddPasswordConfigToMemberTable.cs index 01ea1cf3b328..28985bb0dbb1 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/AddPasswordConfigToMemberTable.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/AddPasswordConfigToMemberTable.cs @@ -1,23 +1,21 @@ -using System.Linq; -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; + +public class AddPasswordConfigToMemberTable : MigrationBase { - public class AddPasswordConfigToMemberTable : MigrationBase + public AddPasswordConfigToMemberTable(IMigrationContext context) + : base(context) { - public AddPasswordConfigToMemberTable(IMigrationContext context) - : base(context) - { - } + } - /// - /// Adds new columns to members table - /// - protected override void Migrate() - { - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); + /// + /// Adds new columns to members table + /// + protected override void Migrate() + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); - AddColumnIfNotExists(columns, "passwordConfig"); - } + AddColumnIfNotExists(columns, "passwordConfig"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/DictionaryTablesIndexes.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/DictionaryTablesIndexes.cs index 44034c5e4547..78c27434f01a 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/DictionaryTablesIndexes.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/DictionaryTablesIndexes.cs @@ -1,129 +1,133 @@ -using System.Linq; using Microsoft.Extensions.Logging; +using NPoco; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Execute.Expressions; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; + +public class DictionaryTablesIndexes : MigrationBase { - public class DictionaryTablesIndexes : MigrationBase + private const string IndexedDictionaryColumn = "key"; + private const string IndexedLanguageTextColumn = "languageId"; + + public DictionaryTablesIndexes(IMigrationContext context) + : base(context) { - private const string IndexedDictionaryColumn = "key"; - private const string IndexedLanguageTextColumn = "languageId"; + } - public DictionaryTablesIndexes(IMigrationContext context) - : base(context) - { - } + protected override void Migrate() + { + var indexDictionaryDto = $"IX_{DictionaryDto.TableName}_{IndexedDictionaryColumn}"; + var indexLanguageTextDto = $"IX_{LanguageTextDto.TableName}_{IndexedLanguageTextColumn}"; + var dictionaryColumnsToBeIndexed = new[] {IndexedDictionaryColumn}; + var langTextColumnsToBeIndexed = new[] {IndexedLanguageTextColumn, "UniqueId"}; - protected override void Migrate() + var dictionaryTableHasDuplicates = ContainsDuplicates(dictionaryColumnsToBeIndexed); + var langTextTableHasDuplicates = ContainsDuplicates(langTextColumnsToBeIndexed); + + // Check if there are any duplicates before we delete and re-create the indexes since + // if there are duplicates we won't be able to create the new unique indexes + if (!dictionaryTableHasDuplicates) { - var indexDictionaryDto = $"IX_{DictionaryDto.TableName}_{IndexedDictionaryColumn}"; - var indexLanguageTextDto = $"IX_{LanguageTextDto.TableName}_{IndexedLanguageTextColumn}"; - var dictionaryColumnsToBeIndexed = new[] { IndexedDictionaryColumn }; - var langTextColumnsToBeIndexed = new[] { IndexedLanguageTextColumn, "UniqueId" }; - - var dictionaryTableHasDuplicates = ContainsDuplicates(dictionaryColumnsToBeIndexed); - var langTextTableHasDuplicates = ContainsDuplicates(langTextColumnsToBeIndexed); - - // Check if there are any duplicates before we delete and re-create the indexes since - // if there are duplicates we won't be able to create the new unique indexes - if (!dictionaryTableHasDuplicates) - { - // Delete existing - DeleteIndex(indexDictionaryDto); - } - - if (!langTextTableHasDuplicates) - { - // Delete existing - DeleteIndex(indexLanguageTextDto); - } - - // Try to re-create/add - TryAddUniqueConstraint(dictionaryColumnsToBeIndexed, indexDictionaryDto, dictionaryTableHasDuplicates); - TryAddUniqueConstraint(langTextColumnsToBeIndexed, indexLanguageTextDto, langTextTableHasDuplicates); + // Delete existing + DeleteIndex(indexDictionaryDto); } - private void DeleteIndex(string indexName) + if (!langTextTableHasDuplicates) { - var tableDef = DefinitionFactory.GetTableDefinition(typeof(TDto), Context.SqlContext.SqlSyntax); - - if (IndexExists(indexName)) - { - Delete.Index(indexName).OnTable(tableDef.Name).Do(); - } + // Delete existing + DeleteIndex(indexLanguageTextDto); } - private void CreateIndex(string indexName) - { - var tableDef = DefinitionFactory.GetTableDefinition(typeof(TDto), Context.SqlContext.SqlSyntax); + // Try to re-create/add + TryAddUniqueConstraint(dictionaryColumnsToBeIndexed, indexDictionaryDto, + dictionaryTableHasDuplicates); + TryAddUniqueConstraint(langTextColumnsToBeIndexed, indexLanguageTextDto, + langTextTableHasDuplicates); + } - // get the definition by name - var index = tableDef.Indexes.First(x => x.Name == indexName); - new ExecuteSqlStatementExpression(Context) { SqlStatement = Context.SqlContext.SqlSyntax.Format(index) }.Execute(); - } + private void DeleteIndex(string indexName) + { + TableDefinition tableDef = DefinitionFactory.GetTableDefinition(typeof(TDto), Context.SqlContext.SqlSyntax); - private void TryAddUniqueConstraint(string[] columns, string index, bool containsDuplicates) + if (IndexExists(indexName)) { - var tableDef = DefinitionFactory.GetTableDefinition(typeof(TDto), Context.SqlContext.SqlSyntax); - - // Check the existing data to ensure the constraint can be successfully applied. - // This seems to be better than relying on catching an exception as this leads to - // transaction errors: "This SqlTransaction has completed; it is no longer usable". - var columnsDescription = string.Join("], [", columns); - if (containsDuplicates) - { - var message = $"Could not create unique constraint on [{tableDef.Name}] due to existing " + - $"duplicate records across the column{(columns.Length > 1 ? "s" : string.Empty)}: [{columnsDescription}]."; - - LogIncompleteMigrationStep(message); - return; - } - - CreateIndex(index); + Delete.Index(indexName).OnTable(tableDef.Name).Do(); } + } - private bool ContainsDuplicates(string[] columns) - { - // Check for duplicates by comparing the total count of all records with the count of records distinct by the - // provided column. If the former is greater than the latter, there's at least one duplicate record. - int recordCount = GetRecordCount(); - int distinctRecordCount = GetDistinctRecordCount(columns); + private void CreateIndex(string indexName) + { + TableDefinition tableDef = DefinitionFactory.GetTableDefinition(typeof(TDto), Context.SqlContext.SqlSyntax); - return recordCount > distinctRecordCount; - } + // get the definition by name + IndexDefinition index = tableDef.Indexes.First(x => x.Name == indexName); + new ExecuteSqlStatementExpression(Context) {SqlStatement = Context.SqlContext.SqlSyntax.Format(index)} + .Execute(); + } + + private void TryAddUniqueConstraint(string[] columns, string index, bool containsDuplicates) + { + TableDefinition tableDef = DefinitionFactory.GetTableDefinition(typeof(TDto), Context.SqlContext.SqlSyntax); - private int GetRecordCount() + // Check the existing data to ensure the constraint can be successfully applied. + // This seems to be better than relying on catching an exception as this leads to + // transaction errors: "This SqlTransaction has completed; it is no longer usable". + var columnsDescription = string.Join("], [", columns); + if (containsDuplicates) { - var countQuery = Database.SqlContext.Sql() - .SelectCount() - .From(); + var message = $"Could not create unique constraint on [{tableDef.Name}] due to existing " + + $"duplicate records across the column{(columns.Length > 1 ? "s" : string.Empty)}: [{columnsDescription}]."; - return Database.ExecuteScalar(countQuery); + LogIncompleteMigrationStep(message); + return; } - private int GetDistinctRecordCount(string[] columns) - { - string columnSpecification; + CreateIndex(index); + } - columnSpecification = columns.Length == 1 - ? QuoteColumnName(columns[0]) - : $"CONCAT({string.Join(",", columns.Select(QuoteColumnName))})"; + private bool ContainsDuplicates(string[] columns) + { + // Check for duplicates by comparing the total count of all records with the count of records distinct by the + // provided column. If the former is greater than the latter, there's at least one duplicate record. + var recordCount = GetRecordCount(); + var distinctRecordCount = GetDistinctRecordCount(columns); - var distinctCountQuery = Database.SqlContext.Sql() - .Select($"COUNT(DISTINCT({columnSpecification}))") - .From(); + return recordCount > distinctRecordCount; + } - return Database.ExecuteScalar(distinctCountQuery); - } + private int GetRecordCount() + { + Sql countQuery = Database.SqlContext.Sql() + .SelectCount() + .From(); + + return Database.ExecuteScalar(countQuery); + } + + private int GetDistinctRecordCount(string[] columns) + { + string columnSpecification; - private void LogIncompleteMigrationStep(string message) => Logger.LogError($"Database migration step failed: {message}"); + columnSpecification = columns.Length == 1 + ? QuoteColumnName(columns[0]) + : $"CONCAT({string.Join(",", columns.Select(QuoteColumnName))})"; - private string StringConvertedAndQuotedColumnName(string column) => $"CONVERT(nvarchar(1000),{QuoteColumnName(column)})"; + Sql distinctCountQuery = Database.SqlContext.Sql() + .Select($"COUNT(DISTINCT({columnSpecification}))") + .From(); - private string QuoteColumnName(string column) => $"[{column}]"; + return Database.ExecuteScalar(distinctCountQuery); } + + private void LogIncompleteMigrationStep(string message) => + Logger.LogError($"Database migration step failed: {message}"); + + private string StringConvertedAndQuotedColumnName(string column) => + $"CONVERT(nvarchar(1000),{QuoteColumnName(column)})"; + + private string QuoteColumnName(string column) => $"[{column}]"; } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexes.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexes.cs index db7f17eee370..946840d2d084 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexes.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexes.cs @@ -1,80 +1,69 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NPoco; -using Umbraco.Cms.Core; -using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 +public class ExternalLoginTableIndexes : MigrationBase { + public ExternalLoginTableIndexes(IMigrationContext context) + : base(context) + { + } - public class ExternalLoginTableIndexes : MigrationBase + /// + /// Adds new indexes to the External Login table + /// + protected override void Migrate() { - public ExternalLoginTableIndexes(IMigrationContext context) - : base(context) - { - } + // Before adding these indexes we need to remove duplicate data. + // Get all logins by latest + var logins = Database.Fetch() + .OrderByDescending(x => x.CreateDate) + .ToList(); - /// - /// Adds new indexes to the External Login table - /// - protected override void Migrate() + var toDelete = new List(); + // used to track duplicates so they can be removed + var keys = new HashSet<(string, string)>(); + foreach (ExternalLoginTokenTable.LegacyExternalLoginDto login in logins) { - // Before adding these indexes we need to remove duplicate data. - // Get all logins by latest - var logins = Database.Fetch() - .OrderByDescending(x => x.CreateDate) - .ToList(); - - var toDelete = new List(); - // used to track duplicates so they can be removed - var keys = new HashSet<(string, string)>(); - foreach(ExternalLoginTokenTable.LegacyExternalLoginDto login in logins) + if (!keys.Add((login.ProviderKey, login.LoginProvider))) { - if (!keys.Add((login.ProviderKey, login.LoginProvider))) - { - // if it already exists we need to remove this one - toDelete.Add(login.Id); - } + // if it already exists we need to remove this one + toDelete.Add(login.Id); } - if (toDelete.Count > 0) - { - Database.DeleteMany().Where(x => toDelete.Contains(x.Id)).Execute(); - } - - var indexName1 = "IX_" + ExternalLoginTokenTable.LegacyExternalLoginDto.TableName + "_LoginProvider"; + } - if (!IndexExists(indexName1)) - { - Create - .Index(indexName1) - .OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName) - .OnColumn("loginProvider") - .Ascending() - .WithOptions() - .Unique() - .WithOptions() - .NonClustered() - .Do(); - } + if (toDelete.Count > 0) + { + Database.DeleteMany().Where(x => toDelete.Contains(x.Id)) + .Execute(); + } - var indexName2 = "IX_" + ExternalLoginTokenTable.LegacyExternalLoginDto.TableName + "_ProviderKey"; + var indexName1 = "IX_" + ExternalLoginTokenTable.LegacyExternalLoginDto.TableName + "_LoginProvider"; - if (!IndexExists(indexName2)) - { - Create - .Index(indexName2) - .OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName) - .OnColumn("loginProvider").Ascending() - .OnColumn("providerKey").Ascending() - .WithOptions() - .NonClustered() - .Do(); - } + if (!IndexExists(indexName1)) + { + Create + .Index(indexName1) + .OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName) + .OnColumn("loginProvider") + .Ascending() + .WithOptions() + .Unique() + .WithOptions() + .NonClustered() + .Do(); } + var indexName2 = "IX_" + ExternalLoginTokenTable.LegacyExternalLoginDto.TableName + "_ProviderKey"; + if (!IndexExists(indexName2)) + { + Create + .Index(indexName2) + .OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName) + .OnColumn("loginProvider").Ascending() + .OnColumn("providerKey").Ascending() + .WithOptions() + .NonClustered() + .Do(); + } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexesFixup.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexesFixup.cs index 2c77b301cec9..2619e151c244 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexesFixup.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexesFixup.cs @@ -1,59 +1,57 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 +/// +/// Fixes up the original for post RC release to ensure that +/// the correct indexes are applied. +/// +public class ExternalLoginTableIndexesFixup : MigrationBase { - /// - /// Fixes up the original for post RC release to ensure that - /// the correct indexes are applied. - /// - public class ExternalLoginTableIndexesFixup : MigrationBase + public ExternalLoginTableIndexesFixup(IMigrationContext context) : base(context) { - public ExternalLoginTableIndexesFixup(IMigrationContext context) : base(context) + } + + protected override void Migrate() + { + var indexName1 = "IX_" + ExternalLoginTokenTable.LegacyExternalLoginDto.TableName + "_LoginProvider"; + var indexName2 = "IX_" + ExternalLoginTokenTable.LegacyExternalLoginDto.TableName + "_ProviderKey"; + + if (IndexExists(indexName1)) { + // drop it since the previous migration index was wrong, and we + // need to modify a column that belons to it + Delete.Index(indexName1).OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName).Do(); } - protected override void Migrate() + if (IndexExists(indexName2)) { - var indexName1 = "IX_" + ExternalLoginTokenTable.LegacyExternalLoginDto.TableName + "_LoginProvider"; - var indexName2 = "IX_" + ExternalLoginTokenTable.LegacyExternalLoginDto.TableName + "_ProviderKey"; - - if (IndexExists(indexName1)) - { - // drop it since the previous migration index was wrong, and we - // need to modify a column that belons to it - Delete.Index(indexName1).OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName).Do(); - } - - if (IndexExists(indexName2)) - { - // drop since it's using a column we're about to modify - Delete.Index(indexName2).OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName).Do(); - } + // drop since it's using a column we're about to modify + Delete.Index(indexName2).OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName).Do(); + } - // then fixup the length of the loginProvider column - AlterColumn(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName, "loginProvider"); + // then fixup the length of the loginProvider column + AlterColumn( + ExternalLoginTokenTable.LegacyExternalLoginDto.TableName, "loginProvider"); - // create it with the correct definition - Create - .Index(indexName1) - .OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName) - .OnColumn("loginProvider").Ascending() - .OnColumn("userId").Ascending() - .WithOptions() - .Unique() - .WithOptions() - .NonClustered() - .Do(); + // create it with the correct definition + Create + .Index(indexName1) + .OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName) + .OnColumn("loginProvider").Ascending() + .OnColumn("userId").Ascending() + .WithOptions() + .Unique() + .WithOptions() + .NonClustered() + .Do(); - // re-create the original - Create - .Index(indexName2) - .OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName) - .OnColumn("loginProvider").Ascending() - .OnColumn("providerKey").Ascending() - .WithOptions() - .NonClustered() - .Do(); - } + // re-create the original + Create + .Index(indexName2) + .OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName) + .OnColumn("loginProvider").Ascending() + .OnColumn("providerKey").Ascending() + .WithOptions() + .NonClustered() + .Do(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTokenTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTokenTable.cs index ee089ad89c65..f682fa267624 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTokenTable.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTokenTable.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; @@ -7,75 +5,74 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; + +public class ExternalLoginTokenTable : MigrationBase { - public class ExternalLoginTokenTable : MigrationBase + public ExternalLoginTokenTable(IMigrationContext context) + : base(context) { - public ExternalLoginTokenTable(IMigrationContext context) - : base(context) - { - } + } - /// - /// Adds new External Login token table - /// - protected override void Migrate() + /// + /// Adds new External Login token table + /// + protected override void Migrate() + { + IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database); + if (tables.InvariantContains(ExternalLoginTokenDto.TableName)) { - IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database); - if (tables.InvariantContains(ExternalLoginTokenDto.TableName)) - { - return; - } - - Create.Table().Do(); + return; } - [TableName(TableName)] - [ExplicitColumns] - [PrimaryKey("Id")] - internal class LegacyExternalLoginDto - { - public const string TableName = Constants.DatabaseSchema.Tables.ExternalLogin; + Create.Table().Do(); + } - [Column("id")] [PrimaryKeyColumn] public int Id { get; set; } + [TableName(TableName)] + [ExplicitColumns] + [PrimaryKey("Id")] + internal class LegacyExternalLoginDto + { + public const string TableName = Constants.DatabaseSchema.Tables.ExternalLogin; - [Obsolete( - "This only exists to ensure you can upgrade using external logins from umbraco version where this was used to the new where it is not used")] - [Column("userId")] - public int? UserId { get; set; } + [Column("id")] [PrimaryKeyColumn] public int Id { get; set; } + [Obsolete( + "This only exists to ensure you can upgrade using external logins from umbraco version where this was used to the new where it is not used")] + [Column("userId")] + public int? UserId { get; set; } - /// - /// Used to store the name of the provider (i.e. Facebook, Google) - /// - [Column("loginProvider")] - [Length(400)] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.UniqueNonClustered, ForColumns = "loginProvider,userOrMemberKey", - Name = "IX_" + TableName + "_LoginProvider")] - public string LoginProvider { get; set; } = null!; - /// - /// Stores the key the provider uses to lookup the login - /// - [Column("providerKey")] - [Length(4000)] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.NonClustered, ForColumns = "loginProvider,providerKey", - Name = "IX_" + TableName + "_ProviderKey")] - public string ProviderKey { get; set; } = null!; + /// + /// Used to store the name of the provider (i.e. Facebook, Google) + /// + [Column("loginProvider")] + [Length(400)] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "loginProvider,userOrMemberKey", + Name = "IX_" + TableName + "_LoginProvider")] + public string LoginProvider { get; set; } = null!; - [Column("createDate")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime CreateDate { get; set; } + /// + /// Stores the key the provider uses to lookup the login + /// + [Column("providerKey")] + [Length(4000)] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.NonClustered, ForColumns = "loginProvider,providerKey", + Name = "IX_" + TableName + "_ProviderKey")] + public string ProviderKey { get; set; } = null!; - /// - /// Used to store any arbitrary data for the user and external provider - like user tokens returned from the provider - /// - [Column("userData")] - [NullSetting(NullSetting = NullSettings.Null)] - [SpecialDbType(SpecialDbTypes.NTEXT)] - public string? UserData { get; set; } - } + [Column("createDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime CreateDate { get; set; } + + /// + /// Used to store any arbitrary data for the user and external provider - like user tokens returned from the provider + /// + [Column("userData")] + [NullSetting(NullSetting = NullSettings.Null)] + [SpecialDbType(SpecialDbTypes.NTEXT)] + public string? UserData { get; set; } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/MemberTableColumns.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/MemberTableColumns.cs index 5dd274ad05ef..50204e443279 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/MemberTableColumns.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/MemberTableColumns.cs @@ -1,24 +1,22 @@ -using System.Linq; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; + +public class MemberTableColumns : MigrationBase { - public class MemberTableColumns : MigrationBase + public MemberTableColumns(IMigrationContext context) + : base(context) { - public MemberTableColumns(IMigrationContext context) - : base(context) - { - } + } - /// - /// Adds new columns to members table - /// - protected override void Migrate() - { - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); + /// + /// Adds new columns to members table + /// + protected override void Migrate() + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); - AddColumnIfNotExists(columns, "securityStampToken"); - AddColumnIfNotExists(columns, "emailConfirmedDate"); - } + AddColumnIfNotExists(columns, "securityStampToken"); + AddColumnIfNotExists(columns, "emailConfirmedDate"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/MigrateLogViewerQueriesFromFileToDb.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/MigrateLogViewerQueriesFromFileToDb.cs index 365c50b3f86e..9a2a3984e7be 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/MigrateLogViewerQueriesFromFileToDb.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/MigrateLogViewerQueriesFromFileToDb.cs @@ -1,105 +1,106 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Newtonsoft.Json; +using Newtonsoft.Json; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Infrastructure.Migrations.PostMigrations; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 -{ +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; - public class MigrateLogViewerQueriesFromFileToDb : MigrationBase +public class MigrateLogViewerQueriesFromFileToDb : MigrationBase +{ + internal static readonly IEnumerable DefaultLogQueries = new LogViewerQueryDto[] { - private readonly IHostingEnvironment _hostingEnvironment; - internal static readonly IEnumerable DefaultLogQueries = new LogViewerQueryDto[] + new() { - new (){ - Name = "Find all logs where the Level is NOT Verbose and NOT Debug", - Query = "Not(@Level='Verbose') and Not(@Level='Debug')" - }, - new (){ - Name = "Find all logs that has an exception property (Warning, Error & Fatal with Exceptions)", - Query = "Has(@Exception)" - }, - new (){ - Name = "Find all logs that have the property 'Duration'", - Query = "Has(Duration)" - }, - new (){ - Name = "Find all logs that have the property 'Duration' and the duration is greater than 1000ms", - Query = "Has(Duration) and Duration > 1000" - }, - new (){ - Name = "Find all logs that are from the namespace 'Umbraco.Core'", - Query = "StartsWith(SourceContext, 'Umbraco.Core')" - }, - new (){ - Name = "Find all logs that use a specific log message template", - Query = "@MessageTemplate = '[Timing {TimingId}] {EndMessage} ({TimingDuration}ms)'" - }, - new (){ - Name = "Find logs where one of the items in the SortedComponentTypes property array is equal to", - Query = "SortedComponentTypes[?] = 'Umbraco.Web.Search.ExamineComponent'" - }, - new (){ - Name = "Find logs where one of the items in the SortedComponentTypes property array contains", - Query = "Contains(SortedComponentTypes[?], 'DatabaseServer')" - }, - new (){ - Name = "Find all logs that the message has localhost in it with SQL like", - Query = "@Message like '%localhost%'" - }, - new (){ - Name = "Find all logs that the message that starts with 'end' in it with SQL like", - Query = "@Message like 'end%'" - } - }; - - public MigrateLogViewerQueriesFromFileToDb(IMigrationContext context, IHostingEnvironment hostingEnvironment) - : base(context) + Name = "Find all logs where the Level is NOT Verbose and NOT Debug", + Query = "Not(@Level='Verbose') and Not(@Level='Debug')" + }, + new() { - _hostingEnvironment = hostingEnvironment; - } - - protected override void Migrate() + Name = "Find all logs that has an exception property (Warning, Error & Fatal with Exceptions)", + Query = "Has(@Exception)" + }, + new() {Name = "Find all logs that have the property 'Duration'", Query = "Has(Duration)"}, + new() { - CreateDatabaseTable(); - MigrateFileContentToDB(); - } - private void CreateDatabaseTable() + Name = "Find all logs that have the property 'Duration' and the duration is greater than 1000ms", + Query = "Has(Duration) and Duration > 1000" + }, + new() { - var tables = SqlSyntax.GetTablesInSchema(Context.Database); - if (!tables.InvariantContains(Core.Constants.DatabaseSchema.Tables.LogViewerQuery)) - { - Create.Table().Do(); - } + Name = "Find all logs that are from the namespace 'Umbraco.Core'", + Query = "StartsWith(SourceContext, 'Umbraco.Core')" + }, + new() + { + Name = "Find all logs that use a specific log message template", + Query = "@MessageTemplate = '[Timing {TimingId}] {EndMessage} ({TimingDuration}ms)'" + }, + new() + { + Name = "Find logs where one of the items in the SortedComponentTypes property array is equal to", + Query = "SortedComponentTypes[?] = 'Umbraco.Web.Search.ExamineComponent'" + }, + new() + { + Name = "Find logs where one of the items in the SortedComponentTypes property array contains", + Query = "Contains(SortedComponentTypes[?], 'DatabaseServer')" + }, + new() + { + Name = "Find all logs that the message has localhost in it with SQL like", + Query = "@Message like '%localhost%'" + }, + new() + { + Name = "Find all logs that the message that starts with 'end' in it with SQL like", + Query = "@Message like 'end%'" } + }; + + private readonly IHostingEnvironment _hostingEnvironment; - internal static string GetLogViewerQueryFile(IHostingEnvironment hostingEnvironment) + public MigrateLogViewerQueriesFromFileToDb(IMigrationContext context, IHostingEnvironment hostingEnvironment) + : base(context) => + _hostingEnvironment = hostingEnvironment; + + protected override void Migrate() + { + CreateDatabaseTable(); + MigrateFileContentToDB(); + } + + private void CreateDatabaseTable() + { + IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database); + if (!tables.InvariantContains(Constants.DatabaseSchema.Tables.LogViewerQuery)) { - return hostingEnvironment.MapPathContentRoot( - Path.Combine(Cms.Core.Constants.SystemDirectories.Config, "logviewer.searches.config.js")); + Create.Table().Do(); } - private void MigrateFileContentToDB() - { - var logViewerQueryFile = GetLogViewerQueryFile(_hostingEnvironment); + } - var logQueriesInFile = File.Exists(logViewerQueryFile) ? - JsonConvert.DeserializeObject(File.ReadAllText(logViewerQueryFile)) - : DefaultLogQueries; + internal static string GetLogViewerQueryFile(IHostingEnvironment hostingEnvironment) => + hostingEnvironment.MapPathContentRoot( + Path.Combine(Constants.SystemDirectories.Config, "logviewer.searches.config.js")); - var logQueriesInDb = Database.Query().ToArray(); + private void MigrateFileContentToDB() + { + var logViewerQueryFile = GetLogViewerQueryFile(_hostingEnvironment); - if (logQueriesInDb.Any()) - { - return; - } + IEnumerable? logQueriesInFile = File.Exists(logViewerQueryFile) + ? JsonConvert.DeserializeObject(File.ReadAllText(logViewerQueryFile)) + : DefaultLogQueries; - Database.InsertBulk(logQueriesInFile!); + LogViewerQueryDto[]? logQueriesInDb = Database.Query().ToArray(); - Context.AddPostMigration(); + if (logQueriesInDb.Any()) + { + return; } + + Database.InsertBulk(logQueriesInFile!); + + Context.AddPostMigration(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/UmbracoServerColumn.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/UmbracoServerColumn.cs index 601f2bd96636..da9128957009 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/UmbracoServerColumn.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/UmbracoServerColumn.cs @@ -1,20 +1,18 @@ +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; + +public class UmbracoServerColumn : MigrationBase { - public class UmbracoServerColumn : MigrationBase + public UmbracoServerColumn(IMigrationContext context) + : base(context) { - public UmbracoServerColumn(IMigrationContext context) - : base(context) - { - } - - /// - /// Adds new columns to members table - /// - protected override void Migrate() - { - ReplaceColumn(Cms.Core.Constants.DatabaseSchema.Tables.Server, "isMaster", "isSchedulingPublisher"); - } } + + /// + /// Adds new columns to members table + /// + protected override void Migrate() => ReplaceColumn(Constants.DatabaseSchema.Tables.Server, + "isMaster", "isSchedulingPublisher"); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_1_0/AddContentVersionCleanupFeature.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_1_0/AddContentVersionCleanupFeature.cs index aa0d4472e813..7a885eca07bb 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_1_0/AddContentVersionCleanupFeature.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_1_0/AddContentVersionCleanupFeature.cs @@ -1,28 +1,29 @@ -using Umbraco.Cms.Infrastructure.Migrations; using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_1_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_1_0; + +internal class AddContentVersionCleanupFeature : MigrationBase { - class AddContentVersionCleanupFeature : MigrationBase + public AddContentVersionCleanupFeature(IMigrationContext context) + : base(context) { - public AddContentVersionCleanupFeature(IMigrationContext context) - : base(context) { } + } - /// - /// The conditionals are useful to enable the same migration to be used in multiple - /// migration paths x.x -> 8.18 and x.x -> 9.x - /// - protected override void Migrate() + /// + /// The conditionals are useful to enable the same migration to be used in multiple + /// migration paths x.x -> 8.18 and x.x -> 9.x + /// + protected override void Migrate() + { + IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database); + if (!tables.InvariantContains(ContentVersionCleanupPolicyDto.TableName)) { - var tables = SqlSyntax.GetTablesInSchema(Context.Database); - if (!tables.InvariantContains(ContentVersionCleanupPolicyDto.TableName)) - { - Create.Table().Do(); - } - - var columns = SqlSyntax.GetColumnsInSchema(Context.Database); - AddColumnIfNotExists(columns, "preventCleanup"); + Create.Table().Do(); } + + IEnumerable columns = SqlSyntax.GetColumnsInSchema(Context.Database); + AddColumnIfNotExists(columns, "preventCleanup"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_2_0/AddDefaultForNotificationsToggle.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_2_0/AddDefaultForNotificationsToggle.cs index 3bc62ab42efd..92e66fd190d2 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_2_0/AddDefaultForNotificationsToggle.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_2_0/AddDefaultForNotificationsToggle.cs @@ -1,17 +1,21 @@ -using Umbraco.Cms.Core; +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_2_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_2_0; + +public class AddDefaultForNotificationsToggle : MigrationBase { - public class AddDefaultForNotificationsToggle : MigrationBase + public AddDefaultForNotificationsToggle(IMigrationContext context) + : base(context) { - public AddDefaultForNotificationsToggle(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - var updateSQL = Sql($"UPDATE {Constants.DatabaseSchema.Tables.UserGroup} SET userGroupDefaultPermissions = userGroupDefaultPermissions + 'N' WHERE userGroupAlias IN ('admin', 'writer', 'editor')"); - Execute.Sql(updateSQL.SQL).Do(); - } + protected override void Migrate() + { + Sql updateSQL = + Sql( + $"UPDATE {Constants.DatabaseSchema.Tables.UserGroup} SET userGroupDefaultPermissions = userGroupDefaultPermissions + 'N' WHERE userGroupAlias IN ('admin', 'writer', 'editor')"); + Execute.Sql(updateSQL.SQL).Do(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_2_0/AddUserGroup2NodeTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_2_0/AddUserGroup2NodeTable.cs index 1bb7b71c893a..72d93a3a27ef 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_2_0/AddUserGroup2NodeTable.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_2_0/AddUserGroup2NodeTable.cs @@ -1,30 +1,31 @@ -using System.Linq; using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_2_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_2_0; + +internal class AddUserGroup2NodeTable : MigrationBase { - class AddUserGroup2NodeTable : MigrationBase + public AddUserGroup2NodeTable(IMigrationContext context) + : base(context) { - public AddUserGroup2NodeTable(IMigrationContext context) - : base(context) { } + } - protected override void Migrate() + protected override void Migrate() + { + IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database); + if (!tables.InvariantContains(UserGroup2NodeDto.TableName)) { - var tables = SqlSyntax.GetTablesInSchema(Context.Database); - if (!tables.InvariantContains(UserGroup2NodeDto.TableName)) - { - Create.Table().Do(); - } + Create.Table().Do(); + } - // Insert if there exists specific permissions today. Can't do it directly in db in any nice way. - var allData = Database.Fetch(); - var toInsert = allData.Select(x => new UserGroup2NodeDto() { NodeId = x.NodeId, UserGroupId = x.UserGroupId }).Distinct( + // Insert if there exists specific permissions today. Can't do it directly in db in any nice way. + List? allData = Database.Fetch(); + UserGroup2NodeDto[] toInsert = allData + .Select(x => new UserGroup2NodeDto {NodeId = x.NodeId, UserGroupId = x.UserGroupId}).Distinct( new DelegateEqualityComparer( - (x, y) => x?.NodeId == y?.NodeId && x?.UserGroupId == y?.UserGroupId, - x => x.NodeId.GetHashCode() + x.UserGroupId.GetHashCode())).ToArray(); - Database.InsertBulk(toInsert); - } + (x, y) => x?.NodeId == y?.NodeId && x?.UserGroupId == y?.UserGroupId, + x => x.NodeId.GetHashCode() + x.UserGroupId.GetHashCode())).ToArray(); + Database.InsertBulk(toInsert); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/AddTwoFactorLoginTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/AddTwoFactorLoginTable.cs index c5e569282a67..0950b3bb6454 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/AddTwoFactorLoginTable.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/AddTwoFactorLoginTable.cs @@ -1,24 +1,22 @@ -using System.Collections.Generic; -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_3_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_3_0; + +public class AddTwoFactorLoginTable : MigrationBase { - public class AddTwoFactorLoginTable : MigrationBase + public AddTwoFactorLoginTable(IMigrationContext context) : base(context) { - public AddTwoFactorLoginTable(IMigrationContext context) : base(context) - { - } + } - protected override void Migrate() + protected override void Migrate() + { + IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database); + if (tables.InvariantContains(TwoFactorLoginDto.TableName)) { - IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database); - if (tables.InvariantContains(TwoFactorLoginDto.TableName)) - { - return; - } - - Create.Table().Do(); + return; } + + Create.Table().Do(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/MovePackageXMLToDb.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/MovePackageXMLToDb.cs index 3173e739a9bc..ea41c3be911c 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/MovePackageXMLToDb.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/MovePackageXMLToDb.cs @@ -1,72 +1,64 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Xml; -using System.Xml.Linq; using Umbraco.Cms.Core.Packaging; -using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_3_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_3_0; + +public class MovePackageXMLToDb : MigrationBase { - public class MovePackageXMLToDb : MigrationBase + private readonly PackagesRepository _packagesRepository; + private readonly PackageDefinitionXmlParser _xmlParser; + + /// + /// Initializes a new instance of the class. + /// + public MovePackageXMLToDb(IMigrationContext context, PackagesRepository packagesRepository) + : base(context) { - private readonly PackagesRepository _packagesRepository; - private readonly PackageDefinitionXmlParser _xmlParser; + _packagesRepository = packagesRepository; + _xmlParser = new PackageDefinitionXmlParser(); + } - /// - /// Initializes a new instance of the class. - /// - public MovePackageXMLToDb(IMigrationContext context, PackagesRepository packagesRepository) - : base(context) + private void CreateDatabaseTable() + { + // Add CreatedPackage table in database if it doesn't exist + IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database); + if (!tables.InvariantContains(CreatedPackageSchemaDto.TableName)) { - _packagesRepository = packagesRepository; - _xmlParser = new PackageDefinitionXmlParser(); + Create.Table().Do(); } + } - private void CreateDatabaseTable() + private void MigrateCreatedPackageFilesToDb() + { + // Load data from file + IEnumerable packages = _packagesRepository.GetAll().WhereNotNull(); + var createdPackageDtos = new List(); + foreach (PackageDefinition package in packages) { - // Add CreatedPackage table in database if it doesn't exist - IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database); - if (!tables.InvariantContains(CreatedPackageSchemaDto.TableName)) + // Create dto from xmlDocument + var dto = new CreatedPackageSchemaDto { - Create.Table().Do(); - } + Name = package.Name, + Value = _xmlParser.ToXml(package).ToString(), + UpdateDate = DateTime.Now, + PackageId = Guid.NewGuid() + }; + createdPackageDtos.Add(dto); } - private void MigrateCreatedPackageFilesToDb() + _packagesRepository.DeleteLocalRepositoryFiles(); + if (createdPackageDtos.Any()) { - // Load data from file - IEnumerable packages = _packagesRepository.GetAll().WhereNotNull(); - var createdPackageDtos = new List(); - foreach (PackageDefinition package in packages) - { - // Create dto from xmlDocument - var dto = new CreatedPackageSchemaDto() - { - Name = package.Name, - Value = _xmlParser.ToXml(package).ToString(), - UpdateDate = DateTime.Now, - PackageId = Guid.NewGuid() - }; - createdPackageDtos.Add(dto); - } - - _packagesRepository.DeleteLocalRepositoryFiles(); - if (createdPackageDtos.Any()) - { - // Insert dto into CreatedPackage table - Database.InsertBulk(createdPackageDtos); - } + // Insert dto into CreatedPackage table + Database.InsertBulk(createdPackageDtos); } + } - /// - protected override void Migrate() - { - CreateDatabaseTable(); - MigrateCreatedPackageFilesToDb(); - } + /// + protected override void Migrate() + { + CreateDatabaseTable(); + MigrateCreatedPackageFilesToDb(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/UpdateExternalLoginToUseKeyInsteadOfId.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/UpdateExternalLoginToUseKeyInsteadOfId.cs index 6b74c49f67bf..c16ee37dc2c6 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/UpdateExternalLoginToUseKeyInsteadOfId.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/UpdateExternalLoginToUseKeyInsteadOfId.cs @@ -1,63 +1,62 @@ -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_3_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_3_0; + +public class UpdateExternalLoginToUseKeyInsteadOfId : MigrationBase { - public class UpdateExternalLoginToUseKeyInsteadOfId : MigrationBase + public UpdateExternalLoginToUseKeyInsteadOfId(IMigrationContext context) : base(context) { - public UpdateExternalLoginToUseKeyInsteadOfId(IMigrationContext context) : base(context) - { - } + } - protected override void Migrate() + protected override void Migrate() + { + if (!ColumnExists(ExternalLoginDto.TableName, "userOrMemberKey")) { - if (!ColumnExists(ExternalLoginDto.TableName, "userOrMemberKey")) + var indexNameToRecreate = "IX_" + ExternalLoginDto.TableName + "_LoginProvider"; + var indexNameToDelete = "IX_" + ExternalLoginDto.TableName + "_userId"; + + if (IndexExists(indexNameToRecreate)) { - var indexNameToRecreate = "IX_" + ExternalLoginDto.TableName + "_LoginProvider"; - var indexNameToDelete = "IX_" + ExternalLoginDto.TableName + "_userId"; - - if (IndexExists(indexNameToRecreate)) - { - // drop it since the previous migration index was wrong, and we - // need to modify a column that belons to it - Delete.Index(indexNameToRecreate).OnTable(ExternalLoginDto.TableName).Do(); - } - - if (IndexExists(indexNameToDelete)) - { - // drop it since the previous migration index was wrong, and we - // need to modify a column that belons to it - Delete.Index(indexNameToDelete).OnTable(ExternalLoginDto.TableName).Do(); - } - - //special trick to add the column without constraints and return the sql to add them later - AddColumn("userOrMemberKey", out var sqls); - - //populate the new columns with the userId as a Guid. Same method as IntExtensions.ToGuid. - Execute.Sql($"UPDATE {ExternalLoginDto.TableName} SET userOrMemberKey = CAST(CONVERT(char(8), CONVERT(BINARY(4), userId), 2) + '-0000-0000-0000-000000000000' AS UNIQUEIDENTIFIER)").Do(); - - //now apply constraints (NOT NULL) to new table - foreach (var sql in sqls) Execute.Sql(sql).Do(); - - //now remove these old columns - Delete.Column("userId").FromTable(ExternalLoginDto.TableName).Do(); - - // create index with the correct definition - Create - .Index(indexNameToRecreate) - .OnTable(ExternalLoginDto.TableName) - .OnColumn("loginProvider").Ascending() - .OnColumn("userOrMemberKey").Ascending() - .WithOptions() - .Unique() - .WithOptions() - .NonClustered() - .Do(); + // drop it since the previous migration index was wrong, and we + // need to modify a column that belons to it + Delete.Index(indexNameToRecreate).OnTable(ExternalLoginDto.TableName).Do(); } - } + if (IndexExists(indexNameToDelete)) + { + // drop it since the previous migration index was wrong, and we + // need to modify a column that belons to it + Delete.Index(indexNameToDelete).OnTable(ExternalLoginDto.TableName).Do(); + } + + //special trick to add the column without constraints and return the sql to add them later + AddColumn("userOrMemberKey", out IEnumerable sqls); + //populate the new columns with the userId as a Guid. Same method as IntExtensions.ToGuid. + Execute.Sql( + $"UPDATE {ExternalLoginDto.TableName} SET userOrMemberKey = CAST(CONVERT(char(8), CONVERT(BINARY(4), userId), 2) + '-0000-0000-0000-000000000000' AS UNIQUEIDENTIFIER)") + .Do(); + + //now apply constraints (NOT NULL) to new table + foreach (var sql in sqls) + { + Execute.Sql(sql).Do(); + } + + //now remove these old columns + Delete.Column("userId").FromTable(ExternalLoginDto.TableName).Do(); + + // create index with the correct definition + Create + .Index(indexNameToRecreate) + .OnTable(ExternalLoginDto.TableName) + .OnColumn("loginProvider").Ascending() + .OnColumn("userOrMemberKey").Ascending() + .WithOptions() + .Unique() + .WithOptions() + .NonClustered() + .Do(); + } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/AddScheduledPublishingLock.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/AddScheduledPublishingLock.cs index 01cfb22a3d14..0d3a683f14a9 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/AddScheduledPublishingLock.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/AddScheduledPublishingLock.cs @@ -1,15 +1,16 @@ +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_4_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_4_0; + +internal class AddScheduledPublishingLock : MigrationBase { - internal class AddScheduledPublishingLock : MigrationBase + public AddScheduledPublishingLock(IMigrationContext context) + : base(context) { - public AddScheduledPublishingLock(IMigrationContext context) - : base(context) - { - } - - protected override void Migrate() => - Database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.ScheduledPublishing, Name = "ScheduledPublishing" }); } + + protected override void Migrate() => + Database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto {Id = Constants.Locks.ScheduledPublishing, Name = "ScheduledPublishing"}); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/UpdateRelationTypesToHandleDependencies.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/UpdateRelationTypesToHandleDependencies.cs index 1c8fe7ed7201..4bbc3c984c0f 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/UpdateRelationTypesToHandleDependencies.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/UpdateRelationTypesToHandleDependencies.cs @@ -1,34 +1,31 @@ -using System.Linq; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_4_0; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_4_0 +internal class UpdateRelationTypesToHandleDependencies : MigrationBase { - internal class UpdateRelationTypesToHandleDependencies : MigrationBase + public UpdateRelationTypesToHandleDependencies(IMigrationContext context) + : base(context) { - public UpdateRelationTypesToHandleDependencies(IMigrationContext context) - : base(context) - { - } - - protected override void Migrate() - { - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); + } - AddColumnIfNotExists(columns, "isDependency"); + protected override void Migrate() + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); - var aliasesWithDependencies = new[] - { - Core.Constants.Conventions.RelationTypes.RelatedDocumentAlias, - Core.Constants.Conventions.RelationTypes.RelatedMediaAlias - }; + AddColumnIfNotExists(columns, "isDependency"); - Database.Execute( - Sql() - .Update(u => u.Set(x => x.IsDependency, true)) - .WhereIn(x => x.Alias, aliasesWithDependencies)); + var aliasesWithDependencies = new[] + { + Constants.Conventions.RelationTypes.RelatedDocumentAlias, + Constants.Conventions.RelationTypes.RelatedMediaAlias + }; - } + Database.Execute( + Sql() + .Update(u => u.Set(x => x.IsDependency, true)) + .WhereIn(x => x.Alias, aliasesWithDependencies)); } } diff --git a/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorData.cs b/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorData.cs index e2eece831384..7b6bb7f185f6 100644 --- a/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorData.cs +++ b/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorData.cs @@ -1,45 +1,48 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Linq; -namespace Umbraco.Cms.Core.Models.Blocks +namespace Umbraco.Cms.Core.Models.Blocks; + +/// +/// Convertable block data from json +/// +public class BlockEditorData { - /// - /// Convertable block data from json - /// - public class BlockEditorData - { - private readonly string _propertyEditorAlias; + private readonly string _propertyEditorAlias; - public static BlockEditorData Empty { get; } = new BlockEditorData(); + private BlockEditorData() + { + _propertyEditorAlias = string.Empty; + BlockValue = new BlockValue(); + } - private BlockEditorData() + public BlockEditorData(string propertyEditorAlias, + IEnumerable references, + BlockValue blockValue) + { + if (string.IsNullOrWhiteSpace(propertyEditorAlias)) { - _propertyEditorAlias = string.Empty; - BlockValue = new BlockValue(); + throw new ArgumentException($"'{nameof(propertyEditorAlias)}' cannot be null or whitespace", + nameof(propertyEditorAlias)); } - public BlockEditorData(string propertyEditorAlias, - IEnumerable references, - BlockValue blockValue) - { - if (string.IsNullOrWhiteSpace(propertyEditorAlias)) - throw new ArgumentException($"'{nameof(propertyEditorAlias)}' cannot be null or whitespace", nameof(propertyEditorAlias)); - _propertyEditorAlias = propertyEditorAlias; - BlockValue = blockValue ?? throw new ArgumentNullException(nameof(blockValue)); - References = references != null ? new List(references) : throw new ArgumentNullException(nameof(references)); - } + _propertyEditorAlias = propertyEditorAlias; + BlockValue = blockValue ?? throw new ArgumentNullException(nameof(blockValue)); + References = references != null + ? new List(references) + : throw new ArgumentNullException(nameof(references)); + } - /// - /// Returns the layout for this specific property editor - /// - public JToken? Layout => BlockValue.Layout.TryGetValue(_propertyEditorAlias, out var layout) ? layout : null; + public static BlockEditorData Empty { get; } = new(); + + /// + /// Returns the layout for this specific property editor + /// + public JToken? Layout => BlockValue.Layout.TryGetValue(_propertyEditorAlias, out JToken? layout) ? layout : null; - /// - /// Returns the reference to the original BlockValue - /// - public BlockValue BlockValue { get; } + /// + /// Returns the reference to the original BlockValue + /// + public BlockValue BlockValue { get; } - public List References { get; } = new List(); - } + public List References { get; } = new(); } diff --git a/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorDataConverter.cs b/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorDataConverter.cs index 0389603ac2d6..e36c004de63d 100644 --- a/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorDataConverter.cs +++ b/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorDataConverter.cs @@ -1,68 +1,65 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; +using System.Diagnostics.CodeAnalysis; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace Umbraco.Cms.Core.Models.Blocks +namespace Umbraco.Cms.Core.Models.Blocks; + +/// +/// Converts the block json data into objects +/// +public abstract class BlockEditorDataConverter { - /// - /// Converts the block json data into objects - /// - public abstract class BlockEditorDataConverter + private readonly string _propertyEditorAlias; + + protected BlockEditorDataConverter(string propertyEditorAlias) => _propertyEditorAlias = propertyEditorAlias; + + public BlockEditorData ConvertFrom(JToken json) { - private readonly string _propertyEditorAlias; + BlockValue? value = json.ToObject(); + return Convert(value); + } - protected BlockEditorDataConverter(string propertyEditorAlias) + public bool TryDeserialize(string json, [MaybeNullWhen(false)] out BlockEditorData blockEditorData) + { + try { - _propertyEditorAlias = propertyEditorAlias; + BlockValue? value = JsonConvert.DeserializeObject(json); + blockEditorData = Convert(value); + return true; } - - public BlockEditorData ConvertFrom(JToken json) + catch (Exception) { - var value = json.ToObject(); - return Convert(value); + blockEditorData = null; + return false; } + } - public bool TryDeserialize(string json, [MaybeNullWhen(false)] out BlockEditorData blockEditorData) - { - try - { - var value = JsonConvert.DeserializeObject(json); - blockEditorData = Convert(value); - return true; - } - catch (System.Exception) - { - blockEditorData = null; - return false; - } - } + public BlockEditorData Deserialize(string json) + { + BlockValue? value = JsonConvert.DeserializeObject(json); + return Convert(value); + } - public BlockEditorData Deserialize(string json) + private BlockEditorData Convert(BlockValue? value) + { + if (value?.Layout == null) { - var value = JsonConvert.DeserializeObject(json); - return Convert(value); + return BlockEditorData.Empty; } - private BlockEditorData Convert(BlockValue? value) - { - if (value?.Layout == null) - return BlockEditorData.Empty; - - var references = value.Layout.TryGetValue(_propertyEditorAlias, out var layout) + IEnumerable? references = + value.Layout.TryGetValue(_propertyEditorAlias, out JToken? layout) ? GetBlockReferences(layout) : Enumerable.Empty(); - return new BlockEditorData(_propertyEditorAlias, references!, value); - } - - /// - /// Return the collection of from the block editor's Layout (which could be an array or an object depending on the editor) - /// - /// - /// - protected abstract IEnumerable? GetBlockReferences(JToken jsonLayout); - + return new BlockEditorData(_propertyEditorAlias, references!, value); } + + /// + /// Return the collection of from the block editor's Layout (which could be an array or + /// an object depending on the editor) + /// + /// + /// + protected abstract IEnumerable? GetBlockReferences(JToken jsonLayout); } diff --git a/src/Umbraco.Infrastructure/Models/Blocks/BlockItemData.cs b/src/Umbraco.Infrastructure/Models/Blocks/BlockItemData.cs index a459a055ce3d..913158380b5f 100644 --- a/src/Umbraco.Infrastructure/Models/Blocks/BlockItemData.cs +++ b/src/Umbraco.Infrastructure/Models/Blocks/BlockItemData.cs @@ -1,62 +1,59 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json; +using Newtonsoft.Json; using Umbraco.Cms.Infrastructure.Serialization; -namespace Umbraco.Cms.Core.Models.Blocks +namespace Umbraco.Cms.Core.Models.Blocks; + +/// +/// Represents a single block's data in raw form +/// +public class BlockItemData { + [JsonProperty("contentTypeKey")] public Guid ContentTypeKey { get; set; } + + /// + /// not serialized, manually set and used during internally + /// + [JsonIgnore] + public string ContentTypeAlias { get; set; } = string.Empty; + + [JsonProperty("udi")] + [JsonConverter(typeof(UdiJsonConverter))] + public Udi? Udi { get; set; } + + [JsonIgnore] + public Guid Key => Udi is not null ? ((GuidUdi)Udi).Guid : throw new InvalidOperationException("No Udi assigned"); + /// - /// Represents a single block's data in raw form + /// The remaining properties will be serialized to a dictionary /// - public class BlockItemData + /// + /// The JsonExtensionDataAttribute is used to put the non-typed properties into a bucket + /// http://www.newtonsoft.com/json/help/html/DeserializeExtensionData.htm + /// NestedContent serializes to string, int, whatever eg + /// "stringValue":"Some String","numericValue":125,"otherNumeric":null + /// + [JsonExtensionData] + public Dictionary RawPropertyValues { get; set; } = new(); + + /// + /// Used during deserialization to convert the raw property data into data with a property type context + /// + [JsonIgnore] + public IDictionary PropertyValues { get; set; } = + new Dictionary(); + + /// + /// Used during deserialization to populate the property value/property type of a block item content property + /// + public class BlockPropertyValue { - [JsonProperty("contentTypeKey")] - public Guid ContentTypeKey { get; set; } - - /// - /// not serialized, manually set and used during internally - /// - [JsonIgnore] - public string ContentTypeAlias { get; set; } = string.Empty; - - [JsonProperty("udi")] - [JsonConverter(typeof(UdiJsonConverter))] - public Udi? Udi { get; set; } - - [JsonIgnore] - public Guid Key => Udi is not null ? ((GuidUdi)Udi).Guid : throw new InvalidOperationException("No Udi assigned"); - - /// - /// The remaining properties will be serialized to a dictionary - /// - /// - /// The JsonExtensionDataAttribute is used to put the non-typed properties into a bucket - /// http://www.newtonsoft.com/json/help/html/DeserializeExtensionData.htm - /// NestedContent serializes to string, int, whatever eg - /// "stringValue":"Some String","numericValue":125,"otherNumeric":null - /// - [JsonExtensionData] - public Dictionary RawPropertyValues { get; set; } = new Dictionary(); - - /// - /// Used during deserialization to convert the raw property data into data with a property type context - /// - [JsonIgnore] - public IDictionary PropertyValues { get; set; } = new Dictionary(); - - /// - /// Used during deserialization to populate the property value/property type of a block item content property - /// - public class BlockPropertyValue + public BlockPropertyValue(object? value, IPropertyType propertyType) { - public BlockPropertyValue(object? value, IPropertyType propertyType) - { - Value = value; - PropertyType = propertyType ?? throw new ArgumentNullException(nameof(propertyType)); - } - - public object? Value { get; } - public IPropertyType PropertyType { get; } + Value = value; + PropertyType = propertyType ?? throw new ArgumentNullException(nameof(propertyType)); } + + public object? Value { get; } + public IPropertyType PropertyType { get; } } } diff --git a/src/Umbraco.Infrastructure/Models/Blocks/BlockListEditorDataConverter.cs b/src/Umbraco.Infrastructure/Models/Blocks/BlockListEditorDataConverter.cs index 3d6c49c2e95e..d7e2100ea1e1 100644 --- a/src/Umbraco.Infrastructure/Models/Blocks/BlockListEditorDataConverter.cs +++ b/src/Umbraco.Infrastructure/Models/Blocks/BlockListEditorDataConverter.cs @@ -1,22 +1,19 @@ -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Linq; -namespace Umbraco.Cms.Core.Models.Blocks +namespace Umbraco.Cms.Core.Models.Blocks; + +/// +/// Data converter for the block list property editor +/// +public class BlockListEditorDataConverter : BlockEditorDataConverter { - /// - /// Data converter for the block list property editor - /// - public class BlockListEditorDataConverter : BlockEditorDataConverter + public BlockListEditorDataConverter() : base(Constants.PropertyEditors.Aliases.BlockList) { - public BlockListEditorDataConverter() : base(Cms.Core.Constants.PropertyEditors.Aliases.BlockList) - { - } + } - protected override IEnumerable? GetBlockReferences(JToken jsonLayout) - { - var blockListLayout = jsonLayout.ToObject>(); - return blockListLayout?.Select(x => new ContentAndSettingsReference(x.ContentUdi, x.SettingsUdi)).ToList(); - } + protected override IEnumerable? GetBlockReferences(JToken jsonLayout) + { + IEnumerable? blockListLayout = jsonLayout.ToObject>(); + return blockListLayout?.Select(x => new ContentAndSettingsReference(x.ContentUdi, x.SettingsUdi)).ToList(); } } diff --git a/src/Umbraco.Infrastructure/Models/Blocks/BlockListLayoutItem.cs b/src/Umbraco.Infrastructure/Models/Blocks/BlockListLayoutItem.cs index 6df34079f49a..8e646cb29209 100644 --- a/src/Umbraco.Infrastructure/Models/Blocks/BlockListLayoutItem.cs +++ b/src/Umbraco.Infrastructure/Models/Blocks/BlockListLayoutItem.cs @@ -1,19 +1,18 @@ using Newtonsoft.Json; using Umbraco.Cms.Infrastructure.Serialization; -namespace Umbraco.Cms.Core.Models.Blocks +namespace Umbraco.Cms.Core.Models.Blocks; + +/// +/// Used for deserializing the block list layout +/// +public class BlockListLayoutItem { - /// - /// Used for deserializing the block list layout - /// - public class BlockListLayoutItem - { - [JsonProperty("contentUdi", Required = Required.Always)] - [JsonConverter(typeof(UdiJsonConverter))] - public Udi? ContentUdi { get; set; } + [JsonProperty("contentUdi", Required = Required.Always)] + [JsonConverter(typeof(UdiJsonConverter))] + public Udi? ContentUdi { get; set; } - [JsonProperty("settingsUdi", NullValueHandling = NullValueHandling.Ignore)] - [JsonConverter(typeof(UdiJsonConverter))] - public Udi? SettingsUdi { get; set; } - } + [JsonProperty("settingsUdi", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(UdiJsonConverter))] + public Udi? SettingsUdi { get; set; } } diff --git a/src/Umbraco.Infrastructure/Models/Blocks/BlockValue.cs b/src/Umbraco.Infrastructure/Models/Blocks/BlockValue.cs index 3b6df714226a..3970fa3a57f9 100644 --- a/src/Umbraco.Infrastructure/Models/Blocks/BlockValue.cs +++ b/src/Umbraco.Infrastructure/Models/Blocks/BlockValue.cs @@ -1,18 +1,13 @@ -using System.Collections.Generic; -using Newtonsoft.Json; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace Umbraco.Cms.Core.Models.Blocks +namespace Umbraco.Cms.Core.Models.Blocks; + +public class BlockValue { - public class BlockValue - { - [JsonProperty("layout")] - public IDictionary Layout { get; set; } = null!; + [JsonProperty("layout")] public IDictionary Layout { get; set; } = null!; - [JsonProperty("contentData")] - public List ContentData { get; set; } = new List(); + [JsonProperty("contentData")] public List ContentData { get; set; } = new(); - [JsonProperty("settingsData")] - public List SettingsData { get; set; } = new List(); - } + [JsonProperty("settingsData")] public List SettingsData { get; set; } = new(); } diff --git a/src/Umbraco.Infrastructure/Models/GridValue.cs b/src/Umbraco.Infrastructure/Models/GridValue.cs index e873287ffb82..03d4775afb7d 100644 --- a/src/Umbraco.Infrastructure/Models/GridValue.cs +++ b/src/Umbraco.Infrastructure/Models/GridValue.cs @@ -1,87 +1,64 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace Umbraco.Cms.Core.Models -{ - // TODO: Make a property value converter for this! +namespace Umbraco.Cms.Core.Models; +// TODO: Make a property value converter for this! - /// - /// A model representing the value saved for the grid - /// - public class GridValue - { - [JsonProperty("name")] - public string? Name { get; set; } +/// +/// A model representing the value saved for the grid +/// +public class GridValue +{ + [JsonProperty("name")] public string? Name { get; set; } - [JsonProperty("sections")] - public IEnumerable Sections { get; set; } = null!; + [JsonProperty("sections")] public IEnumerable Sections { get; set; } = null!; - public class GridSection - { - [JsonProperty("grid")] - public string? Grid { get; set; } // TODO: what is this? + public class GridSection + { + [JsonProperty("grid")] public string? Grid { get; set; } // TODO: what is this? - [JsonProperty("rows")] - public IEnumerable Rows { get; set; } = null!; - } + [JsonProperty("rows")] public IEnumerable Rows { get; set; } = null!; + } - public class GridRow - { - [JsonProperty("name")] - public string? Name { get; set; } + public class GridRow + { + [JsonProperty("name")] public string? Name { get; set; } - [JsonProperty("id")] - public Guid Id { get; set; } + [JsonProperty("id")] public Guid Id { get; set; } - [JsonProperty("areas")] - public IEnumerable Areas { get; set; } = null!; + [JsonProperty("areas")] public IEnumerable Areas { get; set; } = null!; - [JsonProperty("styles")] - public JToken? Styles { get; set; } + [JsonProperty("styles")] public JToken? Styles { get; set; } - [JsonProperty("config")] - public JToken? Config { get; set; } - } + [JsonProperty("config")] public JToken? Config { get; set; } + } - public class GridArea - { - [JsonProperty("grid")] - public string? Grid { get; set; } // TODO: what is this? + public class GridArea + { + [JsonProperty("grid")] public string? Grid { get; set; } // TODO: what is this? - [JsonProperty("controls")] - public IEnumerable Controls { get; set; } = null!; + [JsonProperty("controls")] public IEnumerable Controls { get; set; } = null!; - [JsonProperty("styles")] - public JToken? Styles { get; set; } + [JsonProperty("styles")] public JToken? Styles { get; set; } - [JsonProperty("config")] - public JToken? Config { get; set; } - } + [JsonProperty("config")] public JToken? Config { get; set; } + } - public class GridControl - { - [JsonProperty("value")] - public JToken Value { get; set; } = null!; + public class GridControl + { + [JsonProperty("value")] public JToken Value { get; set; } = null!; - [JsonProperty("editor")] - public GridEditor Editor { get; set; } = null!; + [JsonProperty("editor")] public GridEditor Editor { get; set; } = null!; - [JsonProperty("styles")] - public JToken? Styles { get; set; } + [JsonProperty("styles")] public JToken? Styles { get; set; } - [JsonProperty("config")] - public JToken? Config { get; set; } - } + [JsonProperty("config")] public JToken? Config { get; set; } + } - public class GridEditor - { - [JsonProperty("alias")] - public string Alias { get; set; } = null!; + public class GridEditor + { + [JsonProperty("alias")] public string Alias { get; set; } = null!; - [JsonProperty("view")] - public string? View { get; set; } - } + [JsonProperty("view")] public string? View { get; set; } } } diff --git a/src/Umbraco.Infrastructure/Models/Mapping/EntityMapDefinition.cs b/src/Umbraco.Infrastructure/Models/Mapping/EntityMapDefinition.cs index abb987c1194f..c91501a13ca4 100644 --- a/src/Umbraco.Infrastructure/Models/Mapping/EntityMapDefinition.cs +++ b/src/Umbraco.Infrastructure/Models/Mapping/EntityMapDefinition.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Globalization; +using System.Globalization; using Examine; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; @@ -9,283 +7,318 @@ using Umbraco.Cms.Infrastructure.Examine; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +public class EntityMapDefinition : IMapDefinition { - public class EntityMapDefinition : IMapDefinition + public void DefineMaps(IUmbracoMapper mapper) + { + mapper.Define((source, context) => new EntityBasic(), Map); + mapper.Define((source, context) => new EntityBasic(), Map); + mapper.Define((source, context) => new EntityBasic(), Map); + mapper.Define((source, context) => new EntityBasic(), Map); + mapper.Define((source, context) => new EntityBasic(), Map); + mapper.Define((source, context) => new ContentTypeSort(), Map); + mapper.Define((source, context) => new EntityBasic(), Map); + mapper.Define((source, context) => new SearchResultEntity(), Map); + mapper.Define((source, context) => new SearchResultEntity(), Map); + mapper.Define>((source, context) => + context.MapEnumerable(source).WhereNotNull()); + mapper.Define, IEnumerable>((source, context) => + context.MapEnumerable(source).WhereNotNull()); + } + + // Umbraco.Code.MapAll -Alias + private static void Map(IEntitySlim source, EntityBasic target, MapperContext context) { - public void DefineMaps(IUmbracoMapper mapper) + target.Icon = MapContentTypeIcon(source); + target.Id = source.Id; + target.Key = source.Key; + target.Name = MapName(source, context); + target.ParentId = source.ParentId; + target.Path = source.Path; + target.Trashed = source.Trashed; + target.Udi = Udi.Create(ObjectTypes.GetUdiType(source.NodeObjectType), source.Key); + + if (source is IContentEntitySlim contentSlim) { - mapper.Define((source, context) => new EntityBasic(), Map); - mapper.Define((source, context) => new EntityBasic(), Map); - mapper.Define((source, context) => new EntityBasic(), Map); - mapper.Define((source, context) => new EntityBasic(), Map); - mapper.Define((source, context) => new EntityBasic(), Map); - mapper.Define((source, context) => new ContentTypeSort(), Map); - mapper.Define((source, context) => new EntityBasic(), Map); - mapper.Define((source, context) => new SearchResultEntity(), Map); - mapper.Define((source, context) => new SearchResultEntity(), Map); - mapper.Define>((source, context) => context.MapEnumerable(source).WhereNotNull()); - mapper.Define, IEnumerable>((source, context) => context.MapEnumerable(source).WhereNotNull()); + source.AdditionalData!["ContentTypeAlias"] = contentSlim.ContentTypeAlias; } - // Umbraco.Code.MapAll -Alias - private static void Map(IEntitySlim source, EntityBasic target, MapperContext context) + if (source is IDocumentEntitySlim documentSlim) { - target.Icon = MapContentTypeIcon(source); - target.Id = source.Id; - target.Key = source.Key; - target.Name = MapName(source, context); - target.ParentId = source.ParentId; - target.Path = source.Path; - target.Trashed = source.Trashed; - target.Udi = Udi.Create(ObjectTypes.GetUdiType(source.NodeObjectType), source.Key); - - if (source is IContentEntitySlim contentSlim) - { - source.AdditionalData!["ContentTypeAlias"] = contentSlim.ContentTypeAlias; - } + source.AdditionalData!["IsPublished"] = documentSlim.Published; + } - if (source is IDocumentEntitySlim documentSlim) + if (source is IMediaEntitySlim mediaSlim) + { + if (source.AdditionalData is not null) { - source.AdditionalData!["IsPublished"] = documentSlim.Published; + //pass UpdateDate for MediaPicker ListView ordering + source.AdditionalData["UpdateDate"] = mediaSlim.UpdateDate; + source.AdditionalData["MediaPath"] = mediaSlim.MediaPath; } + } - if (source is IMediaEntitySlim mediaSlim) + if (source.AdditionalData is not null) + { + // NOTE: we're mapping the objects in AdditionalData by object reference here. + // it works fine for now, but it's something to keep in mind in the future + foreach (KeyValuePair kvp in source.AdditionalData) { - if (source.AdditionalData is not null) + if (kvp.Value is not null) { - //pass UpdateDate for MediaPicker ListView ordering - source.AdditionalData["UpdateDate"] = mediaSlim.UpdateDate; - source.AdditionalData["MediaPath"] = mediaSlim.MediaPath; + target.AdditionalData[kvp.Key] = kvp.Value; } } + } - if (source.AdditionalData is not null) - { - // NOTE: we're mapping the objects in AdditionalData by object reference here. - // it works fine for now, but it's something to keep in mind in the future - foreach(var kvp in source.AdditionalData) - { - if (kvp.Value is not null) - { - target.AdditionalData[kvp.Key] = kvp.Value; - } - } - } + target.AdditionalData.Add("IsContainer", source.IsContainer); + } - target.AdditionalData.Add("IsContainer", source.IsContainer); - } + // Umbraco.Code.MapAll -Udi -Trashed + private static void Map(PropertyType source, EntityBasic target, MapperContext context) + { + target.Alias = source.Alias; + target.Icon = "icon-box"; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = -1; + target.Path = ""; + } - // Umbraco.Code.MapAll -Udi -Trashed - private static void Map(PropertyType source, EntityBasic target, MapperContext context) - { - target.Alias = source.Alias; - target.Icon = "icon-box"; - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = -1; - target.Path = ""; - } + // Umbraco.Code.MapAll -Udi -Trashed + private static void Map(PropertyGroup source, EntityBasic target, MapperContext context) + { + target.Alias = source.Alias; + target.Icon = "icon-tab"; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = -1; + target.Path = ""; + } - // Umbraco.Code.MapAll -Udi -Trashed - private static void Map(PropertyGroup source, EntityBasic target, MapperContext context) - { - target.Alias = source.Alias; - target.Icon = "icon-tab"; - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = -1; - target.Path = ""; - } + // Umbraco.Code.MapAll -Udi -Trashed + private static void Map(IUser source, EntityBasic target, MapperContext context) + { + target.Alias = source.Username; + target.Icon = Constants.Icons.User; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = -1; + target.Path = ""; + } - // Umbraco.Code.MapAll -Udi -Trashed - private static void Map(IUser source, EntityBasic target, MapperContext context) - { - target.Alias = source.Username; - target.Icon = Constants.Icons.User; - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = -1; - target.Path = ""; - } + // Umbraco.Code.MapAll -Trashed + private static void Map(ITemplate source, EntityBasic target, MapperContext context) + { + target.Alias = source.Alias; + target.Icon = Constants.Icons.Template; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = -1; + target.Path = source.Path; + target.Udi = Udi.Create(Constants.UdiEntityType.Template, source.Key); + } - // Umbraco.Code.MapAll -Trashed - private static void Map(ITemplate source, EntityBasic target, MapperContext context) - { - target.Alias = source.Alias; - target.Icon = Constants.Icons.Template; - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = -1; - target.Path = source.Path; - target.Udi = Udi.Create(Constants.UdiEntityType.Template, source.Key); - } + // Umbraco.Code.MapAll -SortOrder + private static void Map(EntityBasic source, ContentTypeSort target, MapperContext context) + { + target.Alias = source.Alias; + target.Id = new Lazy(() => Convert.ToInt32(source.Id)); + } - // Umbraco.Code.MapAll -SortOrder - private static void Map(EntityBasic source, ContentTypeSort target, MapperContext context) - { - target.Alias = source.Alias; - target.Id = new Lazy(() => Convert.ToInt32(source.Id)); - } + // Umbraco.Code.MapAll -Trashed + private static void Map(IContentTypeComposition source, EntityBasic target, MapperContext context) + { + target.Alias = source.Alias; + target.Icon = source.Icon; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = source.ParentId; + target.Path = source.Path; + target.Udi = ContentTypeMapDefinition.MapContentTypeUdi(source); + } - // Umbraco.Code.MapAll -Trashed - private static void Map(IContentTypeComposition source, EntityBasic target, MapperContext context) + // Umbraco.Code.MapAll -Trashed -Alias -Score + private static void Map(EntitySlim source, SearchResultEntity target, MapperContext context) + { + target.Icon = MapContentTypeIcon(source); + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = source.ParentId; + target.Path = source.Path; + target.Udi = Udi.Create(ObjectTypes.GetUdiType(source.NodeObjectType), source.Key); + + if (target.Icon.IsNullOrWhiteSpace()) { - target.Alias = source.Alias; - target.Icon = source.Icon; - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = source.ParentId; - target.Path = source.Path; - target.Udi = ContentTypeMapDefinition.MapContentTypeUdi(source); - } + if (source.NodeObjectType == Constants.ObjectTypes.Document) + { + target.Icon = Constants.Icons.Content; + } - // Umbraco.Code.MapAll -Trashed -Alias -Score - private static void Map(EntitySlim source, SearchResultEntity target, MapperContext context) - { - target.Icon = MapContentTypeIcon(source); - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = source.ParentId; - target.Path = source.Path; - target.Udi = Udi.Create(ObjectTypes.GetUdiType(source.NodeObjectType), source.Key); - - if (target.Icon.IsNullOrWhiteSpace()) + if (source.NodeObjectType == Constants.ObjectTypes.Media) { - if (source.NodeObjectType == Constants.ObjectTypes.Document) - target.Icon = Constants.Icons.Content; - if (source.NodeObjectType == Constants.ObjectTypes.Media) - target.Icon = Constants.Icons.Content; - if (source.NodeObjectType == Constants.ObjectTypes.Member) - target.Icon = Constants.Icons.Member; - else if (source.NodeObjectType == Constants.ObjectTypes.DataType) - target.Icon = Constants.Icons.DataType; - else if (source.NodeObjectType == Constants.ObjectTypes.DocumentType) - target.Icon = Constants.Icons.ContentType; - else if (source.NodeObjectType == Constants.ObjectTypes.MediaType) - target.Icon = Constants.Icons.MediaType; - else if (source.NodeObjectType == Constants.ObjectTypes.MemberType) - target.Icon = Constants.Icons.MemberType; - else if (source.NodeObjectType == Constants.ObjectTypes.TemplateType) - target.Icon = Constants.Icons.Template; + target.Icon = Constants.Icons.Content; + } + + if (source.NodeObjectType == Constants.ObjectTypes.Member) + { + target.Icon = Constants.Icons.Member; + } + else if (source.NodeObjectType == Constants.ObjectTypes.DataType) + { + target.Icon = Constants.Icons.DataType; + } + else if (source.NodeObjectType == Constants.ObjectTypes.DocumentType) + { + target.Icon = Constants.Icons.ContentType; + } + else if (source.NodeObjectType == Constants.ObjectTypes.MediaType) + { + target.Icon = Constants.Icons.MediaType; + } + else if (source.NodeObjectType == Constants.ObjectTypes.MemberType) + { + target.Icon = Constants.Icons.MemberType; + } + else if (source.NodeObjectType == Constants.ObjectTypes.TemplateType) + { + target.Icon = Constants.Icons.Template; } } + } - // Umbraco.Code.MapAll -Alias -Trashed - private static void Map(ISearchResult source, SearchResultEntity target, MapperContext context) - { - target.Id = source.Id; - target.Score = source.Score; + // Umbraco.Code.MapAll -Alias -Trashed + private static void Map(ISearchResult source, SearchResultEntity target, MapperContext context) + { + target.Id = source.Id; + target.Score = source.Score; - // TODO: Properly map this (not aftermap) + // TODO: Properly map this (not aftermap) - //get the icon if there is one - target.Icon = source.Values.ContainsKey(UmbracoExamineFieldNames.IconFieldName) - ? source.Values[UmbracoExamineFieldNames.IconFieldName] - : Constants.Icons.DefaultIcon; + //get the icon if there is one + target.Icon = source.Values.ContainsKey(UmbracoExamineFieldNames.IconFieldName) + ? source.Values[UmbracoExamineFieldNames.IconFieldName] + : Constants.Icons.DefaultIcon; - target.Name = source.Values.ContainsKey(UmbracoExamineFieldNames.NodeNameFieldName) ? source.Values[UmbracoExamineFieldNames.NodeNameFieldName] : "[no name]"; + target.Name = source.Values.ContainsKey(UmbracoExamineFieldNames.NodeNameFieldName) + ? source.Values[UmbracoExamineFieldNames.NodeNameFieldName] + : "[no name]"; - var culture = context.GetCulture()?.ToLowerInvariant(); - if(culture.IsNullOrWhiteSpace() == false) - { - target.Name = source.Values.ContainsKey($"nodeName_{culture}") ? source.Values[$"nodeName_{culture}"] : target.Name; - } + var culture = context.GetCulture()?.ToLowerInvariant(); + if (culture.IsNullOrWhiteSpace() == false) + { + target.Name = source.Values.ContainsKey($"nodeName_{culture}") + ? source.Values[$"nodeName_{culture}"] + : target.Name; + } - if (source.Values.TryGetValue(UmbracoExamineFieldNames.UmbracoFileFieldName, out var umbracoFile) && - umbracoFile.IsNullOrWhiteSpace() == false) + if (source.Values.TryGetValue(UmbracoExamineFieldNames.UmbracoFileFieldName, out var umbracoFile) && + umbracoFile.IsNullOrWhiteSpace() == false) + { + if (umbracoFile != null) { - if (umbracoFile != null) - { - target.Name = $"{target.Name} ({umbracoFile})"; - } + target.Name = $"{target.Name} ({umbracoFile})"; } + } - if (source.Values.ContainsKey(UmbracoExamineFieldNames.NodeKeyFieldName)) + if (source.Values.ContainsKey(UmbracoExamineFieldNames.NodeKeyFieldName)) + { + if (Guid.TryParse(source.Values[UmbracoExamineFieldNames.NodeKeyFieldName], out Guid key)) { - if (Guid.TryParse(source.Values[UmbracoExamineFieldNames.NodeKeyFieldName], out var key)) - { - target.Key = key; + target.Key = key; - //need to set the UDI - if (source.Values.ContainsKey(ExamineFieldNames.CategoryFieldName)) + //need to set the UDI + if (source.Values.ContainsKey(ExamineFieldNames.CategoryFieldName)) + { + switch (source.Values[ExamineFieldNames.CategoryFieldName]) { - switch (source.Values[ExamineFieldNames.CategoryFieldName]) - { - case IndexTypes.Member: - target.Udi = new GuidUdi(Constants.UdiEntityType.Member, target.Key); - break; - case IndexTypes.Content: - target.Udi = new GuidUdi(Constants.UdiEntityType.Document, target.Key); - break; - case IndexTypes.Media: - target.Udi = new GuidUdi(Constants.UdiEntityType.Media, target.Key); - break; - } + case IndexTypes.Member: + target.Udi = new GuidUdi(Constants.UdiEntityType.Member, target.Key); + break; + case IndexTypes.Content: + target.Udi = new GuidUdi(Constants.UdiEntityType.Document, target.Key); + break; + case IndexTypes.Media: + target.Udi = new GuidUdi(Constants.UdiEntityType.Media, target.Key); + break; } } } + } - if (source.Values.ContainsKey("parentID")) + if (source.Values.ContainsKey("parentID")) + { + if (int.TryParse(source.Values["parentID"], NumberStyles.Integer, CultureInfo.InvariantCulture, + out var parentId)) { - if (int.TryParse(source.Values["parentID"], NumberStyles.Integer, CultureInfo.InvariantCulture,out var parentId)) - { - target.ParentId = parentId; - } - else - { - target.ParentId = -1; - } + target.ParentId = parentId; } - - target.Path = source.Values.ContainsKey(UmbracoExamineFieldNames.IndexPathFieldName) ? source.Values[UmbracoExamineFieldNames.IndexPathFieldName] : ""; - - if (source.Values.ContainsKey(ExamineFieldNames.ItemTypeFieldName)) + else { - target.AdditionalData.Add("contentType", source.Values[ExamineFieldNames.ItemTypeFieldName]); + target.ParentId = -1; } } - private static string? MapContentTypeIcon(IEntitySlim entity) - { - switch (entity) - { - case IMemberEntitySlim memberEntity: - return memberEntity.ContentTypeIcon; - case IContentEntitySlim contentEntity: - // NOTE: this case covers both content and media entities - return contentEntity.ContentTypeIcon; - } + target.Path = source.Values.ContainsKey(UmbracoExamineFieldNames.IndexPathFieldName) + ? source.Values[UmbracoExamineFieldNames.IndexPathFieldName] + : ""; - return null; + if (source.Values.ContainsKey(ExamineFieldNames.ItemTypeFieldName)) + { + target.AdditionalData.Add("contentType", source.Values[ExamineFieldNames.ItemTypeFieldName]); } + } - private static string MapName(IEntitySlim source, MapperContext context) + private static string? MapContentTypeIcon(IEntitySlim entity) + { + switch (entity) { - if (!(source is DocumentEntitySlim doc)) - return source.Name!; + case IMemberEntitySlim memberEntity: + return memberEntity.ContentTypeIcon; + case IContentEntitySlim contentEntity: + // NOTE: this case covers both content and media entities + return contentEntity.ContentTypeIcon; + } - // invariant = only 1 name - if (!doc.Variations.VariesByCulture()) return source.Name!; + return null; + } + + private static string MapName(IEntitySlim source, MapperContext context) + { + if (!(source is DocumentEntitySlim doc)) + { + return source.Name!; + } - // variant = depends on culture - var culture = context.GetCulture(); + // invariant = only 1 name + if (!doc.Variations.VariesByCulture()) + { + return source.Name!; + } - // if there's no culture here, the issue is somewhere else (UI, whatever) - throw! - if (culture == null) - //throw new InvalidOperationException("Missing culture in mapping options."); - // TODO: we should throw, but this is used in various places that won't set a culture yet - return source.Name!; + // variant = depends on culture + var culture = context.GetCulture(); - // if we don't have a name for a culture, it means the culture is not available, and - // hey we should probably not be mapping it, but it's too late, return a fallback name - return doc.CultureNames.TryGetValue(culture, out var name) && !name.IsNullOrWhiteSpace() ? name : $"({source.Name})"; + // if there's no culture here, the issue is somewhere else (UI, whatever) - throw! + if (culture == null) + //throw new InvalidOperationException("Missing culture in mapping options."); + // TODO: we should throw, but this is used in various places that won't set a culture yet + { + return source.Name!; } + + // if we don't have a name for a culture, it means the culture is not available, and + // hey we should probably not be mapping it, but it's too late, return a fallback name + return doc.CultureNames.TryGetValue(culture, out var name) && !name.IsNullOrWhiteSpace() + ? name + : $"({source.Name})"; } } diff --git a/src/Umbraco.Infrastructure/Models/MediaWithCrops.cs b/src/Umbraco.Infrastructure/Models/MediaWithCrops.cs index 9e5101550a20..8fb9d6447102 100644 --- a/src/Umbraco.Infrastructure/Models/MediaWithCrops.cs +++ b/src/Umbraco.Infrastructure/Models/MediaWithCrops.cs @@ -1,79 +1,74 @@ using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors.ValueConverters; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a media item with local crops. +/// +/// +public class MediaWithCrops : PublishedContentWrapped { /// - /// Represents a media item with local crops. + /// Initializes a new instance of the class. /// - /// - public class MediaWithCrops : PublishedContentWrapped - { - - /// - /// Gets the content/media item. - /// - /// - /// The content/media item. - /// - public IPublishedContent Content => Unwrap(); + /// The content. + /// The published value fallback. + /// The local crops. + public MediaWithCrops(IPublishedContent content, IPublishedValueFallback publishedValueFallback, + ImageCropperValue localCrops) + : base(content, publishedValueFallback) => + LocalCrops = localCrops; - /// - /// Gets the local crops. - /// - /// - /// The local crops. - /// - public ImageCropperValue LocalCrops { get; } + /// + /// Gets the content/media item. + /// + /// + /// The content/media item. + /// + public IPublishedContent Content => Unwrap(); - /// - /// Initializes a new instance of the class. - /// - /// The content. - /// The published value fallback. - /// The local crops. - public MediaWithCrops(IPublishedContent content, IPublishedValueFallback publishedValueFallback, ImageCropperValue localCrops) - : base(content, publishedValueFallback) - { - LocalCrops = localCrops; - } - } + /// + /// Gets the local crops. + /// + /// + /// The local crops. + /// + public ImageCropperValue LocalCrops { get; } +} +/// +/// Represents a media item with local crops. +/// +/// The type of the media item. +/// +public class MediaWithCrops : MediaWithCrops + where T : IPublishedContent +{ /// - /// Represents a media item with local crops. + /// Initializes a new instance of the class. /// - /// The type of the media item. - /// - public class MediaWithCrops : MediaWithCrops - where T : IPublishedContent - { - /// - /// Gets the media item. - /// - /// - /// The media item. - /// - public new T Content { get; } + /// The content. + /// The published value fallback. + /// The local crops. + public MediaWithCrops(T content, IPublishedValueFallback publishedValueFallback, ImageCropperValue localCrops) + : base(content, publishedValueFallback, localCrops) => + Content = content; - /// - /// Initializes a new instance of the class. - /// - /// The content. - /// The published value fallback. - /// The local crops. - public MediaWithCrops(T content,IPublishedValueFallback publishedValueFallback, ImageCropperValue localCrops) - : base(content, publishedValueFallback, localCrops) - { - Content = content; - } + /// + /// Gets the media item. + /// + /// + /// The media item. + /// + public new T Content { get; } - /// - /// Performs an implicit conversion from to . - /// - /// The media with crops. - /// - /// The result of the conversion. - /// - public static implicit operator T(MediaWithCrops mediaWithCrops) => mediaWithCrops.Content; - } + /// + /// Performs an implicit conversion from to . + /// + /// The media with crops. + /// + /// The result of the conversion. + /// + public static implicit operator T(MediaWithCrops mediaWithCrops) => mediaWithCrops.Content; } diff --git a/src/Umbraco.Infrastructure/Models/PathValidationExtensions.cs b/src/Umbraco.Infrastructure/Models/PathValidationExtensions.cs index e7286d683f10..e00dabc2d275 100644 --- a/src/Umbraco.Infrastructure/Models/PathValidationExtensions.cs +++ b/src/Umbraco.Infrastructure/Models/PathValidationExtensions.cs @@ -1,116 +1,131 @@ -using System; -using System.IO; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Provides extension methods for path validation. +/// +internal static class PathValidationExtensions { /// - /// Provides extension methods for path validation. + /// Does a quick check on the entity's set path to ensure that it's valid and consistent /// - internal static class PathValidationExtensions + /// + /// + public static void ValidatePathWithException(this NodeDto entity) { - /// - /// Does a quick check on the entity's set path to ensure that it's valid and consistent - /// - /// - /// - public static void ValidatePathWithException(this NodeDto entity) + //don't validate if it's empty and it has no id + if (entity.NodeId == default && entity.Path.IsNullOrWhiteSpace()) { - //don't validate if it's empty and it has no id - if (entity.NodeId == default(int) && entity.Path.IsNullOrWhiteSpace()) - return; - - if (entity.Path.IsNullOrWhiteSpace()) - throw new InvalidDataException($"The content item {entity.NodeId} has an empty path: {entity.Path} with parentID: {entity.ParentId}"); + return; + } - var pathParts = entity.Path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); - if (pathParts.Length < 2) - { - //a path cannot be less than 2 parts, at a minimum it must be root (-1) and it's own id - throw new InvalidDataException($"The content item {entity.NodeId} has an invalid path: {entity.Path} with parentID: {entity.ParentId}"); - } + if (entity.Path.IsNullOrWhiteSpace()) + { + throw new InvalidDataException( + $"The content item {entity.NodeId} has an empty path: {entity.Path} with parentID: {entity.ParentId}"); + } - if (entity.ParentId != default(int) && pathParts[pathParts.Length - 2] != entity.ParentId.ToInvariantString()) - { - //the 2nd last id in the path must be it's parent id - throw new InvalidDataException($"The content item {entity.NodeId} has an invalid path: {entity.Path} with parentID: {entity.ParentId}"); - } + var pathParts = entity.Path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); + if (pathParts.Length < 2) + { + //a path cannot be less than 2 parts, at a minimum it must be root (-1) and it's own id + throw new InvalidDataException( + $"The content item {entity.NodeId} has an invalid path: {entity.Path} with parentID: {entity.ParentId}"); } - /// - /// Does a quick check on the entity's set path to ensure that it's valid and consistent - /// - /// - /// - public static bool ValidatePath(this IUmbracoEntity entity) + if (entity.ParentId != default && pathParts[pathParts.Length - 2] != entity.ParentId.ToInvariantString()) { - //don't validate if it's empty and it has no id - if (entity.HasIdentity == false && entity.Path.IsNullOrWhiteSpace()) - return true; + //the 2nd last id in the path must be it's parent id + throw new InvalidDataException( + $"The content item {entity.NodeId} has an invalid path: {entity.Path} with parentID: {entity.ParentId}"); + } + } - if (entity.Path.IsNullOrWhiteSpace()) - return false; + /// + /// Does a quick check on the entity's set path to ensure that it's valid and consistent + /// + /// + /// + public static bool ValidatePath(this IUmbracoEntity entity) + { + //don't validate if it's empty and it has no id + if (entity.HasIdentity == false && entity.Path.IsNullOrWhiteSpace()) + { + return true; + } - var pathParts = entity.Path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); - if (pathParts.Length < 2) - { - //a path cannot be less than 2 parts, at a minimum it must be root (-1) and it's own id - return false; - } + if (entity.Path.IsNullOrWhiteSpace()) + { + return false; + } - if (entity.ParentId != default(int) && pathParts[pathParts.Length - 2] != entity.ParentId.ToInvariantString()) - { - //the 2nd last id in the path must be it's parent id - return false; - } + var pathParts = entity.Path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); + if (pathParts.Length < 2) + { + //a path cannot be less than 2 parts, at a minimum it must be root (-1) and it's own id + return false; + } - return true; + if (entity.ParentId != default && pathParts[pathParts.Length - 2] != entity.ParentId.ToInvariantString()) + { + //the 2nd last id in the path must be it's parent id + return false; } - /// - /// This will validate the entity's path and if it's invalid it will fix it, if fixing is required it will recursively - /// check and fix all ancestors if required. - /// - /// - /// - /// A callback specified to retrieve the parent entity of the entity - /// A callback specified to update a fixed entity - public static void EnsureValidPath(this T entity, - ILogger logger, - Func getParent, - Action update) - where T: IUmbracoEntity + return true; + } + + /// + /// This will validate the entity's path and if it's invalid it will fix it, if fixing is required it will recursively + /// check and fix all ancestors if required. + /// + /// + /// + /// A callback specified to retrieve the parent entity of the entity + /// A callback specified to update a fixed entity + public static void EnsureValidPath(this T entity, + ILogger logger, + Func getParent, + Action update) + where T : IUmbracoEntity + { + if (entity.HasIdentity == false) { - if (entity.HasIdentity == false) - throw new InvalidOperationException("Could not ensure the entity path, the entity has not been assigned an identity"); + throw new InvalidOperationException( + "Could not ensure the entity path, the entity has not been assigned an identity"); + } - if (entity.ValidatePath() == false) + if (entity.ValidatePath() == false) + { + logger.LogWarning( + "The content item {EntityId} has an invalid path: {EntityPath} with parentID: {EntityParentId}", + entity.Id, entity.Path, entity.ParentId); + if (entity.ParentId == -1) { - logger.LogWarning("The content item {EntityId} has an invalid path: {EntityPath} with parentID: {EntityParentId}", entity.Id, entity.Path, entity.ParentId); - if (entity.ParentId == -1) + entity.Path = string.Concat("-1,", entity.Id); + //path changed, update it + update(entity); + } + else + { + T? parent = getParent(entity); + if (parent == null) { - entity.Path = string.Concat("-1,", entity.Id); - //path changed, update it - update(entity); + throw new NullReferenceException("Could not ensure path for entity " + entity.Id + + " could not resolve it's parent " + entity.ParentId); } - else - { - var parent = getParent(entity); - if (parent == null) - throw new NullReferenceException("Could not ensure path for entity " + entity.Id + " could not resolve it's parent " + entity.ParentId); - //the parent must also be valid! - parent.EnsureValidPath(logger, getParent, update); + //the parent must also be valid! + parent.EnsureValidPath(logger, getParent, update); - entity.Path = string.Concat(parent.Path, ",", entity.Id); - //path changed, update it - update(entity); - } + entity.Path = string.Concat(parent.Path, ",", entity.Id); + //path changed, update it + update(entity); } } - } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/ApiVersion.cs b/src/Umbraco.Infrastructure/ModelsBuilder/ApiVersion.cs index fc123d485cd7..0c6bacbbc3c4 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/ApiVersion.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/ApiVersion.cs @@ -1,33 +1,33 @@ -using System; using System.Reflection; using Umbraco.Cms.Core.Semver; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder +namespace Umbraco.Cms.Infrastructure.ModelsBuilder; + +/// +/// Manages API version handshake between client and server. +/// +public class ApiVersion { /// - /// Manages API version handshake between client and server. + /// Initializes a new instance of the class. /// - public class ApiVersion - { - /// - /// Initializes a new instance of the class. - /// - /// The currently executing version. - /// - internal ApiVersion(SemVersion executingVersion) => Version = executingVersion ?? throw new ArgumentNullException(nameof(executingVersion)); + /// The currently executing version. + /// + internal ApiVersion(SemVersion executingVersion) => + Version = executingVersion ?? throw new ArgumentNullException(nameof(executingVersion)); - private static SemVersion CurrentAssemblyVersion - => SemVersion.Parse(Assembly.GetExecutingAssembly().GetCustomAttribute()!.InformationalVersion); + private static SemVersion CurrentAssemblyVersion + => SemVersion.Parse(Assembly.GetExecutingAssembly().GetCustomAttribute()! + .InformationalVersion); - /// - /// Gets the currently executing API version. - /// - public static ApiVersion Current { get; } - = new ApiVersion(CurrentAssemblyVersion); + /// + /// Gets the currently executing API version. + /// + public static ApiVersion Current { get; } + = new(CurrentAssemblyVersion); - /// - /// Gets the executing version of the API. - /// - public SemVersion Version { get; } - } + /// + /// Gets the executing version of the API. + /// + public SemVersion Version { get; } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/AutoModelsNotificationHandler.cs b/src/Umbraco.Infrastructure/ModelsBuilder/AutoModelsNotificationHandler.cs index c61de4ada4a8..00e1a3c59267 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/AutoModelsNotificationHandler.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/AutoModelsNotificationHandler.cs @@ -1,136 +1,131 @@ -using System; -using System.Threading; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Runtime; using Umbraco.Cms.Infrastructure.ModelsBuilder.Building; using Umbraco.Extensions; -using Umbraco.Cms.Core.Configuration; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder +namespace Umbraco.Cms.Infrastructure.ModelsBuilder; + +/// +/// Notification handlers used by . +/// +/// +/// supports mode but not mode. +/// +public sealed class AutoModelsNotificationHandler : INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler { + private static int s_req; + private readonly ModelsBuilderSettings _config; + private readonly ILogger _logger; + private readonly IMainDom _mainDom; + private readonly ModelsGenerationError _mbErrors; + private readonly ModelsGenerator _modelGenerator; + /// - /// Notification handlers used by . + /// Initializes a new instance of the class. /// - /// - /// supports mode but not mode. - /// - public sealed class AutoModelsNotificationHandler : INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler + public AutoModelsNotificationHandler( + ILogger logger, + IOptionsMonitor config, + ModelsGenerator modelGenerator, + ModelsGenerationError mbErrors, + IMainDom mainDom) { - private static int s_req; - private readonly ILogger _logger; - private readonly ModelsBuilderSettings _config; - private readonly ModelsGenerator _modelGenerator; - private readonly ModelsGenerationError _mbErrors; - private readonly IMainDom _mainDom; + _logger = logger; + //We cant use IOptionsSnapshot here, cause this is used in the Core runtime, and that cannot use a scoped service as it has no scope + _config = config.CurrentValue ?? throw new ArgumentNullException(nameof(config)); + _modelGenerator = modelGenerator; + _mbErrors = mbErrors; + _mainDom = mainDom; + } - /// - /// Initializes a new instance of the class. - /// - - public AutoModelsNotificationHandler( - ILogger logger, - IOptionsMonitor config, - ModelsGenerator modelGenerator, - ModelsGenerationError mbErrors, - IMainDom mainDom) - { - _logger = logger; - //We cant use IOptionsSnapshot here, cause this is used in the Core runtime, and that cannot use a scoped service as it has no scope - _config = config.CurrentValue ?? throw new ArgumentNullException(nameof(config)); - _modelGenerator = modelGenerator; - _mbErrors = mbErrors; - _mainDom = mainDom; - } + // we do not manage InMemory models here + internal bool IsEnabled => _config.ModelsMode.IsAutoNotInMemory(); - // we do not manage InMemory models here - internal bool IsEnabled => _config.ModelsMode.IsAutoNotInMemory(); + public void Handle(ContentTypeCacheRefresherNotification notification) => RequestModelsGeneration(); - /// - /// Handles the notification - /// - public void Handle(UmbracoApplicationStartingNotification notification) => Install(); + public void Handle(DataTypeCacheRefresherNotification notification) => RequestModelsGeneration(); - private void Install() + /// + /// Handles the notification + /// + public void Handle(UmbracoApplicationStartingNotification notification) => Install(); + + public void Handle(UmbracoRequestEndNotification notification) + { + if (IsEnabled && _mainDom.IsMainDom) { - // don't run if not enabled - if (!IsEnabled) - { - return; - } + GenerateModelsIfRequested(); } + } - // NOTE - // CacheUpdated triggers within some asynchronous backend task where - // we have no HttpContext. So we use a static (non request-bound) - // var to register that models - // need to be generated. Could be by another request. Anyway. We could - // have collisions but... you know the risk. + private void Install() + { + // don't run if not enabled + if (!IsEnabled) + { + } + } + + // NOTE + // CacheUpdated triggers within some asynchronous backend task where + // we have no HttpContext. So we use a static (non request-bound) + // var to register that models + // need to be generated. Could be by another request. Anyway. We could + // have collisions but... you know the risk. - private void RequestModelsGeneration() + private void RequestModelsGeneration() + { + if (!_mainDom.IsMainDom) { - if (!_mainDom.IsMainDom) - { - return; - } + return; + } - _logger.LogDebug("Requested to generate models."); + _logger.LogDebug("Requested to generate models."); + + Interlocked.Exchange(ref s_req, 1); + } - Interlocked.Exchange(ref s_req, 1); + private void GenerateModelsIfRequested() + { + if (Interlocked.Exchange(ref s_req, 0) == 0) + { + return; } - private void GenerateModelsIfRequested() + // cannot proceed unless we are MainDom + if (_mainDom.IsMainDom) { - if (Interlocked.Exchange(ref s_req, 0) == 0) + try { - return; + _logger.LogDebug("Generate models..."); + _logger.LogInformation("Generate models now."); + _modelGenerator.GenerateModels(); + _mbErrors.Clear(); + _logger.LogInformation("Generated."); } - - // cannot proceed unless we are MainDom - if (_mainDom.IsMainDom) + catch (TimeoutException) { - try - { - _logger.LogDebug("Generate models..."); - _logger.LogInformation("Generate models now."); - _modelGenerator.GenerateModels(); - _mbErrors.Clear(); - _logger.LogInformation("Generated."); - } - catch (TimeoutException) - { - _logger.LogWarning("Timeout, models were NOT generated."); - } - catch (Exception e) - { - _mbErrors.Report("Failed to build Live models.", e); - _logger.LogError("Failed to generate models.", e); - } + _logger.LogWarning("Timeout, models were NOT generated."); } - else + catch (Exception e) { - // this will only occur if this appdomain was MainDom and it has - // been released while trying to regenerate models. - _logger.LogWarning("Cannot generate models while app is shutting down"); + _mbErrors.Report("Failed to build Live models.", e); + _logger.LogError("Failed to generate models.", e); } } - - public void Handle(UmbracoRequestEndNotification notification) + else { - if (IsEnabled && _mainDom.IsMainDom) - { - GenerateModelsIfRequested(); - } + // this will only occur if this appdomain was MainDom and it has + // been released while trying to regenerate models. + _logger.LogWarning("Cannot generate models while app is shutting down"); } - - public void Handle(ContentTypeCacheRefresherNotification notification) => RequestModelsGeneration(); - - public void Handle(DataTypeCacheRefresherNotification notification) => RequestModelsGeneration(); } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/Building/Builder.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/Builder.cs index 4bfd6ff3486d..8c25160618ff 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/Building/Builder.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/Builder.cs @@ -1,219 +1,234 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building +namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building; +// NOTE +// The idea was to have different types of builder, because I wanted to experiment with +// building code with CodeDom. Turns out more complicated than I thought and maybe not +// worth it at the moment, to we're using TextBuilder and its Generate method is specific. +// +// Keeping the code as-is for the time being... + +/// +/// Provides a base class for all builders. +/// +public abstract class Builder { - // NOTE - // The idea was to have different types of builder, because I wanted to experiment with - // building code with CodeDom. Turns out more complicated than I thought and maybe not - // worth it at the moment, to we're using TextBuilder and its Generate method is specific. - // - // Keeping the code as-is for the time being... - /// - /// Provides a base class for all builders. + /// Initializes a new instance of the class with a list of models to generate, + /// the result of code parsing, and a models namespace. /// - public abstract class Builder + /// The list of models to generate. + /// The models namespace. + protected Builder(ModelsBuilderSettings config, IList typeModels) { - protected Dictionary ModelsMap { get; } = new Dictionary(); - - // the list of assemblies that will be 'using' by default - protected IList TypesUsing { get; } = new List - { - "System", - "System.Linq.Expressions", - "Umbraco.Cms.Core.Models.PublishedContent", - "Umbraco.Cms.Core.PublishedCache", - "Umbraco.Cms.Infrastructure.ModelsBuilder", - "Umbraco.Cms.Core", - "Umbraco.Extensions" - }; - - /// - /// Gets or sets a value indicating the namespace to use for the models. - /// - /// May be overriden by code attributes. - public string ModelsNamespace { get; set; } - - /// - /// Gets the list of assemblies to add to the set of 'using' assemblies in each model file. - /// - public IList Using => TypesUsing; - - /// - /// Gets the list of models to generate. - /// - /// The models to generate - public IEnumerable GetModelsToGenerate() => TypeModels; - - /// - /// Gets the list of all models. - /// - /// Includes those that are ignored. - public IList TypeModels { get; } - - /// - /// Initializes a new instance of the class with a list of models to generate, - /// the result of code parsing, and a models namespace. - /// - /// The list of models to generate. - /// The models namespace. - protected Builder(ModelsBuilderSettings config, IList typeModels) - { - TypeModels = typeModels ?? throw new ArgumentNullException(nameof(typeModels)); + TypeModels = typeModels ?? throw new ArgumentNullException(nameof(typeModels)); - Config = config ?? throw new ArgumentNullException(nameof(config)); + Config = config ?? throw new ArgumentNullException(nameof(config)); - // can be null or empty, we'll manage - ModelsNamespace = Config.ModelsNamespace; + // can be null or empty, we'll manage + ModelsNamespace = Config.ModelsNamespace; - // but we want it to prepare - Prepare(); - } + // but we want it to prepare + Prepare(); + } - // for unit tests only + // for unit tests only #pragma warning disable CS8618 - protected Builder() + protected Builder() #pragma warning restore CS8618 - { } + { + } + + protected Dictionary ModelsMap { get; } = new(); + + // the list of assemblies that will be 'using' by default + protected IList TypesUsing { get; } = new List + { + "System", + "System.Linq.Expressions", + "Umbraco.Cms.Core.Models.PublishedContent", + "Umbraco.Cms.Core.PublishedCache", + "Umbraco.Cms.Infrastructure.ModelsBuilder", + "Umbraco.Cms.Core", + "Umbraco.Extensions" + }; + + /// + /// Gets or sets a value indicating the namespace to use for the models. + /// + /// May be overriden by code attributes. + public string ModelsNamespace { get; set; } + + /// + /// Gets the list of assemblies to add to the set of 'using' assemblies in each model file. + /// + public IList Using => TypesUsing; + + /// + /// Gets the list of all models. + /// + /// Includes those that are ignored. + public IList TypeModels { get; } + + protected ModelsBuilderSettings Config { get; } + + public string? ModelsNamespaceForTests { get; set; } + + /// + /// Gets the list of models to generate. + /// + /// The models to generate + public IEnumerable GetModelsToGenerate() => TypeModels; + + /// + /// Prepares generation by processing the result of code parsing. + /// + private void Prepare() + { + TypeModel.MapModelTypes(TypeModels, ModelsNamespace); - protected ModelsBuilderSettings Config { get; } + var isInMemoryMode = Config.ModelsMode == ModelsMode.InMemoryAuto; - /// - /// Prepares generation by processing the result of code parsing. - /// - private void Prepare() + // for the first two of these two tests, + // always throw, even in InMemory mode: cannot happen unless ppl start fidling with attributes to rename + // things, and then they should pay attention to the generation error log - there's no magic here + // for the last one, don't throw in InMemory mode, see comment + + // ensure we have no duplicates type names + foreach (IGrouping xx in TypeModels.GroupBy(x => x.ClrName).Where(x => x.Count() > 1)) + { + throw new InvalidOperationException($"Type name \"{xx.Key}\" is used" + + $" for types with alias {string.Join(", ", xx.Select(x => x.ItemType + ":\"" + x.Alias + "\""))}. Names have to be unique." + + " Consider using an attribute to assign different names to conflicting types."); + } + + // ensure we have no duplicates property names + foreach (TypeModel typeModel in TypeModels) + foreach (IGrouping xx in typeModel.Properties.GroupBy(x => x.ClrName) + .Where(x => x.Count() > 1)) + { + throw new InvalidOperationException( + $"Property name \"{xx.Key}\" in type {typeModel.ItemType}:\"{typeModel.Alias}\"" + + $" is used for properties with alias {string.Join(", ", xx.Select(x => "\"" + x.Alias + "\""))}. Names have to be unique." + + " Consider using an attribute to assign different names to conflicting properties."); + } + + // ensure content & property type don't have identical name (csharp hates it) + foreach (TypeModel typeModel in TypeModels) { - TypeModel.MapModelTypes(TypeModels, ModelsNamespace); - - var isInMemoryMode = Config.ModelsMode == ModelsMode.InMemoryAuto; - - // for the first two of these two tests, - // always throw, even in InMemory mode: cannot happen unless ppl start fidling with attributes to rename - // things, and then they should pay attention to the generation error log - there's no magic here - // for the last one, don't throw in InMemory mode, see comment - - // ensure we have no duplicates type names - foreach (var xx in TypeModels.GroupBy(x => x.ClrName).Where(x => x.Count() > 1)) - throw new InvalidOperationException($"Type name \"{xx.Key}\" is used" - + $" for types with alias {string.Join(", ", xx.Select(x => x.ItemType + ":\"" + x.Alias + "\""))}. Names have to be unique." - + " Consider using an attribute to assign different names to conflicting types."); - - // ensure we have no duplicates property names - foreach (var typeModel in TypeModels) - foreach (var xx in typeModel.Properties.GroupBy(x => x.ClrName).Where(x => x.Count() > 1)) - throw new InvalidOperationException($"Property name \"{xx.Key}\" in type {typeModel.ItemType}:\"{typeModel.Alias}\"" - + $" is used for properties with alias {string.Join(", ", xx.Select(x => "\"" + x.Alias + "\""))}. Names have to be unique." - + " Consider using an attribute to assign different names to conflicting properties."); - - // ensure content & property type don't have identical name (csharp hates it) - foreach (var typeModel in TypeModels) + foreach (PropertyModel xx in typeModel.Properties.Where(x => x.ClrName == typeModel.ClrName)) { - foreach (var xx in typeModel.Properties.Where(x => x.ClrName == typeModel.ClrName)) + if (!isInMemoryMode) { - if (!isInMemoryMode) - throw new InvalidOperationException($"The model class for content type with alias \"{typeModel.Alias}\" is named \"{xx.ClrName}\"." - + $" CSharp does not support using the same name for the property with alias \"{xx.Alias}\"." - + " Consider using an attribute to assign a different name to the property."); - - // in InMemory mode we generate commented out properties with an error message, - // instead of throwing, because then it kills the sites and ppl don't understand why - xx.AddError($"The class {typeModel.ClrName} cannot implement this property, because" - + $" CSharp does not support naming the property with alias \"{xx.Alias}\" with the same name as content type with alias \"{typeModel.Alias}\"." + throw new InvalidOperationException( + $"The model class for content type with alias \"{typeModel.Alias}\" is named \"{xx.ClrName}\"." + + $" CSharp does not support using the same name for the property with alias \"{xx.Alias}\"." + " Consider using an attribute to assign a different name to the property."); - - // will not be implemented on interface nor class - // note: we will still create the static getter, and implement the property on other classes... } + + // in InMemory mode we generate commented out properties with an error message, + // instead of throwing, because then it kills the sites and ppl don't understand why + xx.AddError($"The class {typeModel.ClrName} cannot implement this property, because" + + $" CSharp does not support naming the property with alias \"{xx.Alias}\" with the same name as content type with alias \"{typeModel.Alias}\"." + + " Consider using an attribute to assign a different name to the property."); + + // will not be implemented on interface nor class + // note: we will still create the static getter, and implement the property on other classes... } + } - // ensure we have no collision between base types - // NO: we may want to define a base class in a partial, on a model that has a parent - // we are NOT checking that the defined base type does maintain the inheritance chain - //foreach (var xx in _typeModels.Where(x => !x.IsContentIgnored).Where(x => x.BaseType != null && x.HasBase)) - // throw new InvalidOperationException(string.Format("Type alias \"{0}\" has more than one parent class.", - // xx.Alias)); + // ensure we have no collision between base types + // NO: we may want to define a base class in a partial, on a model that has a parent + // we are NOT checking that the defined base type does maintain the inheritance chain + //foreach (var xx in _typeModels.Where(x => !x.IsContentIgnored).Where(x => x.BaseType != null && x.HasBase)) + // throw new InvalidOperationException(string.Format("Type alias \"{0}\" has more than one parent class.", + // xx.Alias)); - // discover interfaces that need to be declared / implemented - foreach (var typeModel in TypeModels) + // discover interfaces that need to be declared / implemented + foreach (TypeModel typeModel in TypeModels) + { + // collect all the (non-removed) types implemented at parent level + // ie the parent content types and the mixins content types, recursively + var parentImplems = new List(); + if (typeModel.BaseType != null) { - // collect all the (non-removed) types implemented at parent level - // ie the parent content types and the mixins content types, recursively - var parentImplems = new List(); - if (typeModel.BaseType != null) - TypeModel.CollectImplems(parentImplems, typeModel.BaseType); - - // interfaces we must declare we implement (initially empty) - // ie this type's mixins, except those that have been removed, - // and except those that are already declared at the parent level - // in other words, DeclaringInterfaces is "local mixins" - var declaring = typeModel.MixinTypes - .Except(parentImplems); - typeModel.DeclaringInterfaces.AddRange(declaring); - - // interfaces we must actually implement (initially empty) - // if we declare we implement a mixin interface, we must actually implement - // its properties, all recursively (ie if the mixin interface implements...) - // so, starting with local mixins, we collect all the (non-removed) types above them - var mixinImplems = new List(); - foreach (var i in typeModel.DeclaringInterfaces) - TypeModel.CollectImplems(mixinImplems, i); - // and then we remove from that list anything that is already declared at the parent level - typeModel.ImplementingInterfaces.AddRange(mixinImplems.Except(parentImplems)); + TypeModel.CollectImplems(parentImplems, typeModel.BaseType); } - // ensure elements don't inherit from non-elements - foreach (var typeModel in TypeModels.Where(x => x.IsElement)) + // interfaces we must declare we implement (initially empty) + // ie this type's mixins, except those that have been removed, + // and except those that are already declared at the parent level + // in other words, DeclaringInterfaces is "local mixins" + IEnumerable declaring = typeModel.MixinTypes + .Except(parentImplems); + typeModel.DeclaringInterfaces.AddRange(declaring); + + // interfaces we must actually implement (initially empty) + // if we declare we implement a mixin interface, we must actually implement + // its properties, all recursively (ie if the mixin interface implements...) + // so, starting with local mixins, we collect all the (non-removed) types above them + var mixinImplems = new List(); + foreach (TypeModel i in typeModel.DeclaringInterfaces) { - if (typeModel.BaseType != null && !typeModel.BaseType.IsElement) - throw new InvalidOperationException($"Cannot generate model for type '{typeModel.Alias}' because it is an element type, but its parent type '{typeModel.BaseType.Alias}' is not."); - - var errs = typeModel.MixinTypes.Where(x => !x.IsElement).ToList(); - if (errs.Count > 0) - throw new InvalidOperationException($"Cannot generate model for type '{typeModel.Alias}' because it is an element type, but it is composed of {string.Join(", ", errs.Select(x => "'" + x.Alias + "'"))} which {(errs.Count == 1 ? "is" : "are")} not."); + TypeModel.CollectImplems(mixinImplems, i); } + + // and then we remove from that list anything that is already declared at the parent level + typeModel.ImplementingInterfaces.AddRange(mixinImplems.Except(parentImplems)); } - // looking for a simple symbol eg 'Umbraco' or 'String' - // expecting to match eg 'Umbraco' or 'System.String' - // returns true if either - // - more than 1 symbol is found (explicitely ambiguous) - // - 1 symbol is found BUT not matching (implicitely ambiguous) - protected bool IsAmbiguousSymbol(string symbol, string match) + // ensure elements don't inherit from non-elements + foreach (TypeModel typeModel in TypeModels.Where(x => x.IsElement)) { - // cannot figure out is a symbol is ambiguous without Roslyn - // so... let's say everything is ambiguous - code won't be - // pretty but it'll work + if (typeModel.BaseType != null && !typeModel.BaseType.IsElement) + { + throw new InvalidOperationException( + $"Cannot generate model for type '{typeModel.Alias}' because it is an element type, but its parent type '{typeModel.BaseType.Alias}' is not."); + } - // Essentially this means that a `global::` syntax will be output for the generated models - return true; + var errs = typeModel.MixinTypes.Where(x => !x.IsElement).ToList(); + if (errs.Count > 0) + { + throw new InvalidOperationException( + $"Cannot generate model for type '{typeModel.Alias}' because it is an element type, but it is composed of {string.Join(", ", errs.Select(x => "'" + x.Alias + "'"))} which {(errs.Count == 1 ? "is" : "are")} not."); + } } + } - public string? ModelsNamespaceForTests { get; set; } - - public string GetModelsNamespace() + // looking for a simple symbol eg 'Umbraco' or 'String' + // expecting to match eg 'Umbraco' or 'System.String' + // returns true if either + // - more than 1 symbol is found (explicitely ambiguous) + // - 1 symbol is found BUT not matching (implicitely ambiguous) + protected bool IsAmbiguousSymbol(string symbol, string match) => + // cannot figure out is a symbol is ambiguous without Roslyn + // so... let's say everything is ambiguous - code won't be + // pretty but it'll work + // Essentially this means that a `global::` syntax will be output for the generated models + true; + + public string GetModelsNamespace() + { + if (ModelsNamespaceForTests != null) { - if (ModelsNamespaceForTests != null) - return ModelsNamespaceForTests; - - // if builder was initialized with a namespace, use that one - if (!string.IsNullOrWhiteSpace(ModelsNamespace)) - return ModelsNamespace; - - // use configured else fallback to default - return string.IsNullOrWhiteSpace(Config.ModelsNamespace) - ? Constants.ModelsBuilder.DefaultModelsNamespace - : Config.ModelsNamespace; + return ModelsNamespaceForTests; } - protected string GetModelsBaseClassName(TypeModel type) + // if builder was initialized with a namespace, use that one + if (!string.IsNullOrWhiteSpace(ModelsNamespace)) { - // default - return type.IsElement ? "PublishedElementModel" : "PublishedContentModel"; + return ModelsNamespace; } + + // use configured else fallback to default + return string.IsNullOrWhiteSpace(Config.ModelsNamespace) + ? Constants.ModelsBuilder.DefaultModelsNamespace + : Config.ModelsNamespace; } + + protected string GetModelsBaseClassName(TypeModel type) => + // default + type.IsElement ? "PublishedElementModel" : "PublishedContentModel"; } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/Building/ModelsGenerator.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/ModelsGenerator.cs index 930bc163f09f..0b2997e99495 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/Building/ModelsGenerator.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/ModelsGenerator.cs @@ -1,65 +1,64 @@ -using System.IO; using System.Text; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building +namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building; + +public class ModelsGenerator { - public class ModelsGenerator + private readonly IHostingEnvironment _hostingEnvironment; + private readonly OutOfDateModelsStatus _outOfDateModels; + private readonly UmbracoServices _umbracoService; + private ModelsBuilderSettings _config; + + public ModelsGenerator(UmbracoServices umbracoService, IOptionsMonitor config, + OutOfDateModelsStatus outOfDateModels, IHostingEnvironment hostingEnvironment) { - private readonly UmbracoServices _umbracoService; - private ModelsBuilderSettings _config; - private readonly OutOfDateModelsStatus _outOfDateModels; - private readonly IHostingEnvironment _hostingEnvironment; + _umbracoService = umbracoService; + _config = config.CurrentValue; + _outOfDateModels = outOfDateModels; + _hostingEnvironment = hostingEnvironment; + config.OnChange(x => _config = x); + } - public ModelsGenerator(UmbracoServices umbracoService, IOptionsMonitor config, OutOfDateModelsStatus outOfDateModels, IHostingEnvironment hostingEnvironment) + public void GenerateModels() + { + var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); + if (!Directory.Exists(modelsDirectory)) { - _umbracoService = umbracoService; - _config = config.CurrentValue; - _outOfDateModels = outOfDateModels; - _hostingEnvironment = hostingEnvironment; - config.OnChange(x => _config = x); + Directory.CreateDirectory(modelsDirectory); } - public void GenerateModels() + foreach (var file in Directory.GetFiles(modelsDirectory, "*.generated.cs")) { - var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); - if (!Directory.Exists(modelsDirectory)) - { - Directory.CreateDirectory(modelsDirectory); - } - - foreach (var file in Directory.GetFiles(modelsDirectory, "*.generated.cs")) - { - File.Delete(file); - } + File.Delete(file); + } - System.Collections.Generic.IList typeModels = _umbracoService.GetAllTypes(); + IList typeModels = _umbracoService.GetAllTypes(); - var builder = new TextBuilder(_config, typeModels); + var builder = new TextBuilder(_config, typeModels); - foreach (TypeModel typeModel in builder.GetModelsToGenerate()) - { - var sb = new StringBuilder(); - builder.Generate(sb, typeModel); - var filename = Path.Combine(modelsDirectory, typeModel.ClrName + ".generated.cs"); - File.WriteAllText(filename, sb.ToString()); - } + foreach (TypeModel typeModel in builder.GetModelsToGenerate()) + { + var sb = new StringBuilder(); + builder.Generate(sb, typeModel); + var filename = Path.Combine(modelsDirectory, typeModel.ClrName + ".generated.cs"); + File.WriteAllText(filename, sb.ToString()); + } - // the idea was to calculate the current hash and to add it as an extra file to the compilation, - // in order to be able to detect whether a DLL is consistent with an environment - however the - // environment *might not* contain the local partial files, and thus it could be impossible to - // calculate the hash. So... maybe that's not a good idea after all? - /* - var currentHash = HashHelper.Hash(ourFiles, typeModels); - ourFiles["models.hash.cs"] = $@"using Umbraco.ModelsBuilder; + // the idea was to calculate the current hash and to add it as an extra file to the compilation, + // in order to be able to detect whether a DLL is consistent with an environment - however the + // environment *might not* contain the local partial files, and thus it could be impossible to + // calculate the hash. So... maybe that's not a good idea after all? + /* + var currentHash = HashHelper.Hash(ourFiles, typeModels); + ourFiles["models.hash.cs"] = $@"using Umbraco.ModelsBuilder; [assembly:ModelsBuilderAssembly(SourceHash = ""{currentHash}"")] "; - */ + */ - _outOfDateModels.Clear(); - } + _outOfDateModels.Clear(); } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/Building/PropertyModel.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/PropertyModel.cs index 673830873546..2475aebc3f26 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/Building/PropertyModel.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/PropertyModel.cs @@ -1,62 +1,67 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Configuration; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building +namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building; + +/// +/// Represents a model property. +/// +public class PropertyModel { /// - /// Represents a model property. + /// Gets the alias of the property. + /// + public string Alias = string.Empty; + + /// + /// Gets the clr name of the property. + /// + /// This is just the local name eg "Price". + public string ClrName = string.Empty; + + /// + /// Gets the CLR type name of the property values. + /// + public string ClrTypeName = string.Empty; + + /// + /// Gets the description of the property. /// - public class PropertyModel + public string? Description; + + /// + /// Gets the generation errors for the property. + /// + /// + /// This should be null, unless something prevents the property from being + /// generated, and then the value should explain what. This can be used to generate + /// commented out code eg in mode. + /// + public List? Errors; + + /// + /// Gets the Model Clr type of the property values. + /// + /// + /// As indicated by the PublishedPropertyType, ie by the IPropertyValueConverter + /// if any, else object. May include some ModelType that will need to be mapped. + /// + public Type ModelClrType = null!; + + /// + /// Gets the name of the property. + /// + public string Name = string.Empty; + + /// + /// Adds an error. + /// + public void AddError(string error) { - /// - /// Gets the alias of the property. - /// - public string Alias = string.Empty; - - /// - /// Gets the name of the property. - /// - public string Name = string.Empty; - - /// - /// Gets the description of the property. - /// - public string? Description; - - /// - /// Gets the clr name of the property. - /// - /// This is just the local name eg "Price". - public string ClrName = string.Empty; - - /// - /// Gets the Model Clr type of the property values. - /// - /// As indicated by the PublishedPropertyType, ie by the IPropertyValueConverter - /// if any, else object. May include some ModelType that will need to be mapped. - public Type ModelClrType = null!; - - /// - /// Gets the CLR type name of the property values. - /// - public string ClrTypeName = string.Empty; - - /// - /// Gets the generation errors for the property. - /// - /// This should be null, unless something prevents the property from being - /// generated, and then the value should explain what. This can be used to generate - /// commented out code eg in mode. - public List? Errors; - - /// - /// Adds an error. - /// - public void AddError(string error) + if (Errors == null) { - if (Errors == null) Errors = new List(); - Errors.Add(error); + Errors = new List(); } + + Errors.Add(error); } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs index 8bb65eb543b6..6ecb85c4e293 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs @@ -1,587 +1,676 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text; using System.Text.RegularExpressions; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building +namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building; + +/// +/// Implements a builder that works by writing text. +/// +public class TextBuilder : Builder { + private static readonly IDictionary TypesMap = + new Dictionary(StringComparer.OrdinalIgnoreCase) + { + {"System.Int16", "short"}, + {"System.Int32", "int"}, + {"System.Int64", "long"}, + {"System.String", "string"}, + {"System.Object", "object"}, + {"System.Boolean", "bool"}, + {"System.Void", "void"}, + {"System.Char", "char"}, + {"System.Byte", "byte"}, + {"System.UInt16", "ushort"}, + {"System.UInt32", "uint"}, + {"System.UInt64", "ulong"}, + {"System.SByte", "sbyte"}, + {"System.Single", "float"}, + {"System.Double", "double"}, + {"System.Decimal", "decimal"} + }; + /// - /// Implements a builder that works by writing text. + /// Initializes a new instance of the class with a list of models to generate + /// and the result of code parsing. /// - public class TextBuilder : Builder + /// The list of models to generate. + public TextBuilder(ModelsBuilderSettings config, IList typeModels) + : base(config, typeModels) { - /// - /// Initializes a new instance of the class with a list of models to generate - /// and the result of code parsing. - /// - /// The list of models to generate. - public TextBuilder(ModelsBuilderSettings config, IList typeModels) - : base(config, typeModels) - { } - - // internal for unit tests only - public TextBuilder() - { } - - /// - /// Outputs a generated model to a string builder. - /// - /// The string builder. - /// The model to generate. - public void Generate(StringBuilder sb, TypeModel typeModel) - { - WriteHeader(sb); - - foreach (var t in TypesUsing) - sb.AppendFormat("using {0};\n", t); - - sb.Append("\n"); - sb.AppendFormat("namespace {0}\n", GetModelsNamespace()); - sb.Append("{\n"); + } - WriteContentType(sb, typeModel); + // internal for unit tests only + public TextBuilder() + { + } - sb.Append("}\n"); - } + /// + /// Outputs a generated model to a string builder. + /// + /// The string builder. + /// The model to generate. + public void Generate(StringBuilder sb, TypeModel typeModel) + { + WriteHeader(sb); - /// - /// Outputs generated models to a string builder. - /// - /// The string builder. - /// The models to generate. - public void Generate(StringBuilder sb, IEnumerable typeModels) + foreach (var t in TypesUsing) { - WriteHeader(sb); - - foreach (var t in TypesUsing) - sb.AppendFormat("using {0};\n", t); + sb.AppendFormat("using {0};\n", t); + } - // assembly attributes marker - sb.Append("\n//ASSATTR\n"); + sb.Append("\n"); + sb.AppendFormat("namespace {0}\n", GetModelsNamespace()); + sb.Append("{\n"); - sb.Append("\n"); - sb.AppendFormat("namespace {0}\n", GetModelsNamespace()); - sb.Append("{\n"); + WriteContentType(sb, typeModel); - foreach (var typeModel in typeModels) - { - WriteContentType(sb, typeModel); - sb.Append("\n"); - } + sb.Append("}\n"); + } - sb.Append("}\n"); - } + /// + /// Outputs generated models to a string builder. + /// + /// The string builder. + /// The models to generate. + public void Generate(StringBuilder sb, IEnumerable typeModels) + { + WriteHeader(sb); - /// - /// Outputs an "auto-generated" header to a string builder. - /// - /// The string builder. - public static void WriteHeader(StringBuilder sb) + foreach (var t in TypesUsing) { - TextHeaderWriter.WriteHeader(sb); + sb.AppendFormat("using {0};\n", t); } - // writes an attribute that identifies code generated by a tool - // (helps reduce warnings, tools such as FxCop use it) - // see https://github.com/zpqrtbnk/Zbu.ModelsBuilder/issues/107 - // see https://docs.microsoft.com/en-us/dotnet/api/system.codedom.compiler.generatedcodeattribute - // see https://blogs.msdn.microsoft.com/codeanalysis/2007/04/27/correct-usage-of-the-compilergeneratedattribute-and-the-generatedcodeattribute/ - // - // note that the blog post above clearly states that "Nor should it be applied at the type level if the type being generated is a partial class." - // and since our models are partial classes, we have to apply the attribute against the individual members, not the class itself. - // - private static void WriteGeneratedCodeAttribute(StringBuilder sb, string tabs) - { - sb.AppendFormat("{0}[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"Umbraco.ModelsBuilder.Embedded\", \"{1}\")]\n", tabs, ApiVersion.Current.Version); - } + // assembly attributes marker + sb.Append("\n//ASSATTR\n"); - // writes an attribute that specifies that an output may be null. - // (useful for consuming projects with nullable reference types enabled) - private static void WriteMaybeNullAttribute(StringBuilder sb, string tabs, bool isReturn = false) - { - sb.AppendFormat("{0}[{1}global::System.Diagnostics.CodeAnalysis.MaybeNull]\n", tabs, isReturn ? "return: " : ""); - } + sb.Append("\n"); + sb.AppendFormat("namespace {0}\n", GetModelsNamespace()); + sb.Append("{\n"); - private void WriteContentType(StringBuilder sb, TypeModel type) + foreach (TypeModel typeModel in typeModels) { - string sep; - - if (type.IsMixin) - { - // write the interface declaration - sb.AppendFormat("\t// Mixin Content Type with alias \"{0}\"\n", type.Alias); - if (!string.IsNullOrWhiteSpace(type.Name)) - sb.AppendFormat("\t/// {0}\n", XmlCommentString(type.Name)); - sb.AppendFormat("\tpublic partial interface I{0}", type.ClrName); - var implements = type.BaseType == null - ? (type.HasBase ? null : (type.IsElement ? "PublishedElement" : "PublishedContent")) - : type.BaseType.ClrName; - if (implements != null) - sb.AppendFormat(" : I{0}", implements); - - // write the mixins - sep = implements == null ? ":" : ","; - foreach (var mixinType in type.DeclaringInterfaces.OrderBy(x => x.ClrName)) - { - sb.AppendFormat("{0} I{1}", sep, mixinType.ClrName); - sep = ","; - } - - sb.Append("\n\t{\n"); + WriteContentType(sb, typeModel); + sb.Append("\n"); + } - // write the properties - only the local (non-ignored) ones, we're an interface - var more = false; - foreach (var prop in type.Properties.OrderBy(x => x.ClrName)) - { - if (more) sb.Append("\n"); - more = true; - WriteInterfaceProperty(sb, prop); - } + sb.Append("}\n"); + } - sb.Append("\t}\n\n"); - } + /// + /// Outputs an "auto-generated" header to a string builder. + /// + /// The string builder. + public static void WriteHeader(StringBuilder sb) => TextHeaderWriter.WriteHeader(sb); + + // writes an attribute that identifies code generated by a tool + // (helps reduce warnings, tools such as FxCop use it) + // see https://github.com/zpqrtbnk/Zbu.ModelsBuilder/issues/107 + // see https://docs.microsoft.com/en-us/dotnet/api/system.codedom.compiler.generatedcodeattribute + // see https://blogs.msdn.microsoft.com/codeanalysis/2007/04/27/correct-usage-of-the-compilergeneratedattribute-and-the-generatedcodeattribute/ + // + // note that the blog post above clearly states that "Nor should it be applied at the type level if the type being generated is a partial class." + // and since our models are partial classes, we have to apply the attribute against the individual members, not the class itself. + // + private static void WriteGeneratedCodeAttribute(StringBuilder sb, string tabs) => sb.AppendFormat( + "{0}[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"Umbraco.ModelsBuilder.Embedded\", \"{1}\")]\n", + tabs, ApiVersion.Current.Version); + + // writes an attribute that specifies that an output may be null. + // (useful for consuming projects with nullable reference types enabled) + private static void WriteMaybeNullAttribute(StringBuilder sb, string tabs, bool isReturn = false) => + sb.AppendFormat("{0}[{1}global::System.Diagnostics.CodeAnalysis.MaybeNull]\n", tabs, + isReturn ? "return: " : ""); + + private void WriteContentType(StringBuilder sb, TypeModel type) + { + string sep; - // write the class declaration + if (type.IsMixin) + { + // write the interface declaration + sb.AppendFormat("\t// Mixin Content Type with alias \"{0}\"\n", type.Alias); if (!string.IsNullOrWhiteSpace(type.Name)) + { sb.AppendFormat("\t/// {0}\n", XmlCommentString(type.Name)); - // cannot do it now. see note in ImplementContentTypeAttribute - //if (!type.HasImplement) - // sb.AppendFormat("\t[ImplementContentType(\"{0}\")]\n", type.Alias); - sb.AppendFormat("\t[PublishedModel(\"{0}\")]\n", type.Alias); - sb.AppendFormat("\tpublic partial class {0}", type.ClrName); - var inherits = type.HasBase - ? null // has its own base already - : (type.BaseType == null - ? GetModelsBaseClassName(type) - : type.BaseType.ClrName); - if (inherits != null) - sb.AppendFormat(" : {0}", inherits); - - sep = inherits == null ? ":" : ","; - if (type.IsMixin) + } + + sb.AppendFormat("\tpublic partial interface I{0}", type.ClrName); + var implements = type.BaseType == null + ? type.HasBase ? null : type.IsElement ? "PublishedElement" : "PublishedContent" + : type.BaseType.ClrName; + if (implements != null) { - // if it's a mixin it implements its own interface - sb.AppendFormat("{0} I{1}", sep, type.ClrName); + sb.AppendFormat(" : I{0}", implements); } - else + + // write the mixins + sep = implements == null ? ":" : ","; + foreach (TypeModel mixinType in type.DeclaringInterfaces.OrderBy(x => x.ClrName)) { - // write the mixins, if any, as interfaces - // only if not a mixin because otherwise the interface already has them already - foreach (var mixinType in type.DeclaringInterfaces.OrderBy(x => x.ClrName)) - { - sb.AppendFormat("{0} I{1}", sep, mixinType.ClrName); - sep = ","; - } + sb.AppendFormat("{0} I{1}", sep, mixinType.ClrName); + sep = ","; } - // begin class body sb.Append("\n\t{\n"); - // write the constants & static methods - // as 'new' since parent has its own - or maybe not - disable warning - sb.Append("\t\t// helpers\n"); - sb.Append("#pragma warning disable 0109 // new is redundant\n"); - WriteGeneratedCodeAttribute(sb, "\t\t"); - sb.AppendFormat("\t\tpublic new const string ModelTypeAlias = \"{0}\";\n", - type.Alias); - var itemType = type.IsElement ? TypeModel.ItemTypes.Content : type.ItemType; // fixme - WriteGeneratedCodeAttribute(sb, "\t\t"); - sb.AppendFormat("\t\tpublic new const PublishedItemType ModelItemType = PublishedItemType.{0};\n", - itemType); - WriteGeneratedCodeAttribute(sb, "\t\t"); - WriteMaybeNullAttribute(sb, "\t\t", true); - sb.Append("\t\tpublic new static IPublishedContentType GetModelContentType(IPublishedSnapshotAccessor publishedSnapshotAccessor)\n"); - sb.Append("\t\t\t=> PublishedModelUtility.GetModelContentType(publishedSnapshotAccessor, ModelItemType, ModelTypeAlias);\n"); - WriteGeneratedCodeAttribute(sb, "\t\t"); - WriteMaybeNullAttribute(sb, "\t\t", true); - sb.AppendFormat("\t\tpublic static IPublishedPropertyType GetModelPropertyType(IPublishedSnapshotAccessor publishedSnapshotAccessor, Expression> selector)\n", - type.ClrName); - sb.Append("\t\t\t=> PublishedModelUtility.GetModelPropertyType(GetModelContentType(publishedSnapshotAccessor), selector);\n"); - sb.Append("#pragma warning restore 0109\n\n"); - sb.Append("\t\tprivate IPublishedValueFallback _publishedValueFallback;"); + // write the properties - only the local (non-ignored) ones, we're an interface + var more = false; + foreach (PropertyModel prop in type.Properties.OrderBy(x => x.ClrName)) + { + if (more) + { + sb.Append("\n"); + } + + more = true; + WriteInterfaceProperty(sb, prop); + } - // write the ctor - sb.AppendFormat("\n\n\t\t// ctor\n\t\tpublic {0}(IPublished{1} content, IPublishedValueFallback publishedValueFallback)\n\t\t\t: base(content, publishedValueFallback)\n\t\t{{\n\t\t\t_publishedValueFallback = publishedValueFallback;\n\t\t}}\n\n", - type.ClrName, type.IsElement ? "Element" : "Content"); + sb.Append("\t}\n\n"); + } - // write the properties - sb.Append("\t\t// properties\n"); - WriteContentTypeProperties(sb, type); + // write the class declaration + if (!string.IsNullOrWhiteSpace(type.Name)) + { + sb.AppendFormat("\t/// {0}\n", XmlCommentString(type.Name)); + } - // close the class declaration - sb.Append("\t}\n"); + // cannot do it now. see note in ImplementContentTypeAttribute + //if (!type.HasImplement) + // sb.AppendFormat("\t[ImplementContentType(\"{0}\")]\n", type.Alias); + sb.AppendFormat("\t[PublishedModel(\"{0}\")]\n", type.Alias); + sb.AppendFormat("\tpublic partial class {0}", type.ClrName); + var inherits = type.HasBase + ? null // has its own base already + : type.BaseType == null + ? GetModelsBaseClassName(type) + : type.BaseType.ClrName; + if (inherits != null) + { + sb.AppendFormat(" : {0}", inherits); } - private void WriteContentTypeProperties(StringBuilder sb, TypeModel type) + sep = inherits == null ? ":" : ","; + if (type.IsMixin) + { + // if it's a mixin it implements its own interface + sb.AppendFormat("{0} I{1}", sep, type.ClrName); + } + else { - var staticMixinGetters = true; + // write the mixins, if any, as interfaces + // only if not a mixin because otherwise the interface already has them already + foreach (TypeModel mixinType in type.DeclaringInterfaces.OrderBy(x => x.ClrName)) + { + sb.AppendFormat("{0} I{1}", sep, mixinType.ClrName); + sep = ","; + } + } - // write the properties - foreach (var prop in type.Properties.OrderBy(x => x.ClrName)) - WriteProperty(sb, type, prop, staticMixinGetters && type.IsMixin ? type.ClrName : null); + // begin class body + sb.Append("\n\t{\n"); + + // write the constants & static methods + // as 'new' since parent has its own - or maybe not - disable warning + sb.Append("\t\t// helpers\n"); + sb.Append("#pragma warning disable 0109 // new is redundant\n"); + WriteGeneratedCodeAttribute(sb, "\t\t"); + sb.AppendFormat("\t\tpublic new const string ModelTypeAlias = \"{0}\";\n", + type.Alias); + TypeModel.ItemTypes itemType = type.IsElement ? TypeModel.ItemTypes.Content : type.ItemType; // fixme + WriteGeneratedCodeAttribute(sb, "\t\t"); + sb.AppendFormat("\t\tpublic new const PublishedItemType ModelItemType = PublishedItemType.{0};\n", + itemType); + WriteGeneratedCodeAttribute(sb, "\t\t"); + WriteMaybeNullAttribute(sb, "\t\t", true); + sb.Append( + "\t\tpublic new static IPublishedContentType GetModelContentType(IPublishedSnapshotAccessor publishedSnapshotAccessor)\n"); + sb.Append( + "\t\t\t=> PublishedModelUtility.GetModelContentType(publishedSnapshotAccessor, ModelItemType, ModelTypeAlias);\n"); + WriteGeneratedCodeAttribute(sb, "\t\t"); + WriteMaybeNullAttribute(sb, "\t\t", true); + sb.AppendFormat( + "\t\tpublic static IPublishedPropertyType GetModelPropertyType(IPublishedSnapshotAccessor publishedSnapshotAccessor, Expression> selector)\n", + type.ClrName); + sb.Append( + "\t\t\t=> PublishedModelUtility.GetModelPropertyType(GetModelContentType(publishedSnapshotAccessor), selector);\n"); + sb.Append("#pragma warning restore 0109\n\n"); + sb.Append("\t\tprivate IPublishedValueFallback _publishedValueFallback;"); + + // write the ctor + sb.AppendFormat( + "\n\n\t\t// ctor\n\t\tpublic {0}(IPublished{1} content, IPublishedValueFallback publishedValueFallback)\n\t\t\t: base(content, publishedValueFallback)\n\t\t{{\n\t\t\t_publishedValueFallback = publishedValueFallback;\n\t\t}}\n\n", + type.ClrName, type.IsElement ? "Element" : "Content"); + + // write the properties + sb.Append("\t\t// properties\n"); + WriteContentTypeProperties(sb, type); + + // close the class declaration + sb.Append("\t}\n"); + } - // no need to write the parent properties since we inherit from the parent - // and the parent defines its own properties. need to write the mixins properties - // since the mixins are only interfaces and we have to provide an implementation. + private void WriteContentTypeProperties(StringBuilder sb, TypeModel type) + { + var staticMixinGetters = true; - // write the mixins properties - foreach (var mixinType in type.ImplementingInterfaces.OrderBy(x => x.ClrName)) - foreach (var prop in mixinType.Properties.OrderBy(x => x.ClrName)) - if (staticMixinGetters) - WriteMixinProperty(sb, prop, mixinType.ClrName); - else - WriteProperty(sb, mixinType, prop); + // write the properties + foreach (PropertyModel prop in type.Properties.OrderBy(x => x.ClrName)) + { + WriteProperty(sb, type, prop, staticMixinGetters && type.IsMixin ? type.ClrName : null); } - private void WriteMixinProperty(StringBuilder sb, PropertyModel property, string mixinClrName) - { - sb.Append("\n"); + // no need to write the parent properties since we inherit from the parent + // and the parent defines its own properties. need to write the mixins properties + // since the mixins are only interfaces and we have to provide an implementation. - // Adds xml summary to each property containing - // property name and property description - if (!string.IsNullOrWhiteSpace(property.Name) || !string.IsNullOrWhiteSpace(property.Description)) + // write the mixins properties + foreach (TypeModel mixinType in type.ImplementingInterfaces.OrderBy(x => x.ClrName)) + foreach (PropertyModel prop in mixinType.Properties.OrderBy(x => x.ClrName)) + { + if (staticMixinGetters) { - sb.Append("\t\t///\n"); - - if (!string.IsNullOrWhiteSpace(property.Description)) - sb.AppendFormat("\t\t/// {0}: {1}\n", XmlCommentString(property.Name), XmlCommentString(property.Description)); - else - sb.AppendFormat("\t\t/// {0}\n", XmlCommentString(property.Name)); - - sb.Append("\t\t///\n"); + WriteMixinProperty(sb, prop, mixinType.ClrName); + } + else + { + WriteProperty(sb, mixinType, prop); } + } + } - WriteGeneratedCodeAttribute(sb, "\t\t"); + private void WriteMixinProperty(StringBuilder sb, PropertyModel property, string mixinClrName) + { + sb.Append("\n"); - if (!property.ModelClrType.IsValueType) + // Adds xml summary to each property containing + // property name and property description + if (!string.IsNullOrWhiteSpace(property.Name) || !string.IsNullOrWhiteSpace(property.Description)) + { + sb.Append("\t\t///\n"); + + if (!string.IsNullOrWhiteSpace(property.Description)) { - WriteMaybeNullAttribute(sb, "\t\t", false); + sb.AppendFormat("\t\t/// {0}: {1}\n", XmlCommentString(property.Name), + XmlCommentString(property.Description)); + } + else + { + sb.AppendFormat("\t\t/// {0}\n", XmlCommentString(property.Name)); } - sb.AppendFormat("\t\t[ImplementPropertyType(\"{0}\")]\n", property.Alias); - - sb.Append("\t\tpublic virtual "); - WriteClrType(sb, property.ClrTypeName); - sb.AppendFormat(" {0} => ", - property.ClrName); - WriteNonGenericClrType(sb, GetModelsNamespace() + "." + mixinClrName); - sb.AppendFormat(".{0}(this, _publishedValueFallback);\n", - MixinStaticGetterName(property.ClrName)); + sb.Append("\t\t///\n"); } - private static string MixinStaticGetterName(string clrName) + WriteGeneratedCodeAttribute(sb, "\t\t"); + + if (!property.ModelClrType.IsValueType) { - return string.Format("Get{0}", clrName); + WriteMaybeNullAttribute(sb, "\t\t"); } - private void WriteProperty(StringBuilder sb, TypeModel type, PropertyModel property, string? mixinClrName = null) - { - var mixinStatic = mixinClrName != null; + sb.AppendFormat("\t\t[ImplementPropertyType(\"{0}\")]\n", property.Alias); - sb.Append("\n"); + sb.Append("\t\tpublic virtual "); + WriteClrType(sb, property.ClrTypeName); + + sb.AppendFormat(" {0} => ", + property.ClrName); + WriteNonGenericClrType(sb, GetModelsNamespace() + "." + mixinClrName); + sb.AppendFormat(".{0}(this, _publishedValueFallback);\n", + MixinStaticGetterName(property.ClrName)); + } + + private static string MixinStaticGetterName(string clrName) => string.Format("Get{0}", clrName); + + private void WriteProperty(StringBuilder sb, TypeModel type, PropertyModel property, string? mixinClrName = null) + { + var mixinStatic = mixinClrName != null; + + sb.Append("\n"); - if (property.Errors != null) + if (property.Errors != null) + { + sb.Append("\t\t/*\n"); + sb.Append("\t\t * THIS PROPERTY CANNOT BE IMPLEMENTED, BECAUSE:\n"); + sb.Append("\t\t *\n"); + var first = true; + foreach (var error in property.Errors) { - sb.Append("\t\t/*\n"); - sb.Append("\t\t * THIS PROPERTY CANNOT BE IMPLEMENTED, BECAUSE:\n"); - sb.Append("\t\t *\n"); - var first = true; - foreach (var error in property.Errors) + if (first) { - if (first) first = false; - else sb.Append("\t\t *\n"); - foreach (var s in SplitError(error)) - { - sb.Append("\t\t * "); - sb.Append(s); - sb.Append("\n"); - } + first = false; } - sb.Append("\t\t *\n"); - sb.Append("\n"); - } - - // Adds xml summary to each property containing - // property name and property description - if (!string.IsNullOrWhiteSpace(property.Name) || !string.IsNullOrWhiteSpace(property.Description)) - { - sb.Append("\t\t///\n"); - - if (!string.IsNullOrWhiteSpace(property.Description)) - sb.AppendFormat("\t\t/// {0}: {1}\n", XmlCommentString(property.Name), XmlCommentString(property.Description)); else - sb.AppendFormat("\t\t/// {0}\n", XmlCommentString(property.Name)); + { + sb.Append("\t\t *\n"); + } - sb.Append("\t\t///\n"); + foreach (var s in SplitError(error)) + { + sb.Append("\t\t * "); + sb.Append(s); + sb.Append("\n"); + } } - WriteGeneratedCodeAttribute(sb, "\t\t"); - if (!property.ModelClrType.IsValueType) - WriteMaybeNullAttribute(sb, "\t\t"); - sb.AppendFormat("\t\t[ImplementPropertyType(\"{0}\")]\n", property.Alias); + sb.Append("\t\t *\n"); + sb.Append("\n"); + } - if (mixinStatic) + // Adds xml summary to each property containing + // property name and property description + if (!string.IsNullOrWhiteSpace(property.Name) || !string.IsNullOrWhiteSpace(property.Description)) + { + sb.Append("\t\t///\n"); + + if (!string.IsNullOrWhiteSpace(property.Description)) { - sb.Append("\t\tpublic virtual "); - WriteClrType(sb, property.ClrTypeName); - sb.AppendFormat(" {0} => {1}(this, _publishedValueFallback);\n", - property.ClrName, MixinStaticGetterName(property.ClrName)); + sb.AppendFormat("\t\t/// {0}: {1}\n", XmlCommentString(property.Name), + XmlCommentString(property.Description)); } else { - sb.Append("\t\tpublic virtual "); - WriteClrType(sb, property.ClrTypeName); - sb.AppendFormat(" {0} => this.Value", - property.ClrName); - if (property.ModelClrType != typeof(object)) - { - sb.Append("<"); - WriteClrType(sb, property.ClrTypeName); - sb.Append(">"); - } - sb.AppendFormat("(_publishedValueFallback, \"{0}\");\n", - property.Alias); - } - - if (property.Errors != null) - { - sb.Append("\n"); - sb.Append("\t\t *\n"); - sb.Append("\t\t */\n"); + sb.AppendFormat("\t\t/// {0}\n", XmlCommentString(property.Name)); } - if (!mixinStatic) return; - - var mixinStaticGetterName = MixinStaticGetterName(property.ClrName); - - //if (type.StaticMixinMethods.Contains(mixinStaticGetterName)) return; + sb.Append("\t\t///\n"); + } - sb.Append("\n"); + WriteGeneratedCodeAttribute(sb, "\t\t"); + if (!property.ModelClrType.IsValueType) + { + WriteMaybeNullAttribute(sb, "\t\t"); + } - if (!string.IsNullOrWhiteSpace(property.Name)) - sb.AppendFormat("\t\t/// Static getter for {0}\n", XmlCommentString(property.Name)); + sb.AppendFormat("\t\t[ImplementPropertyType(\"{0}\")]\n", property.Alias); - WriteGeneratedCodeAttribute(sb, "\t\t"); - if (!property.ModelClrType.IsValueType) - WriteMaybeNullAttribute(sb, "\t\t", true); - sb.Append("\t\tpublic static "); + if (mixinStatic) + { + sb.Append("\t\tpublic virtual "); WriteClrType(sb, property.ClrTypeName); - sb.AppendFormat(" {0}(I{1} that, IPublishedValueFallback publishedValueFallback) => that.Value", - mixinStaticGetterName, mixinClrName); + sb.AppendFormat(" {0} => {1}(this, _publishedValueFallback);\n", + property.ClrName, MixinStaticGetterName(property.ClrName)); + } + else + { + sb.Append("\t\tpublic virtual "); + WriteClrType(sb, property.ClrTypeName); + sb.AppendFormat(" {0} => this.Value", + property.ClrName); if (property.ModelClrType != typeof(object)) { sb.Append("<"); WriteClrType(sb, property.ClrTypeName); sb.Append(">"); } - sb.AppendFormat("(publishedValueFallback, \"{0}\");\n", + + sb.AppendFormat("(_publishedValueFallback, \"{0}\");\n", property.Alias); } - private static IEnumerable SplitError(string error) + if (property.Errors != null) { - var p = 0; - while (p < error.Length) - { - var n = p + 50; - while (n < error.Length && error[n] != ' ') n++; - if (n >= error.Length) break; - yield return error.Substring(p, n - p); - p = n + 1; - } - if (p < error.Length) - yield return error.Substring(p); + sb.Append("\n"); + sb.Append("\t\t *\n"); + sb.Append("\t\t */\n"); } - private void WriteInterfaceProperty(StringBuilder sb, PropertyModel property) + if (!mixinStatic) { - if (property.Errors != null) - { - sb.Append("\t\t/*\n"); - sb.Append("\t\t * THIS PROPERTY CANNOT BE IMPLEMENTED, BECAUSE:\n"); - sb.Append("\t\t *\n"); - var first = true; - foreach (var error in property.Errors) - { - if (first) first = false; - else sb.Append("\t\t *\n"); - foreach (var s in SplitError(error)) - { - sb.Append("\t\t * "); - sb.Append(s); - sb.Append("\n"); - } - } - sb.Append("\t\t *\n"); - sb.Append("\n"); - } + return; + } - if (!string.IsNullOrWhiteSpace(property.Name)) - sb.AppendFormat("\t\t/// {0}\n", XmlCommentString(property.Name)); - WriteGeneratedCodeAttribute(sb, "\t\t"); - if (!property.ModelClrType.IsValueType) - WriteMaybeNullAttribute(sb, "\t\t"); + var mixinStaticGetterName = MixinStaticGetterName(property.ClrName); - sb.Append("\t\t"); - WriteClrType(sb, property.ClrTypeName); - sb.AppendFormat(" {0} {{ get; }}\n", - property.ClrName); + //if (type.StaticMixinMethods.Contains(mixinStaticGetterName)) return; - if (property.Errors != null) - { - sb.Append("\n"); - sb.Append("\t\t *\n"); - sb.Append("\t\t */\n"); - } + sb.Append("\n"); + + if (!string.IsNullOrWhiteSpace(property.Name)) + { + sb.AppendFormat("\t\t/// Static getter for {0}\n", XmlCommentString(property.Name)); + } + + WriteGeneratedCodeAttribute(sb, "\t\t"); + if (!property.ModelClrType.IsValueType) + { + WriteMaybeNullAttribute(sb, "\t\t", true); } - // internal for unit tests - public void WriteClrType(StringBuilder sb, Type type) + sb.Append("\t\tpublic static "); + WriteClrType(sb, property.ClrTypeName); + sb.AppendFormat(" {0}(I{1} that, IPublishedValueFallback publishedValueFallback) => that.Value", + mixinStaticGetterName, mixinClrName); + if (property.ModelClrType != typeof(object)) { - var s = type.ToString(); + sb.Append("<"); + WriteClrType(sb, property.ClrTypeName); + sb.Append(">"); + } - if (type.IsGenericType) + sb.AppendFormat("(publishedValueFallback, \"{0}\");\n", + property.Alias); + } + + private static IEnumerable SplitError(string error) + { + var p = 0; + while (p < error.Length) + { + var n = p + 50; + while (n < error.Length && error[n] != ' ') { - var p = s.IndexOf('`'); - WriteNonGenericClrType(sb, s.Substring(0, p)); - sb.Append("<"); - var args = type.GetGenericArguments(); - for (var i = 0; i < args.Length; i++) - { - if (i > 0) sb.Append(", "); - WriteClrType(sb, args[i]); - } - sb.Append(">"); + n++; } - else + + if (n >= error.Length) { - WriteNonGenericClrType(sb, s); + break; } + + yield return error.Substring(p, n - p); + p = n + 1; } - internal void WriteClrType(StringBuilder sb, string type) + if (p < error.Length) { - var p = type.IndexOf('<'); - if (type.Contains('<')) + yield return error.Substring(p); + } + } + + private void WriteInterfaceProperty(StringBuilder sb, PropertyModel property) + { + if (property.Errors != null) + { + sb.Append("\t\t/*\n"); + sb.Append("\t\t * THIS PROPERTY CANNOT BE IMPLEMENTED, BECAUSE:\n"); + sb.Append("\t\t *\n"); + var first = true; + foreach (var error in property.Errors) { - WriteNonGenericClrType(sb, type.Substring(0, p)); - sb.Append("<"); - var args = type.Substring(p + 1).TrimEnd(Constants.CharArrays.GreaterThan).Split(Constants.CharArrays.Comma); // fixme will NOT work with nested generic types - for (var i = 0; i < args.Length; i++) + if (first) { - if (i > 0) sb.Append(", "); - WriteClrType(sb, args[i]); + first = false; + } + else + { + sb.Append("\t\t *\n"); + } + + foreach (var s in SplitError(error)) + { + sb.Append("\t\t * "); + sb.Append(s); + sb.Append("\n"); } - sb.Append(">"); - } - else - { - WriteNonGenericClrType(sb, type); } + + sb.Append("\t\t *\n"); + sb.Append("\n"); } - private void WriteNonGenericClrType(StringBuilder sb, string s) + if (!string.IsNullOrWhiteSpace(property.Name)) { - // map model types - s = Regex.Replace(s, @"\{(.*)\}\[\*\]", m => ModelsMap[m.Groups[1].Value + "[]"]); + sb.AppendFormat("\t\t/// {0}\n", XmlCommentString(property.Name)); + } - // takes care eg of "System.Int32" vs. "int" - if (TypesMap.TryGetValue(s, out string? typeName)) - { - sb.Append(typeName); - return; - } + WriteGeneratedCodeAttribute(sb, "\t\t"); + if (!property.ModelClrType.IsValueType) + { + WriteMaybeNullAttribute(sb, "\t\t"); + } + + sb.Append("\t\t"); + WriteClrType(sb, property.ClrTypeName); + sb.AppendFormat(" {0} {{ get; }}\n", + property.ClrName); + + if (property.Errors != null) + { + sb.Append("\n"); + sb.Append("\t\t *\n"); + sb.Append("\t\t */\n"); + } + } + + // internal for unit tests + public void WriteClrType(StringBuilder sb, Type type) + { + var s = type.ToString(); - // if full type name matches a using clause, strip - // so if we want Umbraco.Core.Models.IPublishedContent - // and using Umbraco.Core.Models, then we just need IPublishedContent - typeName = s; - string? typeUsing = null; - var p = typeName.LastIndexOf('.'); - if (p > 0) + if (type.IsGenericType) + { + var p = s.IndexOf('`'); + WriteNonGenericClrType(sb, s.Substring(0, p)); + sb.Append("<"); + Type[] args = type.GetGenericArguments(); + for (var i = 0; i < args.Length; i++) { - var x = typeName.Substring(0, p); - if (Using.Contains(x)) + if (i > 0) { - typeName = typeName.Substring(p + 1); - typeUsing = x; + sb.Append(", "); } - else if (x == ModelsNamespace) // that one is used by default + + WriteClrType(sb, args[i]); + } + + sb.Append(">"); + } + else + { + WriteNonGenericClrType(sb, s); + } + } + + internal void WriteClrType(StringBuilder sb, string type) + { + var p = type.IndexOf('<'); + if (type.Contains('<')) + { + WriteNonGenericClrType(sb, type.Substring(0, p)); + sb.Append("<"); + var args = type.Substring(p + 1).TrimEnd(Constants.CharArrays.GreaterThan) + .Split(Constants.CharArrays.Comma); // fixme will NOT work with nested generic types + for (var i = 0; i < args.Length; i++) + { + if (i > 0) { - typeName = typeName.Substring(p + 1); - typeUsing = ModelsNamespace; + sb.Append(", "); } + + WriteClrType(sb, args[i]); } - // nested types *after* using - typeName = typeName.Replace("+", "."); + sb.Append(">"); + } + else + { + WriteNonGenericClrType(sb, type); + } + } - // symbol to test is the first part of the name - // so if type name is Foo.Bar.Nil we want to ensure that Foo is not ambiguous - p = typeName.IndexOf('.'); - var symbol = p > 0 ? typeName.Substring(0, p) : typeName; + private void WriteNonGenericClrType(StringBuilder sb, string s) + { + // map model types + s = Regex.Replace(s, @"\{(.*)\}\[\*\]", m => ModelsMap[m.Groups[1].Value + "[]"]); - // what we should find - WITHOUT any generic thing - just the type - // no 'using' = the exact symbol - // a 'using' = using.symbol - var match = typeUsing == null ? symbol : (typeUsing + "." + symbol); + // takes care eg of "System.Int32" vs. "int" + if (TypesMap.TryGetValue(s, out var typeName)) + { + sb.Append(typeName); + return; + } - // if not ambiguous, be happy - if (!IsAmbiguousSymbol(symbol, match)) + // if full type name matches a using clause, strip + // so if we want Umbraco.Core.Models.IPublishedContent + // and using Umbraco.Core.Models, then we just need IPublishedContent + typeName = s; + string? typeUsing = null; + var p = typeName.LastIndexOf('.'); + if (p > 0) + { + var x = typeName.Substring(0, p); + if (Using.Contains(x)) { - sb.Append(typeName); - return; + typeName = typeName.Substring(p + 1); + typeUsing = x; } - - // symbol is ambiguous - // if no 'using', must prepend global:: - if (typeUsing == null) + else if (x == ModelsNamespace) // that one is used by default { - sb.Append("global::"); - sb.Append(s.Replace("+", ".")); - return; + typeName = typeName.Substring(p + 1); + typeUsing = ModelsNamespace; } + } - // could fullname be non-ambiguous? - // note: all-or-nothing, not trying to segment the using clause - typeName = s.Replace("+", "."); - p = typeName.IndexOf('.'); - symbol = typeName.Substring(0, p); - match = symbol; + // nested types *after* using + typeName = typeName.Replace("+", "."); - // still ambiguous, must prepend global:: - if (IsAmbiguousSymbol(symbol, match)) - sb.Append("global::"); + // symbol to test is the first part of the name + // so if type name is Foo.Bar.Nil we want to ensure that Foo is not ambiguous + p = typeName.IndexOf('.'); + var symbol = p > 0 ? typeName.Substring(0, p) : typeName; + // what we should find - WITHOUT any generic thing - just the type + // no 'using' = the exact symbol + // a 'using' = using.symbol + var match = typeUsing == null ? symbol : typeUsing + "." + symbol; + + // if not ambiguous, be happy + if (!IsAmbiguousSymbol(symbol, match)) + { sb.Append(typeName); + return; } - private static string XmlCommentString(string s) + // symbol is ambiguous + // if no 'using', must prepend global:: + if (typeUsing == null) { - return s.Replace('<', '{').Replace('>', '}').Replace('\r', ' ').Replace('\n', ' '); + sb.Append("global::"); + sb.Append(s.Replace("+", ".")); + return; } - private static readonly IDictionary TypesMap = new Dictionary(StringComparer.OrdinalIgnoreCase) + // could fullname be non-ambiguous? + // note: all-or-nothing, not trying to segment the using clause + typeName = s.Replace("+", "."); + p = typeName.IndexOf('.'); + symbol = typeName.Substring(0, p); + match = symbol; + + // still ambiguous, must prepend global:: + if (IsAmbiguousSymbol(symbol, match)) { - { "System.Int16", "short" }, - { "System.Int32", "int" }, - { "System.Int64", "long" }, - { "System.String", "string" }, - { "System.Object", "object" }, - { "System.Boolean", "bool" }, - { "System.Void", "void" }, - { "System.Char", "char" }, - { "System.Byte", "byte" }, - { "System.UInt16", "ushort" }, - { "System.UInt32", "uint" }, - { "System.UInt64", "ulong" }, - { "System.SByte", "sbyte" }, - { "System.Single", "float" }, - { "System.Double", "double" }, - { "System.Decimal", "decimal" } - }; + sb.Append("global::"); + } + + sb.Append(typeName); } + + private static string XmlCommentString(string s) => + s.Replace('<', '{').Replace('>', '}').Replace('\r', ' ').Replace('\n', ' '); } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextHeaderWriter.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextHeaderWriter.cs index a192560f1dc7..5a532cbdbac1 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextHeaderWriter.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextHeaderWriter.cs @@ -1,25 +1,24 @@ using System.Text; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building +namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building; + +internal static class TextHeaderWriter { - internal static class TextHeaderWriter + /// + /// Outputs an "auto-generated" header to a string builder. + /// + /// The string builder. + public static void WriteHeader(StringBuilder sb) { - /// - /// Outputs an "auto-generated" header to a string builder. - /// - /// The string builder. - public static void WriteHeader(StringBuilder sb) - { - sb.Append("//------------------------------------------------------------------------------\n"); - sb.Append("// \n"); - sb.Append("// This code was generated by a tool.\n"); - sb.Append("//\n"); - sb.AppendFormat("// Umbraco.ModelsBuilder.Embedded v{0}\n", ApiVersion.Current.Version); - sb.Append("//\n"); - sb.Append("// Changes to this file will be lost if the code is regenerated.\n"); - sb.Append("// \n"); - sb.Append("//------------------------------------------------------------------------------\n"); - sb.Append("\n"); - } + sb.Append("//------------------------------------------------------------------------------\n"); + sb.Append("// \n"); + sb.Append("// This code was generated by a tool.\n"); + sb.Append("//\n"); + sb.AppendFormat("// Umbraco.ModelsBuilder.Embedded v{0}\n", ApiVersion.Current.Version); + sb.Append("//\n"); + sb.Append("// Changes to this file will be lost if the code is regenerated.\n"); + sb.Append("// \n"); + sb.Append("//------------------------------------------------------------------------------\n"); + sb.Append("\n"); } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModel.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModel.cs index 00da2e06fcc9..d8fd00158fb1 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModel.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModel.cs @@ -1,204 +1,218 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building +namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building; + +/// +/// Represents a model. +/// +public class TypeModel { /// - /// Represents a model. + /// Represents the different model item types. /// - public class TypeModel + public enum ItemTypes { /// - /// Gets the unique identifier of the corresponding content type. + /// Element. /// - public int Id; + Element, /// - /// Gets the alias of the model. + /// Content. /// - public string Alias = string.Empty; + Content, /// - /// Gets the name of the content type. + /// Media. /// - public string? Name; + Media, /// - /// Gets the description of the content type. + /// Member. /// - public string? Description; + Member + } - /// - /// Gets the clr name of the model. - /// - /// This is the complete name eg "Foo.Bar.MyContent". - public string ClrName = string.Empty; + /// + /// Gets the list of interfaces that this model needs to declare it implements. + /// + /// + /// Some of these interfaces may actually be implemented by a base model + /// that this model inherits from. + /// + public readonly List DeclaringInterfaces = new(); - /// - /// Gets the unique identifier of the parent. - /// - /// The parent can either be a base content type, or a content types container. If the content - /// type does not have a base content type, then returns -1. - public int ParentId; + /// + /// Gets the list of interfaces that this model needs to actually implement. + /// + public readonly List ImplementingInterfaces = new(); - /// - /// Gets the base model. - /// - /// - /// If the content type does not have a base content type, then returns null. - /// The current model inherits from its base model. - /// - public TypeModel? BaseType; // the parent type in Umbraco (type inherits its properties) + /// + /// Gets the mixin models. + /// + /// The current model implements mixins. + public readonly List MixinTypes = new(); - /// - /// Gets the list of properties that are defined by this model. - /// - /// These are only those property that are defined locally by this model, - /// and the list does not contain properties inherited from base models or from mixins. - public readonly List Properties = new List(); + /// + /// Gets the list of properties that are defined by this model. + /// + /// + /// These are only those property that are defined locally by this model, + /// and the list does not contain properties inherited from base models or from mixins. + /// + public readonly List Properties = new(); - /// - /// Gets the mixin models. - /// - /// The current model implements mixins. - public readonly List MixinTypes = new List(); + private ItemTypes _itemType; - /// - /// Gets the list of interfaces that this model needs to declare it implements. - /// - /// Some of these interfaces may actually be implemented by a base model - /// that this model inherits from. - public readonly List DeclaringInterfaces = new List(); + /// + /// Gets the alias of the model. + /// + public string Alias = string.Empty; - /// - /// Gets the list of interfaces that this model needs to actually implement. - /// - public readonly List ImplementingInterfaces = new List(); + /// + /// Gets the base model. + /// + /// + /// If the content type does not have a base content type, then returns null. + /// The current model inherits from its base model. + /// + public TypeModel? BaseType; // the parent type in Umbraco (type inherits its properties) - ///// - ///// Gets the list of existing static mixin method candidates. - ///// - //public readonly List StaticMixinMethods = new List(); //TODO: Do we need this? it isn't used + /// + /// Gets the clr name of the model. + /// + /// This is the complete name eg "Foo.Bar.MyContent". + public string ClrName = string.Empty; - /// - /// Gets a value indicating whether this model has a base class. - /// - /// Can be either because the content type has a base content type declared in Umbraco, - /// or because the existing user's code declares a base class for this model. - public bool HasBase; + /// + /// Gets the description of the content type. + /// + public string? Description; - /// - /// Gets a value indicating whether this model is used as a mixin by another model. - /// - public bool IsMixin; + ///// + ///// Gets the list of existing static mixin method candidates. + ///// + //public readonly List StaticMixinMethods = new List(); //TODO: Do we need this? it isn't used - /// - /// Gets a value indicating whether this model is the base model of another model. - /// - public bool IsParent; + /// + /// Gets a value indicating whether this model has a base class. + /// + /// + /// Can be either because the content type has a base content type declared in Umbraco, + /// or because the existing user's code declares a base class for this model. + /// + public bool HasBase; - /// - /// Gets a value indicating whether the type is an element. - /// - public bool IsElement => ItemType == ItemTypes.Element; + /// + /// Gets the unique identifier of the corresponding content type. + /// + public int Id; - /// - /// Represents the different model item types. - /// - public enum ItemTypes - { - /// - /// Element. - /// - Element, - - /// - /// Content. - /// - Content, - - /// - /// Media. - /// - Media, - - /// - /// Member. - /// - Member - } + /// + /// Gets a value indicating whether this model is used as a mixin by another model. + /// + public bool IsMixin; + + /// + /// Gets a value indicating whether this model is the base model of another model. + /// + public bool IsParent; - private ItemTypes _itemType; + /// + /// Gets the name of the content type. + /// + public string? Name; - /// - /// Gets or sets the model item type. - /// - public ItemTypes ItemType + /// + /// Gets the unique identifier of the parent. + /// + /// + /// The parent can either be a base content type, or a content types container. If the content + /// type does not have a base content type, then returns -1. + /// + public int ParentId; + + /// + /// Gets a value indicating whether the type is an element. + /// + public bool IsElement => ItemType == ItemTypes.Element; + + /// + /// Gets or sets the model item type. + /// + public ItemTypes ItemType + { + get => _itemType; + set { - get { return _itemType; } - set + switch (value) { - switch (value) - { - case ItemTypes.Element: - case ItemTypes.Content: - case ItemTypes.Media: - case ItemTypes.Member: - _itemType = value; - break; - default: - throw new ArgumentException("value"); - } + case ItemTypes.Element: + case ItemTypes.Content: + case ItemTypes.Media: + case ItemTypes.Member: + _itemType = value; + break; + default: + throw new ArgumentException("value"); } } + } - /// - /// Recursively collects all types inherited, or implemented as interfaces, by a specified type. - /// - /// The collection. - /// The type. - /// Includes the specified type. - internal static void CollectImplems(ICollection types, TypeModel type) + /// + /// Recursively collects all types inherited, or implemented as interfaces, by a specified type. + /// + /// The collection. + /// The type. + /// Includes the specified type. + internal static void CollectImplems(ICollection types, TypeModel type) + { + if (types.Contains(type) == false) { - if (types.Contains(type) == false) - types.Add(type); - if (type.BaseType != null) - CollectImplems(types, type.BaseType); - foreach (var mixin in type.MixinTypes) - CollectImplems(types, mixin); + types.Add(type); } - /// - /// Enumerates the base models starting from the current model up. - /// - /// Indicates whether the enumeration should start with the current model - /// or from its base model. - /// The base models. - public IEnumerable EnumerateBaseTypes(bool andSelf = false) + if (type.BaseType != null) { - var typeModel = andSelf ? this : BaseType; - while (typeModel != null) - { - yield return typeModel; - typeModel = typeModel.BaseType; - } + CollectImplems(types, type.BaseType); } - /// - /// Maps ModelType. - /// - public static void MapModelTypes(IList typeModels, string ns) + foreach (TypeModel mixin in type.MixinTypes) + { + CollectImplems(types, mixin); + } + } + + /// + /// Enumerates the base models starting from the current model up. + /// + /// + /// Indicates whether the enumeration should start with the current model + /// or from its base model. + /// + /// The base models. + public IEnumerable EnumerateBaseTypes(bool andSelf = false) + { + TypeModel? typeModel = andSelf ? this : BaseType; + while (typeModel != null) + { + yield return typeModel; + typeModel = typeModel.BaseType; + } + } + + /// + /// Maps ModelType. + /// + public static void MapModelTypes(IList typeModels, string ns) + { + var hasNs = !string.IsNullOrWhiteSpace(ns); + var map = typeModels.ToDictionary(x => x.Alias, x => hasNs ? ns + "." + x.ClrName : x.ClrName); + foreach (TypeModel typeModel in typeModels) { - var hasNs = !string.IsNullOrWhiteSpace(ns); - var map = typeModels.ToDictionary(x => x.Alias, x => hasNs ? (ns + "." + x.ClrName) : x.ClrName); - foreach (var typeModel in typeModels) + foreach (PropertyModel propertyModel in typeModel.Properties) { - foreach (var propertyModel in typeModel.Properties) - { - propertyModel.ClrTypeName = ModelType.MapToName(propertyModel.ModelClrType, map); - } + propertyModel.ClrTypeName = ModelType.MapToName(propertyModel.ModelClrType, map); } } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModelHasher.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModelHasher.cs index 46af4572997f..844b207d626a 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModelHasher.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModelHasher.cs @@ -1,46 +1,43 @@ -using System.Collections.Generic; -using System.Linq; using System.Text; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building +namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building; + +public class TypeModelHasher { - public class TypeModelHasher + public static string Hash(IEnumerable typeModels) { - public static string Hash(IEnumerable typeModels) - { - var builder = new StringBuilder(); + var builder = new StringBuilder(); - // see Umbraco.ModelsBuilder.Umbraco.Application for what's important to hash - // ie what comes from Umbraco (not computed by ModelsBuilder) and makes a difference + // see Umbraco.ModelsBuilder.Umbraco.Application for what's important to hash + // ie what comes from Umbraco (not computed by ModelsBuilder) and makes a difference - foreach (var typeModel in typeModels.OrderBy(x => x.Alias)) - { - builder.AppendLine("--- CONTENT TYPE MODEL ---"); - builder.AppendLine(typeModel.Id.ToString()); - builder.AppendLine(typeModel.Alias); - builder.AppendLine(typeModel.ClrName); - builder.AppendLine(typeModel.ParentId.ToString()); - builder.AppendLine(typeModel.Name); - builder.AppendLine(typeModel.Description); - builder.AppendLine(typeModel.ItemType.ToString()); - builder.AppendLine("MIXINS:" + string.Join(",", typeModel.MixinTypes.OrderBy(x => x.Id).Select(x => x.Id))); + foreach (TypeModel typeModel in typeModels.OrderBy(x => x.Alias)) + { + builder.AppendLine("--- CONTENT TYPE MODEL ---"); + builder.AppendLine(typeModel.Id.ToString()); + builder.AppendLine(typeModel.Alias); + builder.AppendLine(typeModel.ClrName); + builder.AppendLine(typeModel.ParentId.ToString()); + builder.AppendLine(typeModel.Name); + builder.AppendLine(typeModel.Description); + builder.AppendLine(typeModel.ItemType.ToString()); + builder.AppendLine("MIXINS:" + string.Join(",", typeModel.MixinTypes.OrderBy(x => x.Id).Select(x => x.Id))); - foreach (var prop in typeModel.Properties.OrderBy(x => x.Alias)) - { - builder.AppendLine("--- PROPERTY ---"); - builder.AppendLine(prop.Alias); - builder.AppendLine(prop.ClrName); - builder.AppendLine(prop.Name); - builder.AppendLine(prop.Description); - builder.AppendLine(prop.ModelClrType.ToString()); // see ModelType tests, want ToString() not FullName - } + foreach (PropertyModel prop in typeModel.Properties.OrderBy(x => x.Alias)) + { + builder.AppendLine("--- PROPERTY ---"); + builder.AppendLine(prop.Alias); + builder.AppendLine(prop.ClrName); + builder.AppendLine(prop.Name); + builder.AppendLine(prop.Description); + builder.AppendLine(prop.ModelClrType.ToString()); // see ModelType tests, want ToString() not FullName } + } - // Include the MB version in the hash so that if the MB version changes, models are rebuilt - builder.AppendLine(ApiVersion.Current.Version.ToString()); + // Include the MB version in the hash so that if the MB version changes, models are rebuilt + builder.AppendLine(ApiVersion.Current.Version.ToString()); - return builder.ToString().GenerateHash(); - } + return builder.ToString().GenerateHash(); } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/ImplementPropertyTypeAttribute.cs b/src/Umbraco.Infrastructure/ModelsBuilder/ImplementPropertyTypeAttribute.cs index 474bea9251f3..53c70ef8ac95 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/ImplementPropertyTypeAttribute.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/ImplementPropertyTypeAttribute.cs @@ -1,16 +1,13 @@ -using System; +namespace Umbraco.Cms.Infrastructure.ModelsBuilder; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder +/// +/// Indicates that a property implements a given property alias. +/// +/// And therefore it should not be generated. +[AttributeUsage(AttributeTargets.Property /*, AllowMultiple = false, Inherited = false*/)] +public class ImplementPropertyTypeAttribute : Attribute { - /// - /// Indicates that a property implements a given property alias. - /// - /// And therefore it should not be generated. - [AttributeUsage(AttributeTargets.Property /*, AllowMultiple = false, Inherited = false*/)] - public class ImplementPropertyTypeAttribute : Attribute - { - public ImplementPropertyTypeAttribute(string alias) => Alias = alias; + public ImplementPropertyTypeAttribute(string alias) => Alias = alias; - public string Alias { get; } - } + public string Alias { get; } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/ModelsBuilderAssemblyAttribute.cs b/src/Umbraco.Infrastructure/ModelsBuilder/ModelsBuilderAssemblyAttribute.cs index f016a3ecd288..073f72c6ada6 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/ModelsBuilderAssemblyAttribute.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/ModelsBuilderAssemblyAttribute.cs @@ -1,23 +1,20 @@ -using System; +namespace Umbraco.Cms.Infrastructure.ModelsBuilder; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder +/// +/// Indicates that an Assembly is a Models Builder assembly. +/// +[AttributeUsage(AttributeTargets.Assembly /*, AllowMultiple = false, Inherited = false*/)] +public sealed class ModelsBuilderAssemblyAttribute : Attribute { /// - /// Indicates that an Assembly is a Models Builder assembly. + /// Gets or sets a value indicating whether the assembly is a InMemory assembly. /// - [AttributeUsage(AttributeTargets.Assembly /*, AllowMultiple = false, Inherited = false*/)] - public sealed class ModelsBuilderAssemblyAttribute : Attribute - { - /// - /// Gets or sets a value indicating whether the assembly is a InMemory assembly. - /// - /// A Models Builder assembly can be either InMemory or a normal Dll. - public bool IsInMemory { get; set; } + /// A Models Builder assembly can be either InMemory or a normal Dll. + public bool IsInMemory { get; set; } - /// - /// Gets or sets a hash value representing the state of the custom source code files - /// and the Umbraco content types that were used to generate and compile the assembly. - /// - public string? SourceHash { get; set; } - } + /// + /// Gets or sets a hash value representing the state of the custom source code files + /// and the Umbraco content types that were used to generate and compile the assembly. + /// + public string? SourceHash { get; set; } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/ModelsGenerationError.cs b/src/Umbraco.Infrastructure/ModelsBuilder/ModelsGenerationError.cs index b4210429286c..02db02afda02 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/ModelsGenerationError.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/ModelsGenerationError.cs @@ -1,87 +1,84 @@ -using System; -using System.IO; using System.Text; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder +namespace Umbraco.Cms.Infrastructure.ModelsBuilder; + +public sealed class ModelsGenerationError { - public sealed class ModelsGenerationError + private readonly IHostingEnvironment _hostingEnvironment; + private ModelsBuilderSettings _config; + + /// + /// Initializes a new instance of the class. + /// + public ModelsGenerationError(IOptionsMonitor config, IHostingEnvironment hostingEnvironment) { - private ModelsBuilderSettings _config; - private readonly IHostingEnvironment _hostingEnvironment; + _config = config.CurrentValue; + _hostingEnvironment = hostingEnvironment; + config.OnChange(x => _config = x); + } - /// - /// Initializes a new instance of the class. - /// - public ModelsGenerationError(IOptionsMonitor config, IHostingEnvironment hostingEnvironment) + public void Clear() + { + var errFile = GetErrFile(); + if (errFile == null) { - _config = config.CurrentValue; - _hostingEnvironment = hostingEnvironment; - config.OnChange(x => _config = x); + return; } - public void Clear() - { - var errFile = GetErrFile(); - if (errFile == null) - { - return; - } + // "If the file to be deleted does not exist, no exception is thrown." + File.Delete(errFile); + } - // "If the file to be deleted does not exist, no exception is thrown." - File.Delete(errFile); + public void Report(string message, Exception e) + { + var errFile = GetErrFile(); + if (errFile == null) + { + return; } - public void Report(string message, Exception e) - { - var errFile = GetErrFile(); - if (errFile == null) - { - return; - } + var sb = new StringBuilder(); + sb.Append(message); + sb.Append("\r\n"); + sb.Append(e.Message); + sb.Append("\r\n\r\n"); + sb.Append(e.StackTrace); + sb.Append("\r\n"); - var sb = new StringBuilder(); - sb.Append(message); - sb.Append("\r\n"); - sb.Append(e.Message); - sb.Append("\r\n\r\n"); - sb.Append(e.StackTrace); - sb.Append("\r\n"); + File.WriteAllText(errFile, sb.ToString()); + } - File.WriteAllText(errFile, sb.ToString()); + public string? GetLastError() + { + var errFile = GetErrFile(); + if (errFile == null) + { + return null; } - public string? GetLastError() + try { - var errFile = GetErrFile(); - if (errFile == null) - { - return null; - } - - try - { - return File.ReadAllText(errFile); - } - catch - { - // accepted - return null; - } + return File.ReadAllText(errFile); } - - private string? GetErrFile() + catch { - var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); - if (!Directory.Exists(modelsDirectory)) - { - return null; - } + // accepted + return null; + } + } - return Path.Combine(modelsDirectory, "models.err"); + private string? GetErrFile() + { + var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); + if (!Directory.Exists(modelsDirectory)) + { + return null; } + + return Path.Combine(modelsDirectory, "models.err"); } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/OutOfDateModelsStatus.cs b/src/Umbraco.Infrastructure/ModelsBuilder/OutOfDateModelsStatus.cs index 1d9ea7d4999a..819dd6dcba3e 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/OutOfDateModelsStatus.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/OutOfDateModelsStatus.cs @@ -1,102 +1,99 @@ -using System.IO; using Microsoft.Extensions.Options; -using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Notifications; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder +namespace Umbraco.Cms.Infrastructure.ModelsBuilder; + +/// +/// Used to track if ModelsBuilder models are out of date/stale +/// +public sealed class OutOfDateModelsStatus : INotificationHandler, + INotificationHandler { + private readonly IHostingEnvironment _hostingEnvironment; + private ModelsBuilderSettings _config; + /// - /// Used to track if ModelsBuilder models are out of date/stale + /// Initializes a new instance of the class. /// - public sealed class OutOfDateModelsStatus : INotificationHandler, - INotificationHandler + public OutOfDateModelsStatus(IOptionsMonitor config, IHostingEnvironment hostingEnvironment) { - private ModelsBuilderSettings _config; - private readonly IHostingEnvironment _hostingEnvironment; - - /// - /// Initializes a new instance of the class. - /// - public OutOfDateModelsStatus(IOptionsMonitor config, IHostingEnvironment hostingEnvironment) - { - _config = config.CurrentValue; - _hostingEnvironment = hostingEnvironment; - config.OnChange(x => _config = x); - } + _config = config.CurrentValue; + _hostingEnvironment = hostingEnvironment; + config.OnChange(x => _config = x); + } - /// - /// Gets a value indicating whether flagging out of date models is enabled - /// - public bool IsEnabled => _config.FlagOutOfDateModels; + /// + /// Gets a value indicating whether flagging out of date models is enabled + /// + public bool IsEnabled => _config.FlagOutOfDateModels; - /// - /// Gets a value indicating whether models are out of date - /// - public bool IsOutOfDate + /// + /// Gets a value indicating whether models are out of date + /// + public bool IsOutOfDate + { + get { - get + if (_config.FlagOutOfDateModels == false) { - if (_config.FlagOutOfDateModels == false) - { - return false; - } - - var path = GetFlagPath(); - return path != null && File.Exists(path); + return false; } + + var path = GetFlagPath(); + return path != null && File.Exists(path); } + } + public void Handle(ContentTypeCacheRefresherNotification notification) => Write(); - private string GetFlagPath() - { - var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); - if (!Directory.Exists(modelsDirectory)) - { - Directory.CreateDirectory(modelsDirectory); - } + public void Handle(DataTypeCacheRefresherNotification notification) => Write(); - return Path.Combine(modelsDirectory, "ood.flag"); - } - private void Write() + private string GetFlagPath() + { + var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); + if (!Directory.Exists(modelsDirectory)) { - // don't run if not configured - if (!IsEnabled) - { - return; - } + Directory.CreateDirectory(modelsDirectory); + } - var path = GetFlagPath(); - if (path == null || File.Exists(path)) - { - return; - } + return Path.Combine(modelsDirectory, "ood.flag"); + } - File.WriteAllText(path, "THIS FILE INDICATES THAT MODELS ARE OUT-OF-DATE\n\n"); + private void Write() + { + // don't run if not configured + if (!IsEnabled) + { + return; } - public void Clear() + var path = GetFlagPath(); + if (path == null || File.Exists(path)) { - if (_config.FlagOutOfDateModels == false) - { - return; - } + return; + } - var path = GetFlagPath(); - if (path == null || !File.Exists(path)) - { - return; - } + File.WriteAllText(path, "THIS FILE INDICATES THAT MODELS ARE OUT-OF-DATE\n\n"); + } - File.Delete(path); + public void Clear() + { + if (_config.FlagOutOfDateModels == false) + { + return; } - public void Handle(ContentTypeCacheRefresherNotification notification) => Write(); + var path = GetFlagPath(); + if (path == null || !File.Exists(path)) + { + return; + } - public void Handle(DataTypeCacheRefresherNotification notification) => Write(); + File.Delete(path); } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/PublishedElementExtensions.cs b/src/Umbraco.Infrastructure/ModelsBuilder/PublishedElementExtensions.cs index 85d953da3abb..87ed2b0538fe 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/PublishedElementExtensions.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/PublishedElementExtensions.cs @@ -1,4 +1,3 @@ -using System; using System.Linq.Expressions; using System.Reflection; using Umbraco.Cms.Core.Models.PublishedContent; @@ -6,46 +5,55 @@ // same namespace as original Umbraco.Web PublishedElementExtensions // ReSharper disable once CheckNamespace -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods to models. +/// +public static class PublishedElementExtensions { /// - /// Provides extension methods to models. + /// Gets the value of a property. /// - public static class PublishedElementExtensions + public static TValue? ValueFor(this TModel model, IPublishedValueFallback publishedValueFallback, + Expression> property, string? culture = null, string? segment = null, + Fallback fallback = default, TValue? defaultValue = default) + where TModel : IPublishedElement { - /// - /// Gets the value of a property. - /// - public static TValue? ValueFor(this TModel model, IPublishedValueFallback publishedValueFallback, Expression> property, string? culture = null, string? segment = null, Fallback fallback = default, TValue? defaultValue = default) - where TModel : IPublishedElement - { - var alias = GetAlias(model, property); - return model.Value(publishedValueFallback, alias, culture, segment, fallback, defaultValue); - } + var alias = GetAlias(model, property); + return model.Value(publishedValueFallback, alias, culture, segment, fallback, defaultValue); + } - // fixme that one should be public so ppl can use it - private static string GetAlias(TModel model, Expression> property) + // fixme that one should be public so ppl can use it + private static string GetAlias(TModel model, Expression> property) + { + if (property.NodeType != ExpressionType.Lambda) { - if (property.NodeType != ExpressionType.Lambda) - throw new ArgumentException("Not a proper lambda expression (lambda).", nameof(property)); - - var lambda = (LambdaExpression) property; - var lambdaBody = lambda.Body; + throw new ArgumentException("Not a proper lambda expression (lambda).", nameof(property)); + } - if (lambdaBody.NodeType != ExpressionType.MemberAccess) - throw new ArgumentException("Not a proper lambda expression (body).", nameof(property)); + var lambda = (LambdaExpression)property; + Expression lambdaBody = lambda.Body; - var memberExpression = (MemberExpression) lambdaBody; - if (memberExpression.Expression?.NodeType != ExpressionType.Parameter) - throw new ArgumentException("Not a proper lambda expression (member).", nameof(property)); + if (lambdaBody.NodeType != ExpressionType.MemberAccess) + { + throw new ArgumentException("Not a proper lambda expression (body).", nameof(property)); + } - var member = memberExpression.Member; + var memberExpression = (MemberExpression)lambdaBody; + if (memberExpression.Expression?.NodeType != ExpressionType.Parameter) + { + throw new ArgumentException("Not a proper lambda expression (member).", nameof(property)); + } - var attribute = member.GetCustomAttribute(); - if (attribute == null) - throw new InvalidOperationException("Property is not marked with ImplementPropertyType attribute."); + MemberInfo member = memberExpression.Member; - return attribute.Alias; + ImplementPropertyTypeAttribute? attribute = member.GetCustomAttribute(); + if (attribute == null) + { + throw new InvalidOperationException("Property is not marked with ImplementPropertyType attribute."); } + + return attribute.Alias; } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/PublishedModelUtility.cs b/src/Umbraco.Infrastructure/ModelsBuilder/PublishedModelUtility.cs index b782751dd833..ad74d29d0c66 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/PublishedModelUtility.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/PublishedModelUtility.cs @@ -1,74 +1,78 @@ -using System; -using System.Linq; using System.Linq.Expressions; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder +namespace Umbraco.Cms.Infrastructure.ModelsBuilder; + +/// +/// This is called from within the generated model classes +/// +/// +/// DO NOT REMOVE - although there are not code references this is used directly by the generated models. +/// +public static class PublishedModelUtility { - /// - /// This is called from within the generated model classes - /// - /// - /// DO NOT REMOVE - although there are not code references this is used directly by the generated models. - /// - public static class PublishedModelUtility - { - // looks safer but probably useless... ppl should not call these methods directly - // and if they do... they have to take care about not doing stupid things + // looks safer but probably useless... ppl should not call these methods directly + // and if they do... they have to take care about not doing stupid things - //public static PublishedPropertyType GetModelPropertyType2(Expression> selector) - // where T : PublishedContentModel - //{ - // var type = typeof (T); - // var s1 = type.GetField("ModelTypeAlias", BindingFlags.Public | BindingFlags.Static); - // var alias = (s1.IsLiteral && s1.IsInitOnly && s1.FieldType == typeof(string)) ? (string)s1.GetValue(null) : null; - // var s2 = type.GetField("ModelItemType", BindingFlags.Public | BindingFlags.Static); - // var itemType = (s2.IsLiteral && s2.IsInitOnly && s2.FieldType == typeof(PublishedItemType)) ? (PublishedItemType)s2.GetValue(null) : 0; + //public static PublishedPropertyType GetModelPropertyType2(Expression> selector) + // where T : PublishedContentModel + //{ + // var type = typeof (T); + // var s1 = type.GetField("ModelTypeAlias", BindingFlags.Public | BindingFlags.Static); + // var alias = (s1.IsLiteral && s1.IsInitOnly && s1.FieldType == typeof(string)) ? (string)s1.GetValue(null) : null; + // var s2 = type.GetField("ModelItemType", BindingFlags.Public | BindingFlags.Static); + // var itemType = (s2.IsLiteral && s2.IsInitOnly && s2.FieldType == typeof(PublishedItemType)) ? (PublishedItemType)s2.GetValue(null) : 0; - // var contentType = PublishedContentType.Get(itemType, alias); - // // etc... - //} + // var contentType = PublishedContentType.Get(itemType, alias); + // // etc... + //} - public static IPublishedContentType? GetModelContentType(IPublishedSnapshotAccessor publishedSnapshotAccessor, PublishedItemType itemType, string alias) + public static IPublishedContentType? GetModelContentType(IPublishedSnapshotAccessor publishedSnapshotAccessor, + PublishedItemType itemType, string alias) + { + IPublishedSnapshot publishedSnapshot = publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); + switch (itemType) { - var publishedSnapshot = publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); - switch (itemType) - { - case PublishedItemType.Content: - return publishedSnapshot.Content?.GetContentType(alias); - case PublishedItemType.Media: - return publishedSnapshot.Media?.GetContentType(alias); - case PublishedItemType.Member: - return publishedSnapshot.Members?.GetContentType(alias); - default: - throw new ArgumentOutOfRangeException(nameof(itemType)); - } + case PublishedItemType.Content: + return publishedSnapshot.Content?.GetContentType(alias); + case PublishedItemType.Media: + return publishedSnapshot.Media?.GetContentType(alias); + case PublishedItemType.Member: + return publishedSnapshot.Members?.GetContentType(alias); + default: + throw new ArgumentOutOfRangeException(nameof(itemType)); } + } - public static IPublishedPropertyType? GetModelPropertyType(IPublishedContentType contentType, Expression> selector) + public static IPublishedPropertyType? GetModelPropertyType(IPublishedContentType contentType, + Expression> selector) //where TModel : PublishedContentModel // fixme PublishedContentModel _or_ PublishedElementModel - { - // fixme therefore, missing a check on TModel here - - var expr = selector.Body as MemberExpression; + { + // fixme therefore, missing a check on TModel here - if (expr == null) - throw new ArgumentException("Not a property expression.", nameof(selector)); + var expr = selector.Body as MemberExpression; - // there _is_ a risk that contentType and T do not match - // see note above : accepted risk... + if (expr == null) + { + throw new ArgumentException("Not a property expression.", nameof(selector)); + } - var attr = expr.Member - .GetCustomAttributes(typeof(ImplementPropertyTypeAttribute), false) - .OfType() - .SingleOrDefault(); + // there _is_ a risk that contentType and T do not match + // see note above : accepted risk... - if (string.IsNullOrWhiteSpace(attr?.Alias)) - throw new InvalidOperationException($"Could not figure out property alias for property \"{expr.Member.Name}\"."); + ImplementPropertyTypeAttribute? attr = expr.Member + .GetCustomAttributes(typeof(ImplementPropertyTypeAttribute), false) + .OfType() + .SingleOrDefault(); - return contentType.GetPropertyType(attr.Alias); + if (string.IsNullOrWhiteSpace(attr?.Alias)) + { + throw new InvalidOperationException( + $"Could not figure out property alias for property \"{expr.Member.Name}\"."); } + + return contentType.GetPropertyType(attr.Alias); } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/RoslynCompiler.cs b/src/Umbraco.Infrastructure/ModelsBuilder/RoslynCompiler.cs index fd4b4495d905..e378560ee61c 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/RoslynCompiler.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/RoslynCompiler.cs @@ -1,80 +1,78 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Emit; using Microsoft.CodeAnalysis.Text; using Microsoft.Extensions.DependencyModel; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder +namespace Umbraco.Cms.Infrastructure.ModelsBuilder; + +public class RoslynCompiler { - public class RoslynCompiler - { - public const string GeneratedAssemblyName = "ModelsGeneratedAssembly"; + public const string GeneratedAssemblyName = "ModelsGeneratedAssembly"; - private readonly OutputKind _outputKind; - private readonly CSharpParseOptions _parseOptions; - private readonly IEnumerable _refs; + private readonly OutputKind _outputKind; + private readonly CSharpParseOptions _parseOptions; + private readonly IEnumerable _refs; - /// - /// Initializes a new instance of the class. - /// - /// - /// Roslyn compiler which can be used to compile a c# file to a Dll assembly - /// - public RoslynCompiler() - { - _outputKind = OutputKind.DynamicallyLinkedLibrary; - _parseOptions = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Latest); // What languageversion should we default to? + /// + /// Initializes a new instance of the class. + /// + /// + /// Roslyn compiler which can be used to compile a c# file to a Dll assembly + /// + public RoslynCompiler() + { + _outputKind = OutputKind.DynamicallyLinkedLibrary; + _parseOptions = + CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion + .Latest); // What languageversion should we default to? - // In order to dynamically compile the assembly, we need to add all refs from our current - // application. This will also add the correct framework dependencies and we won't have to worry - // about the specific framework that is currently being run. - // This was borrowed from: https://github.com/dotnet/core/issues/2082#issuecomment-442713181 - // because we were running into the same error as that thread because we were either: - // - not adding enough of the runtime dependencies OR - // - we were explicitly adding the wrong runtime dependencies - // ... at least that the gist of what I can tell. - MetadataReference[] refs = - DependencyContext.Default.CompileLibraries + // In order to dynamically compile the assembly, we need to add all refs from our current + // application. This will also add the correct framework dependencies and we won't have to worry + // about the specific framework that is currently being run. + // This was borrowed from: https://github.com/dotnet/core/issues/2082#issuecomment-442713181 + // because we were running into the same error as that thread because we were either: + // - not adding enough of the runtime dependencies OR + // - we were explicitly adding the wrong runtime dependencies + // ... at least that the gist of what I can tell. + MetadataReference[] refs = + DependencyContext.Default.CompileLibraries .SelectMany(cl => cl.ResolveReferencePaths()) .Select(asm => MetadataReference.CreateFromFile(asm)) .ToArray(); - _refs = refs.ToList(); - } + _refs = refs.ToList(); + } - /// - /// Compile a source file to a dll - /// - /// Path to the source file containing the code to be compiled. - /// The path where the output assembly will be saved. - public void CompileToFile(string pathToSourceFile, string savePath) - { - var sourceCode = File.ReadAllText(pathToSourceFile); + /// + /// Compile a source file to a dll + /// + /// Path to the source file containing the code to be compiled. + /// The path where the output assembly will be saved. + public void CompileToFile(string pathToSourceFile, string savePath) + { + var sourceCode = File.ReadAllText(pathToSourceFile); - var sourceText = SourceText.From(sourceCode); + var sourceText = SourceText.From(sourceCode); - var syntaxTree = SyntaxFactory.ParseSyntaxTree(sourceText, _parseOptions); + SyntaxTree syntaxTree = SyntaxFactory.ParseSyntaxTree(sourceText, _parseOptions); - var compilation = CSharpCompilation.Create( - GeneratedAssemblyName, - new[] { syntaxTree }, - references: _refs, - options: new CSharpCompilationOptions( - _outputKind, - optimizationLevel: OptimizationLevel.Release, - // Not entirely certain that assemblyIdentityComparer is nececary? - assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default)); + var compilation = CSharpCompilation.Create( + GeneratedAssemblyName, + new[] {syntaxTree}, + _refs, + new CSharpCompilationOptions( + _outputKind, + optimizationLevel: OptimizationLevel.Release, + // Not entirely certain that assemblyIdentityComparer is nececary? + assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default)); - var emitResult = compilation.Emit(savePath); + EmitResult emitResult = compilation.Emit(savePath); - if (!emitResult.Success) - { - throw new InvalidOperationException("Roslyn compiler could not create ModelsBuilder dll:\n" + - string.Join("\n", emitResult.Diagnostics.Select(x=>x.GetMessage()))); - } + if (!emitResult.Success) + { + throw new InvalidOperationException("Roslyn compiler could not create ModelsBuilder dll:\n" + + string.Join("\n", emitResult.Diagnostics.Select(x => x.GetMessage()))); } } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/TypeExtensions.cs b/src/Umbraco.Infrastructure/ModelsBuilder/TypeExtensions.cs index 5d3187c707ba..7f8b02928470 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/TypeExtensions.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/TypeExtensions.cs @@ -1,22 +1,21 @@ -using System; +namespace Umbraco.Cms.Infrastructure.ModelsBuilder; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder +internal static class TypeExtensions { - internal static class TypeExtensions + /// + /// Creates a generic instance of a generic type with the proper actual type of an object. + /// + /// A generic type such as Something{} + /// An object whose type is used as generic type param. + /// Arguments for the constructor. + /// A generic instance of the generic type with the proper type. + /// + /// Usage... typeof (Something{}).CreateGenericInstance(object1, object2, object3) will return + /// a Something{Type1} if object1.GetType() is Type1. + /// + public static object? CreateGenericInstance(this Type genericType, object typeParmObj, params object[] ctorArgs) { - /// - /// Creates a generic instance of a generic type with the proper actual type of an object. - /// - /// A generic type such as Something{} - /// An object whose type is used as generic type param. - /// Arguments for the constructor. - /// A generic instance of the generic type with the proper type. - /// Usage... typeof (Something{}).CreateGenericInstance(object1, object2, object3) will return - /// a Something{Type1} if object1.GetType() is Type1. - public static object? CreateGenericInstance(this Type genericType, object typeParmObj, params object[] ctorArgs) - { - var type = genericType.MakeGenericType(typeParmObj.GetType()); - return Activator.CreateInstance(type, ctorArgs); - } + Type type = genericType.MakeGenericType(typeParmObj.GetType()); + return Activator.CreateInstance(type, ctorArgs); } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/UmbracoServices.cs b/src/Umbraco.Infrastructure/ModelsBuilder/UmbracoServices.cs index 8d096ee9e2cf..c6ab19b24741 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/UmbracoServices.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/UmbracoServices.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Exceptions; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; @@ -9,200 +6,232 @@ using Umbraco.Cms.Infrastructure.ModelsBuilder.Building; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder +namespace Umbraco.Cms.Infrastructure.ModelsBuilder; + +public sealed class UmbracoServices { + private readonly IContentTypeService _contentTypeService; + private readonly IMediaTypeService _mediaTypeService; + private readonly IMemberTypeService _memberTypeService; + private readonly IPublishedContentTypeFactory _publishedContentTypeFactory; + private readonly IShortStringHelper _shortStringHelper; + + /// + /// Initializes a new instance of the class. + /// + public UmbracoServices( + IContentTypeService contentTypeService, + IMediaTypeService mediaTypeService, + IMemberTypeService memberTypeService, + IPublishedContentTypeFactory publishedContentTypeFactory, + IShortStringHelper shortStringHelper) + { + _contentTypeService = contentTypeService; + _mediaTypeService = mediaTypeService; + _memberTypeService = memberTypeService; + _publishedContentTypeFactory = publishedContentTypeFactory; + _shortStringHelper = shortStringHelper; + } + + #region Services - public sealed class UmbracoServices + public IList GetAllTypes() { - private readonly IContentTypeService _contentTypeService; - private readonly IMediaTypeService _mediaTypeService; - private readonly IMemberTypeService _memberTypeService; - private readonly IPublishedContentTypeFactory _publishedContentTypeFactory; - private readonly IShortStringHelper _shortStringHelper; - - /// - /// Initializes a new instance of the class. - /// - public UmbracoServices( - IContentTypeService contentTypeService, - IMediaTypeService mediaTypeService, - IMemberTypeService memberTypeService, - IPublishedContentTypeFactory publishedContentTypeFactory, - IShortStringHelper shortStringHelper) - { - _contentTypeService = contentTypeService; - _mediaTypeService = mediaTypeService; - _memberTypeService = memberTypeService; - _publishedContentTypeFactory = publishedContentTypeFactory; - _shortStringHelper = shortStringHelper; - } + var types = new List(); + + // TODO: this will require 3 rather large SQL queries on startup in ModelsMode.InMemoryAuto mode. I know that these will be cached after lookup but it will slow + // down startup time ... BUT these queries are also used in NuCache on startup so we can't really avoid them. Maybe one day we can + // load all of these in in one query and still have them cached per service, and/or somehow improve the perf of these since they are used on startup + // in more than one place. + types.AddRange(GetTypes(PublishedItemType.Content, + _contentTypeService.GetAll().Cast().ToArray())); + types.AddRange(GetTypes(PublishedItemType.Media, + _mediaTypeService.GetAll().Cast().ToArray())); + types.AddRange(GetTypes(PublishedItemType.Member, + _memberTypeService.GetAll().Cast().ToArray())); + + return EnsureDistinctAliases(types); + } - #region Services + public IList GetContentTypes() + { + IContentTypeComposition[] contentTypes = _contentTypeService.GetAll().Cast().ToArray(); + return GetTypes(PublishedItemType.Content, contentTypes); // aliases have to be unique here + } - public IList GetAllTypes() - { - var types = new List(); + public IList GetMediaTypes() + { + IContentTypeComposition[] contentTypes = _mediaTypeService.GetAll().Cast().ToArray(); + return GetTypes(PublishedItemType.Media, contentTypes); // aliases have to be unique here + } - // TODO: this will require 3 rather large SQL queries on startup in ModelsMode.InMemoryAuto mode. I know that these will be cached after lookup but it will slow - // down startup time ... BUT these queries are also used in NuCache on startup so we can't really avoid them. Maybe one day we can - // load all of these in in one query and still have them cached per service, and/or somehow improve the perf of these since they are used on startup - // in more than one place. - types.AddRange(GetTypes(PublishedItemType.Content, _contentTypeService.GetAll().Cast().ToArray())); - types.AddRange(GetTypes(PublishedItemType.Media, _mediaTypeService.GetAll().Cast().ToArray())); - types.AddRange(GetTypes(PublishedItemType.Member, _memberTypeService.GetAll().Cast().ToArray())); + public IList GetMemberTypes() + { + IContentTypeComposition[] memberTypes = _memberTypeService.GetAll().Cast().ToArray(); + return GetTypes(PublishedItemType.Member, memberTypes); // aliases have to be unique here + } - return EnsureDistinctAliases(types); - } + public static string GetClrName(IShortStringHelper shortStringHelper, string? name, string alias) => + // ModelsBuilder's legacy - but not ideal + alias.ToCleanString(shortStringHelper, CleanStringType.ConvertCase | CleanStringType.PascalCase); - public IList GetContentTypes() - { - var contentTypes = _contentTypeService.GetAll().Cast().ToArray(); - return GetTypes(PublishedItemType.Content, contentTypes); // aliases have to be unique here - } + private IList GetTypes(PublishedItemType itemType, IContentTypeComposition[] contentTypes) + { + var typeModels = new List(); + var uniqueTypes = new HashSet(); - public IList GetMediaTypes() + // get the types and the properties + foreach (IContentTypeComposition contentType in contentTypes) { - var contentTypes = _mediaTypeService.GetAll().Cast().ToArray(); - return GetTypes(PublishedItemType.Media, contentTypes); // aliases have to be unique here - } + var typeModel = new TypeModel + { + Id = contentType.Id, + Alias = contentType.Alias, + ClrName = GetClrName(_shortStringHelper, contentType.Name, contentType.Alias), + ParentId = contentType.ParentId, + Name = contentType.Name, + Description = contentType.Description + }; + + // of course this should never happen, but when it happens, better detect it + // else we end up with weird nullrefs everywhere + if (uniqueTypes.Contains(typeModel.ClrName)) + { + throw new PanicException($"Panic: duplicate type ClrName \"{typeModel.ClrName}\"."); + } - public IList GetMemberTypes() - { - var memberTypes = _memberTypeService.GetAll().Cast().ToArray(); - return GetTypes(PublishedItemType.Member, memberTypes); // aliases have to be unique here - } + uniqueTypes.Add(typeModel.ClrName); - public static string GetClrName(IShortStringHelper shortStringHelper, string? name, string alias) - { - // ModelsBuilder's legacy - but not ideal - return alias.ToCleanString(shortStringHelper, CleanStringType.ConvertCase | CleanStringType.PascalCase); - } + IPublishedContentType publishedContentType = _publishedContentTypeFactory.CreateContentType(contentType); + switch (itemType) + { + case PublishedItemType.Content: + typeModel.ItemType = publishedContentType.ItemType == PublishedItemType.Element + ? TypeModel.ItemTypes.Element + : TypeModel.ItemTypes.Content; + break; + case PublishedItemType.Media: + typeModel.ItemType = publishedContentType.ItemType == PublishedItemType.Element + ? TypeModel.ItemTypes.Element + : TypeModel.ItemTypes.Media; + break; + case PublishedItemType.Member: + typeModel.ItemType = publishedContentType.ItemType == PublishedItemType.Element + ? TypeModel.ItemTypes.Element + : TypeModel.ItemTypes.Member; + break; + default: + throw new InvalidOperationException(string.Format("Unsupported PublishedItemType \"{0}\".", + itemType)); + } - private IList GetTypes(PublishedItemType itemType, IContentTypeComposition[] contentTypes) - { - var typeModels = new List(); - var uniqueTypes = new HashSet(); + typeModels.Add(typeModel); - // get the types and the properties - foreach (var contentType in contentTypes) + foreach (IPropertyType propertyType in contentType.PropertyTypes) { - var typeModel = new TypeModel + var propertyModel = new PropertyModel { - Id = contentType.Id, - Alias = contentType.Alias, - ClrName = GetClrName(_shortStringHelper, contentType.Name, contentType.Alias), - ParentId = contentType.ParentId, - - Name = contentType.Name, - Description = contentType.Description + Alias = propertyType.Alias, + ClrName = GetClrName(_shortStringHelper, propertyType.Name, propertyType.Alias), + Name = propertyType.Name, + Description = propertyType.Description }; - // of course this should never happen, but when it happens, better detect it - // else we end up with weird nullrefs everywhere - if (uniqueTypes.Contains(typeModel.ClrName)) - throw new PanicException($"Panic: duplicate type ClrName \"{typeModel.ClrName}\"."); - uniqueTypes.Add(typeModel.ClrName); - - var publishedContentType = _publishedContentTypeFactory.CreateContentType(contentType); - switch (itemType) + IPublishedPropertyType? publishedPropertyType = + publishedContentType.GetPropertyType(propertyType.Alias); + if (publishedPropertyType == null) { - case PublishedItemType.Content: - typeModel.ItemType = publishedContentType.ItemType == PublishedItemType.Element - ? TypeModel.ItemTypes.Element - : TypeModel.ItemTypes.Content; - break; - case PublishedItemType.Media: - typeModel.ItemType = publishedContentType.ItemType == PublishedItemType.Element - ? TypeModel.ItemTypes.Element - : TypeModel.ItemTypes.Media; - break; - case PublishedItemType.Member: - typeModel.ItemType = publishedContentType.ItemType == PublishedItemType.Element - ? TypeModel.ItemTypes.Element - : TypeModel.ItemTypes.Member; - break; - default: - throw new InvalidOperationException(string.Format("Unsupported PublishedItemType \"{0}\".", itemType)); + throw new PanicException( + $"Panic: could not get published property type {contentType.Alias}.{propertyType.Alias}."); } - typeModels.Add(typeModel); + propertyModel.ModelClrType = publishedPropertyType.ModelClrType; - foreach (var propertyType in contentType.PropertyTypes) - { - var propertyModel = new PropertyModel - { - Alias = propertyType.Alias, - ClrName = GetClrName(_shortStringHelper, propertyType.Name, propertyType.Alias), - - Name = propertyType.Name, - Description = propertyType.Description - }; - - var publishedPropertyType = publishedContentType.GetPropertyType(propertyType.Alias); - if (publishedPropertyType == null) - throw new PanicException($"Panic: could not get published property type {contentType.Alias}.{propertyType.Alias}."); + typeModel.Properties.Add(propertyModel); + } + } - propertyModel.ModelClrType = publishedPropertyType.ModelClrType; + // wire the base types + foreach (TypeModel typeModel in typeModels.Where(x => x.ParentId > 0)) + { + typeModel.BaseType = typeModels.SingleOrDefault(x => x.Id == typeModel.ParentId); + // Umbraco 7.4 introduces content types containers, so even though ParentId > 0, the parent might + // not be a content type - here we assume that BaseType being null while ParentId > 0 means that + // the parent is a container (and we don't check). + typeModel.IsParent = typeModel.BaseType != null; + } - typeModel.Properties.Add(propertyModel); - } + // discover mixins + foreach (IContentTypeComposition contentType in contentTypes) + { + TypeModel? typeModel = typeModels.SingleOrDefault(x => x.Id == contentType.Id); + if (typeModel == null) + { + throw new PanicException("Panic: no type model matching content type."); } - // wire the base types - foreach (var typeModel in typeModels.Where(x => x.ParentId > 0)) + IEnumerable compositionTypes; + var contentTypeAsMedia = contentType as IMediaType; + var contentTypeAsContent = contentType as IContentType; + var contentTypeAsMember = contentType as IMemberType; + if (contentTypeAsMedia != null) + { + compositionTypes = contentTypeAsMedia.ContentTypeComposition; + } + else if (contentTypeAsContent != null) + { + compositionTypes = contentTypeAsContent.ContentTypeComposition; + } + else if (contentTypeAsMember != null) { - typeModel.BaseType = typeModels.SingleOrDefault(x => x.Id == typeModel.ParentId); - // Umbraco 7.4 introduces content types containers, so even though ParentId > 0, the parent might - // not be a content type - here we assume that BaseType being null while ParentId > 0 means that - // the parent is a container (and we don't check). - typeModel.IsParent = typeModel.BaseType != null; + compositionTypes = contentTypeAsMember.ContentTypeComposition; + } + else + { + throw new PanicException(string.Format("Panic: unsupported type \"{0}\".", + contentType.GetType().FullName)); } - // discover mixins - foreach (var contentType in contentTypes) + foreach (IContentTypeComposition compositionType in compositionTypes) { - var typeModel = typeModels.SingleOrDefault(x => x.Id == contentType.Id); - if (typeModel == null) throw new PanicException("Panic: no type model matching content type."); - - IEnumerable compositionTypes; - var contentTypeAsMedia = contentType as IMediaType; - var contentTypeAsContent = contentType as IContentType; - var contentTypeAsMember = contentType as IMemberType; - if (contentTypeAsMedia != null) compositionTypes = contentTypeAsMedia.ContentTypeComposition; - else if (contentTypeAsContent != null) compositionTypes = contentTypeAsContent.ContentTypeComposition; - else if (contentTypeAsMember != null) compositionTypes = contentTypeAsMember.ContentTypeComposition; - else throw new PanicException(string.Format("Panic: unsupported type \"{0}\".", contentType.GetType().FullName)); - - foreach (var compositionType in compositionTypes) + TypeModel? compositionModel = typeModels.SingleOrDefault(x => x.Id == compositionType.Id); + if (compositionModel == null) { - var compositionModel = typeModels.SingleOrDefault(x => x.Id == compositionType.Id); - if (compositionModel == null) throw new PanicException("Panic: composition type does not exist."); + throw new PanicException("Panic: composition type does not exist."); + } - if (compositionType.Id == contentType.ParentId) continue; + if (compositionType.Id == contentType.ParentId) + { + continue; + } - // add to mixins - typeModel.MixinTypes.Add(compositionModel); + // add to mixins + typeModel.MixinTypes.Add(compositionModel); - // mark as mixin - as well as parents + // mark as mixin - as well as parents + compositionModel.IsMixin = true; + while ((compositionModel = compositionModel.BaseType) != null) + { compositionModel.IsMixin = true; - while ((compositionModel = compositionModel.BaseType) != null) - compositionModel.IsMixin = true; } } - - return typeModels; } - internal static IList EnsureDistinctAliases(IList typeModels) + return typeModels; + } + + internal static IList EnsureDistinctAliases(IList typeModels) + { + IEnumerable> groups = typeModels.GroupBy(x => x.Alias.ToLowerInvariant()); + foreach (IGrouping group in groups.Where(x => x.Count() > 1)) { - var groups = typeModels.GroupBy(x => x.Alias.ToLowerInvariant()); - foreach (var group in groups.Where(x => x.Count() > 1)) - throw new NotSupportedException($"Alias \"{group.Key}\" is used by types" - + $" {string.Join(", ", group.Select(x => x.ItemType + ":\"" + x.Alias + "\""))}. Aliases have to be unique." - + " One of the aliases must be modified in order to use the ModelsBuilder."); - return typeModels; + throw new NotSupportedException($"Alias \"{group.Key}\" is used by types" + + $" {string.Join(", ", group.Select(x => x.ItemType + ":\"" + x.Alias + "\""))}. Aliases have to be unique." + + " One of the aliases must be modified in order to use the ModelsBuilder."); } - #endregion + return typeModels; } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Packaging/AutomaticPackageMigrationPlan.cs b/src/Umbraco.Infrastructure/Packaging/AutomaticPackageMigrationPlan.cs index 92e1086fbd5a..8df3bcd71541 100644 --- a/src/Umbraco.Infrastructure/Packaging/AutomaticPackageMigrationPlan.cs +++ b/src/Umbraco.Infrastructure/Packaging/AutomaticPackageMigrationPlan.cs @@ -1,4 +1,3 @@ -using System; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Packaging; using Umbraco.Cms.Core.PropertyEditors; @@ -7,43 +6,48 @@ using Umbraco.Cms.Infrastructure.Migrations; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Packaging +namespace Umbraco.Cms.Infrastructure.Packaging; + +/// +/// Used to automatically indicate that a package has an embedded package data manifest that needs to be installed +/// +public abstract class AutomaticPackageMigrationPlan : PackageMigrationPlan { - /// - /// Used to automatically indicate that a package has an embedded package data manifest that needs to be installed - /// - public abstract class AutomaticPackageMigrationPlan : PackageMigrationPlan + protected AutomaticPackageMigrationPlan(string packageName) + : this(packageName, packageName) { - protected AutomaticPackageMigrationPlan(string packageName) - : this(packageName, packageName) - { } + } - protected AutomaticPackageMigrationPlan(string packageName, string planName) - : base(packageName, planName) - { } + protected AutomaticPackageMigrationPlan(string packageName, string planName) + : base(packageName, planName) + { + } - protected sealed override void DefinePlan() - { - // calculate the final state based on the hash value of the embedded resource - Type planType = GetType(); - var hash = PackageMigrationResource.GetEmbeddedPackageDataManifestHash(planType); + protected sealed override void DefinePlan() + { + // calculate the final state based on the hash value of the embedded resource + Type planType = GetType(); + var hash = PackageMigrationResource.GetEmbeddedPackageDataManifestHash(planType); - var finalId = hash.ToGuid(); - To(finalId); - } + var finalId = hash.ToGuid(); + To(finalId); + } - private class MigrateToPackageData : PackageMigrationBase + private class MigrateToPackageData : PackageMigrationBase + { + public MigrateToPackageData(IPackagingService packagingService, IMediaService mediaService, + MediaFileManager mediaFileManager, MediaUrlGeneratorCollection mediaUrlGenerators, + IShortStringHelper shortStringHelper, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, + IMigrationContext context) : base(packagingService, mediaService, mediaFileManager, mediaUrlGenerators, + shortStringHelper, contentTypeBaseServiceProvider, context) { - public MigrateToPackageData(IPackagingService packagingService, IMediaService mediaService, MediaFileManager mediaFileManager, MediaUrlGeneratorCollection mediaUrlGenerators, IShortStringHelper shortStringHelper, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, IMigrationContext context) : base(packagingService, mediaService, mediaFileManager, mediaUrlGenerators, shortStringHelper, contentTypeBaseServiceProvider, context) - { - } + } - protected override void Migrate() - { - var plan = (AutomaticPackageMigrationPlan)Context.Plan; + protected override void Migrate() + { + var plan = (AutomaticPackageMigrationPlan)Context.Plan; - ImportPackage.FromEmbeddedResource(plan.GetType()).Do(); - } + ImportPackage.FromEmbeddedResource(plan.GetType()).Do(); } } } diff --git a/src/Umbraco.Infrastructure/Packaging/IImportPackageBuilder.cs b/src/Umbraco.Infrastructure/Packaging/IImportPackageBuilder.cs index 994ac643c61e..f826dd9dfe05 100644 --- a/src/Umbraco.Infrastructure/Packaging/IImportPackageBuilder.cs +++ b/src/Umbraco.Infrastructure/Packaging/IImportPackageBuilder.cs @@ -1,17 +1,15 @@ -using System; using System.Xml.Linq; using Umbraco.Cms.Infrastructure.Migrations.Expressions; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Packaging +namespace Umbraco.Cms.Infrastructure.Packaging; + +public interface IImportPackageBuilder : IFluentBuilder { - public interface IImportPackageBuilder : IFluentBuilder - { - IExecutableBuilder FromEmbeddedResource() - where TPackageMigration : PackageMigrationBase; + IExecutableBuilder FromEmbeddedResource() + where TPackageMigration : PackageMigrationBase; - IExecutableBuilder FromEmbeddedResource(Type packageMigrationType); + IExecutableBuilder FromEmbeddedResource(Type packageMigrationType); - IExecutableBuilder FromXmlDataManifest(XDocument packageDataManifest); - } + IExecutableBuilder FromXmlDataManifest(XDocument packageDataManifest); } diff --git a/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilder.cs b/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilder.cs index fef61a54c3d3..8b28628e4cd9 100644 --- a/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilder.cs +++ b/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilder.cs @@ -1,4 +1,3 @@ -using System; using System.Xml.Linq; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; @@ -10,50 +9,50 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Packaging +namespace Umbraco.Cms.Infrastructure.Packaging; + +internal class ImportPackageBuilder : ExpressionBuilderBase, IImportPackageBuilder, + IExecutableBuilder { - internal class ImportPackageBuilder : ExpressionBuilderBase, IImportPackageBuilder, IExecutableBuilder + public ImportPackageBuilder( + IPackagingService packagingService, + IMediaService mediaService, + MediaFileManager mediaFileManager, + MediaUrlGeneratorCollection mediaUrlGenerators, + IShortStringHelper shortStringHelper, + IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, + IMigrationContext context, + IOptions options) + : base(new ImportPackageBuilderExpression( + packagingService, + mediaService, + mediaFileManager, + mediaUrlGenerators, + shortStringHelper, + contentTypeBaseServiceProvider, + context, + options)) { - public ImportPackageBuilder( - IPackagingService packagingService, - IMediaService mediaService, - MediaFileManager mediaFileManager, - MediaUrlGeneratorCollection mediaUrlGenerators, - IShortStringHelper shortStringHelper, - IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, - IMigrationContext context, - IOptions options) - : base(new ImportPackageBuilderExpression( - packagingService, - mediaService, - mediaFileManager, - mediaUrlGenerators, - shortStringHelper, - contentTypeBaseServiceProvider, - context, - options)) - { - } + } - public void Do() => Expression.Execute(); + public void Do() => Expression.Execute(); - public IExecutableBuilder FromEmbeddedResource() - where TPackageMigration : PackageMigrationBase - { - Expression.EmbeddedResourceMigrationType = typeof(TPackageMigration); - return this; - } + public IExecutableBuilder FromEmbeddedResource() + where TPackageMigration : PackageMigrationBase + { + Expression.EmbeddedResourceMigrationType = typeof(TPackageMigration); + return this; + } - public IExecutableBuilder FromEmbeddedResource(Type packageMigrationType) - { - Expression.EmbeddedResourceMigrationType = packageMigrationType; - return this; - } + public IExecutableBuilder FromEmbeddedResource(Type packageMigrationType) + { + Expression.EmbeddedResourceMigrationType = packageMigrationType; + return this; + } - public IExecutableBuilder FromXmlDataManifest(XDocument packageDataManifest) - { - Expression.PackageDataManifest = packageDataManifest; - return this; - } + public IExecutableBuilder FromXmlDataManifest(XDocument packageDataManifest) + { + Expression.PackageDataManifest = packageDataManifest; + return this; } } diff --git a/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilderExpression.cs b/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilderExpression.cs index 04abcfa8a00c..6ef0a12119b4 100644 --- a/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilderExpression.cs +++ b/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilderExpression.cs @@ -1,7 +1,4 @@ -using System; -using System.IO; using System.IO.Compression; -using System.Linq; using System.Xml.Linq; using System.Xml.XPath; using Microsoft.Extensions.Logging; @@ -17,136 +14,135 @@ using Umbraco.Cms.Infrastructure.Migrations; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Packaging +namespace Umbraco.Cms.Infrastructure.Packaging; + +internal class ImportPackageBuilderExpression : MigrationExpressionBase { - internal class ImportPackageBuilderExpression : MigrationExpressionBase + private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; + private readonly MediaFileManager _mediaFileManager; + private readonly IMediaService _mediaService; + private readonly MediaUrlGeneratorCollection _mediaUrlGenerators; + private readonly PackageMigrationSettings _packageMigrationSettings; + private readonly IPackagingService _packagingService; + private readonly IShortStringHelper _shortStringHelper; + + private bool _executed; + + public ImportPackageBuilderExpression( + IPackagingService packagingService, + IMediaService mediaService, + MediaFileManager mediaFileManager, + MediaUrlGeneratorCollection mediaUrlGenerators, + IShortStringHelper shortStringHelper, + IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, + IMigrationContext context, + IOptions packageMigrationSettings) : base(context) { - private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; - private readonly MediaFileManager _mediaFileManager; - private readonly IMediaService _mediaService; - private readonly MediaUrlGeneratorCollection _mediaUrlGenerators; - private readonly IPackagingService _packagingService; - private readonly IShortStringHelper _shortStringHelper; - private readonly PackageMigrationSettings _packageMigrationSettings; - - private bool _executed; - - public ImportPackageBuilderExpression( - IPackagingService packagingService, - IMediaService mediaService, - MediaFileManager mediaFileManager, - MediaUrlGeneratorCollection mediaUrlGenerators, - IShortStringHelper shortStringHelper, - IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, - IMigrationContext context, - IOptions packageMigrationSettings) : base(context) - { - _packagingService = packagingService; - _mediaService = mediaService; - _mediaFileManager = mediaFileManager; - _mediaUrlGenerators = mediaUrlGenerators; - _shortStringHelper = shortStringHelper; - _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; - _packageMigrationSettings = packageMigrationSettings.Value; - } + _packagingService = packagingService; + _mediaService = mediaService; + _mediaFileManager = mediaFileManager; + _mediaUrlGenerators = mediaUrlGenerators; + _shortStringHelper = shortStringHelper; + _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; + _packageMigrationSettings = packageMigrationSettings.Value; + } - /// - /// The type of the migration which dictates the namespace of the embedded resource - /// - public Type? EmbeddedResourceMigrationType { get; set; } + /// + /// The type of the migration which dictates the namespace of the embedded resource + /// + public Type? EmbeddedResourceMigrationType { get; set; } - public XDocument? PackageDataManifest { get; set; } + public XDocument? PackageDataManifest { get; set; } - public override void Execute() + public override void Execute() + { + if (_executed) { - if (_executed) - { - throw new InvalidOperationException("This expression has already been executed."); - } + throw new InvalidOperationException("This expression has already been executed."); + } - _executed = true; + _executed = true; - Context.BuildingExpression = false; + Context.BuildingExpression = false; - if (EmbeddedResourceMigrationType == null && PackageDataManifest == null) - { - throw new InvalidOperationException( - $"Nothing to execute, neither {nameof(EmbeddedResourceMigrationType)} or {nameof(PackageDataManifest)} has been set."); - } + if (EmbeddedResourceMigrationType == null && PackageDataManifest == null) + { + throw new InvalidOperationException( + $"Nothing to execute, neither {nameof(EmbeddedResourceMigrationType)} or {nameof(PackageDataManifest)} has been set."); + } - if (!_packageMigrationSettings.RunSchemaAndContentMigrations) - { - Logger.LogInformation("Skipping import of embedded schema file, due to configuration"); - return; - } + if (!_packageMigrationSettings.RunSchemaAndContentMigrations) + { + Logger.LogInformation("Skipping import of embedded schema file, due to configuration"); + return; + } - InstallationSummary installationSummary; - if (EmbeddedResourceMigrationType != null) - { - if (PackageMigrationResource.TryGetEmbeddedPackageDataManifest( + InstallationSummary installationSummary; + if (EmbeddedResourceMigrationType != null) + { + if (PackageMigrationResource.TryGetEmbeddedPackageDataManifest( EmbeddedResourceMigrationType, out XDocument? xml, out ZipArchive? zipPackage)) - { - // first install the package - installationSummary = _packagingService.InstallCompiledPackageData(xml!); + { + // first install the package + installationSummary = _packagingService.InstallCompiledPackageData(xml!); - if (zipPackage is not null) + if (zipPackage is not null) + { + // get the embedded resource + using (zipPackage) { - // get the embedded resource - using (zipPackage) + // then we need to save each file to the saved media items + var mediaWithFiles = xml!.XPathSelectElements( + "./umbPackage/MediaItems/MediaSet//*[@id][@mediaFilePath]") + .ToDictionary( + x => x.AttributeValue("key"), + x => x.AttributeValue("mediaFilePath")); + + // Any existing media by GUID will not be installed by the package service, it will just be skipped + // so you cannot 'update' media (or content) using a package since those are not schema type items. + // This means you cannot 'update' the media file either. The installationSummary.MediaInstalled + // will be empty for any existing media which means that the files will also not be updated. + foreach (IMedia media in installationSummary.MediaInstalled) { - // then we need to save each file to the saved media items - var mediaWithFiles = xml!.XPathSelectElements( - "./umbPackage/MediaItems/MediaSet//*[@id][@mediaFilePath]") - .ToDictionary( - x => x.AttributeValue("key"), - x => x.AttributeValue("mediaFilePath")); - - // Any existing media by GUID will not be installed by the package service, it will just be skipped - // so you cannot 'update' media (or content) using a package since those are not schema type items. - // This means you cannot 'update' the media file either. The installationSummary.MediaInstalled - // will be empty for any existing media which means that the files will also not be updated. - foreach (IMedia media in installationSummary.MediaInstalled) + if (mediaWithFiles.TryGetValue(media.Key, out var mediaFilePath)) { - if (mediaWithFiles.TryGetValue(media.Key, out var mediaFilePath)) + // this is a media item that has a file, so find that file in the zip + var entryPath = $"media{mediaFilePath!.EnsureStartsWith('/')}"; + ZipArchiveEntry? mediaEntry = zipPackage.GetEntry(entryPath); + if (mediaEntry == null) + { + throw new InvalidOperationException( + "No media file found in package zip for path " + + entryPath); + } + + // read the media file and save it to the media item + // using the current file system provider. + using (Stream mediaStream = mediaEntry.Open()) { - // this is a media item that has a file, so find that file in the zip - var entryPath = $"media{mediaFilePath!.EnsureStartsWith('/')}"; - ZipArchiveEntry? mediaEntry = zipPackage.GetEntry(entryPath); - if (mediaEntry == null) - { - throw new InvalidOperationException( - "No media file found in package zip for path " + - entryPath); - } - - // read the media file and save it to the media item - // using the current file system provider. - using (Stream mediaStream = mediaEntry.Open()) - { - media.SetValue( - _mediaFileManager, - _mediaUrlGenerators, - _shortStringHelper, - _contentTypeBaseServiceProvider, - Constants.Conventions.Media.File, - Path.GetFileName(mediaFilePath)!, - mediaStream); - } - - _mediaService.Save(media); + media.SetValue( + _mediaFileManager, + _mediaUrlGenerators, + _shortStringHelper, + _contentTypeBaseServiceProvider, + Constants.Conventions.Media.File, + Path.GetFileName(mediaFilePath)!, + mediaStream); } + + _mediaService.Save(media); } } } } - else - { - installationSummary = _packagingService.InstallCompiledPackageData(PackageDataManifest); - } - - Logger.LogInformation($"Package migration executed. Summary: {installationSummary}"); } + else + { + installationSummary = _packagingService.InstallCompiledPackageData(PackageDataManifest); + } + + Logger.LogInformation($"Package migration executed. Summary: {installationSummary}"); } } } diff --git a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs index 69271baf2c60..810942457282 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs @@ -9,6 +9,7 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.Packaging; using Umbraco.Cms.Core.Packaging; @@ -18,1827 +19,1872 @@ using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; +using Language = Umbraco.Cms.Core.Models.Language; +using Stylesheet = Umbraco.Cms.Core.Models.Stylesheet; -namespace Umbraco.Cms.Infrastructure.Packaging +namespace Umbraco.Cms.Infrastructure.Packaging; + +public class PackageDataInstallation { - public class PackageDataInstallation + private readonly IContentService _contentService; + private readonly IContentTypeService _contentTypeService; + private readonly IDataTypeService _dataTypeService; + private readonly IDataValueEditorFactory _dataValueEditorFactory; + private readonly IEntityService _entityService; + private readonly IFileService _fileService; + private readonly ILocalizationService _localizationService; + private readonly ILogger _logger; + private readonly IMacroService _macroService; + private readonly IMediaService _mediaService; + private readonly IMediaTypeService _mediaTypeService; + private readonly PropertyEditorCollection _propertyEditors; + private readonly IScopeProvider _scopeProvider; + private readonly IConfigurationEditorJsonSerializer _serializer; + private readonly IShortStringHelper _shortStringHelper; + + public PackageDataInstallation( + IDataValueEditorFactory dataValueEditorFactory, + ILogger logger, + IFileService fileService, + IMacroService macroService, + ILocalizationService localizationService, + IDataTypeService dataTypeService, + IEntityService entityService, + IContentTypeService contentTypeService, + IContentService contentService, + PropertyEditorCollection propertyEditors, + IScopeProvider scopeProvider, + IShortStringHelper shortStringHelper, + IConfigurationEditorJsonSerializer serializer, + IMediaService mediaService, + IMediaTypeService mediaTypeService) { - private readonly IDataValueEditorFactory _dataValueEditorFactory; - private readonly ILogger _logger; - private readonly IFileService _fileService; - private readonly IMacroService _macroService; - private readonly ILocalizationService _localizationService; - private readonly IDataTypeService _dataTypeService; - private readonly PropertyEditorCollection _propertyEditors; - private readonly IScopeProvider _scopeProvider; - private readonly IShortStringHelper _shortStringHelper; - private readonly IConfigurationEditorJsonSerializer _serializer; - private readonly IMediaService _mediaService; - private readonly IMediaTypeService _mediaTypeService; - private readonly IEntityService _entityService; - private readonly IContentTypeService _contentTypeService; - private readonly IContentService _contentService; - - public PackageDataInstallation( - IDataValueEditorFactory dataValueEditorFactory, - ILogger logger, - IFileService fileService, - IMacroService macroService, - ILocalizationService localizationService, - IDataTypeService dataTypeService, - IEntityService entityService, - IContentTypeService contentTypeService, - IContentService contentService, - PropertyEditorCollection propertyEditors, - IScopeProvider scopeProvider, - IShortStringHelper shortStringHelper, - IConfigurationEditorJsonSerializer serializer, - IMediaService mediaService, - IMediaTypeService mediaTypeService) - { - _dataValueEditorFactory = dataValueEditorFactory; - _logger = logger; - _fileService = fileService; - _macroService = macroService; - _localizationService = localizationService; - _dataTypeService = dataTypeService; - _entityService = entityService; - _contentTypeService = contentTypeService; - _contentService = contentService; - _propertyEditors = propertyEditors; - _scopeProvider = scopeProvider; - _shortStringHelper = shortStringHelper; - _serializer = serializer; - _mediaService = mediaService; - _mediaTypeService = mediaTypeService; - } - - // Also remove factory service registration when this constructor is removed - [Obsolete("Use the constructor with Infrastructure.IScopeProvider and without global settings and hosting environment instead.")] - public PackageDataInstallation( - IDataValueEditorFactory dataValueEditorFactory, - ILogger logger, - IFileService fileService, - IMacroService macroService, - ILocalizationService localizationService, - IDataTypeService dataTypeService, - IEntityService entityService, - IContentTypeService contentTypeService, - IContentService contentService, - PropertyEditorCollection propertyEditors, - Core.Scoping.IScopeProvider scopeProvider, - IShortStringHelper shortStringHelper, - IOptions globalSettings, - IConfigurationEditorJsonSerializer serializer, - IMediaService mediaService, - IMediaTypeService mediaTypeService, - IHostingEnvironment hostingEnvironment) - : this( - dataValueEditorFactory, - logger, - fileService, - macroService, - localizationService, - dataTypeService, - entityService, - contentTypeService, - contentService, - propertyEditors, - scopeProvider, - shortStringHelper, - serializer, - mediaService, - mediaTypeService) - { } - - #region Install/Uninstall - - public InstallationSummary InstallPackageData(CompiledPackage compiledPackage, int userId) - { - using (var scope = _scopeProvider.CreateScope()) - { - var installationSummary = new InstallationSummary(compiledPackage.Name) - { - Warnings = compiledPackage.Warnings, - DataTypesInstalled = - ImportDataTypes(compiledPackage.DataTypes.ToList(), userId, - out IEnumerable dataTypeEntityContainersInstalled), - LanguagesInstalled = ImportLanguages(compiledPackage.Languages, userId), - DictionaryItemsInstalled = ImportDictionaryItems(compiledPackage.DictionaryItems, userId), - MacrosInstalled = ImportMacros(compiledPackage.Macros, userId), - MacroPartialViewsInstalled = ImportMacroPartialViews(compiledPackage.MacroPartialViews, userId), - TemplatesInstalled = ImportTemplates(compiledPackage.Templates.ToList(), userId), - DocumentTypesInstalled = - ImportDocumentTypes(compiledPackage.DocumentTypes, userId, - out IEnumerable documentTypeEntityContainersInstalled), - MediaTypesInstalled = - ImportMediaTypes(compiledPackage.MediaTypes, userId, - out IEnumerable mediaTypeEntityContainersInstalled), - StylesheetsInstalled = ImportStylesheets(compiledPackage.Stylesheets, userId), - ScriptsInstalled = ImportScripts(compiledPackage.Scripts, userId), - PartialViewsInstalled = ImportPartialViews(compiledPackage.PartialViews, userId) - }; - - var entityContainersInstalled = new List(); - entityContainersInstalled.AddRange(dataTypeEntityContainersInstalled); - entityContainersInstalled.AddRange(documentTypeEntityContainersInstalled); - entityContainersInstalled.AddRange(mediaTypeEntityContainersInstalled); - installationSummary.EntityContainersInstalled = entityContainersInstalled; - - // We need a reference to the imported doc types to continue - var importedDocTypes = installationSummary.DocumentTypesInstalled.ToDictionary(x => x.Alias, x => x); - var importedMediaTypes = installationSummary.MediaTypesInstalled.ToDictionary(x => x.Alias, x => x); + _dataValueEditorFactory = dataValueEditorFactory; + _logger = logger; + _fileService = fileService; + _macroService = macroService; + _localizationService = localizationService; + _dataTypeService = dataTypeService; + _entityService = entityService; + _contentTypeService = contentTypeService; + _contentService = contentService; + _propertyEditors = propertyEditors; + _scopeProvider = scopeProvider; + _shortStringHelper = shortStringHelper; + _serializer = serializer; + _mediaService = mediaService; + _mediaTypeService = mediaTypeService; + } - installationSummary.ContentInstalled = ImportContentBase(compiledPackage.Documents, importedDocTypes, - userId, _contentTypeService, _contentService); - installationSummary.MediaInstalled = ImportContentBase(compiledPackage.Media, importedMediaTypes, - userId, _mediaTypeService, _mediaService); + // Also remove factory service registration when this constructor is removed + [Obsolete( + "Use the constructor with Infrastructure.IScopeProvider and without global settings and hosting environment instead.")] + public PackageDataInstallation( + IDataValueEditorFactory dataValueEditorFactory, + ILogger logger, + IFileService fileService, + IMacroService macroService, + ILocalizationService localizationService, + IDataTypeService dataTypeService, + IEntityService entityService, + IContentTypeService contentTypeService, + IContentService contentService, + PropertyEditorCollection propertyEditors, + Core.Scoping.IScopeProvider scopeProvider, + IShortStringHelper shortStringHelper, + IOptions globalSettings, + IConfigurationEditorJsonSerializer serializer, + IMediaService mediaService, + IMediaTypeService mediaTypeService, + IHostingEnvironment hostingEnvironment) + : this( + dataValueEditorFactory, + logger, + fileService, + macroService, + localizationService, + dataTypeService, + entityService, + contentTypeService, + contentService, + propertyEditors, + scopeProvider, + shortStringHelper, + serializer, + mediaService, + mediaTypeService) + { + } - scope.Complete(); + #region Languages - return installationSummary; + /// + /// Imports and saves the 'Languages' part of a package xml as a list of + /// + /// Xml to import + /// Optional id of the User performing the operation + /// An enumerable list of generated languages + public IReadOnlyList ImportLanguages(IEnumerable languageElements, int userId) + { + var list = new List(); + foreach (XElement languageElement in languageElements) + { + var isoCode = languageElement.AttributeValue("CultureAlias"); + if (string.IsNullOrEmpty(isoCode)) + { + continue; } - } - /// - /// Imports and saves package xml as - /// - /// Xml to import - /// Optional id of the User performing the operation. Default is zero (admin). - /// An enumerable list of generated ContentTypes - public IReadOnlyList ImportMediaTypes(IEnumerable docTypeElements, int userId) - => ImportMediaTypes(docTypeElements, userId, out _); - - /// - /// Imports and saves package xml as - /// - /// Xml to import - /// Optional id of the User performing the operation. Default is zero (admin). - /// Collection of entity containers installed by the package to be populated with those created in installing data types. - /// An enumerable list of generated ContentTypes - public IReadOnlyList ImportMediaTypes(IEnumerable docTypeElements, int userId, - out IEnumerable entityContainersInstalled) - => ImportDocumentTypes(docTypeElements.ToList(), true, userId, _mediaTypeService, - out entityContainersInstalled); - - #endregion - - #region Content - - public IReadOnlyList ImportContentBase( - IEnumerable docs, - IDictionary importedDocumentTypes, - int userId, - IContentTypeBaseService typeService, - IContentServiceBase service) - where TContentBase : class, IContentBase - where TContentTypeComposition : IContentTypeComposition - => docs.SelectMany(x => - ImportContentBase(x.XmlData.Elements().Where(doc => (string?)doc.Attribute("isDoc") == string.Empty), -1, - importedDocumentTypes, userId, typeService, service)).ToList(); - - /// - /// Imports and saves package xml as - /// - /// Xml to import - /// Optional parent Id for the content being imported - /// A dictionary of already imported document types (basically used as a cache) - /// Optional Id of the user performing the import - /// An enumerable list of generated content - public IEnumerable ImportContentBase( - IEnumerable roots, - int parentId, - IDictionary importedDocumentTypes, - int userId, - IContentTypeBaseService typeService, - IContentServiceBase service) - where TContentBase : class, IContentBase - where TContentTypeComposition : IContentTypeComposition - { - var contents = ParseContentBaseRootXml(roots, parentId, importedDocumentTypes, typeService, service) - .ToList(); - if (contents.Any()) + ILanguage? existingLanguage = _localizationService.GetLanguageByIsoCode(isoCode); + if (existingLanguage != null) { - service.Save(contents, userId); + continue; } - return contents; - - //var attribute = element.Attribute("isDoc"); - //if (attribute != null) - //{ - // //This is a single doc import - // var elements = new List { element }; - // var contents = ParseContentBaseRootXml(elements, parentId, importedDocumentTypes).ToList(); - // if (contents.Any()) - // _contentService.Save(contents, userId); + var cultureName = languageElement.AttributeValue("FriendlyName") ?? isoCode; - // return contents; - //} + var langauge = new Language(isoCode, cultureName); + _localizationService.Save(langauge, userId); - //throw new ArgumentException( - // "The passed in XElement is not valid! It does not contain a root element called " + - // "'DocumentSet' (for structured imports) nor is the first element a Document (for single document import)."); + list.Add(langauge); } - private IEnumerable ParseContentBaseRootXml( - IEnumerable roots, - int parentId, - IDictionary importedContentTypes, - IContentTypeBaseService typeService, - IContentServiceBase service) - where TContentBase : class, IContentBase - where TContentTypeComposition : IContentTypeComposition + return list; + } + + #endregion + + public IReadOnlyList ImportScripts(IEnumerable scriptElements, int userId) + { + var result = new List(); + + foreach (XElement scriptXml in scriptElements) { - var contents = new List(); - foreach (XElement root in roots) + var path = scriptXml.AttributeValue("path"); + if (path.IsNullOrWhiteSpace()) { - var contentTypeAlias = root.Name.LocalName; - - if (!importedContentTypes.ContainsKey(contentTypeAlias)) - { - TContentTypeComposition contentType = FindContentTypeByAlias(contentTypeAlias, typeService); - if (contentType == null) - { - throw new InvalidOperationException( - "Could not find content type with alias " + contentTypeAlias); - } + continue; + } - importedContentTypes.Add(contentTypeAlias, contentType); - } + IScript? script = _fileService.GetScript(path!); - if (TryCreateContentFromXml(root, importedContentTypes[contentTypeAlias], null, parentId, service, - out var content)) + // only update if it doesn't exist + if (script == null) + { + var content = scriptXml.Value; + if (content == null) { - contents.Add(content); + continue; } - var children = root.Elements().Where(doc => (string?)doc.Attribute("isDoc") == string.Empty).ToList(); - if (children.Count > 0) - { - contents.AddRange( - CreateContentFromXml(children, content, importedContentTypes, typeService, service) - .WhereNotNull()); - } + script = new Script(path!) {Content = content}; + _fileService.SaveScript(script, userId); + result.Add(script); } - - return contents; } - private IEnumerable CreateContentFromXml( - IEnumerable children, - TContentBase parent, - IDictionary importedContentTypes, - IContentTypeBaseService typeService, - IContentServiceBase service) - where TContentBase : class, IContentBase - where TContentTypeComposition : IContentTypeComposition + return result; + } + + public IReadOnlyList ImportPartialViews(IEnumerable partialViewElements, int userId) + { + var result = new List(); + + foreach (XElement partialViewXml in partialViewElements) { - var list = new List(); + var path = partialViewXml.AttributeValue("path"); - foreach (var child in children) + if (path == null) { - string contentTypeAlias = child.Name.LocalName; - if (importedContentTypes.ContainsKey(contentTypeAlias) == false) - { - var contentType = FindContentTypeByAlias(contentTypeAlias, typeService); + throw new InvalidOperationException("No path attribute found"); + } - importedContentTypes.Add(contentTypeAlias, contentType); - } + IPartialView? partialView = _fileService.GetPartialView(path); - // Create and add the child to the list - if (TryCreateContentFromXml(child, importedContentTypes[contentTypeAlias], parent, default, service, - out var content)) - { - list.Add(content); - } + // only update if it doesn't exist + if (partialView == null) + { + var content = partialViewXml.Value ?? string.Empty; - // Recursive call - var grandChildren = child.Elements().Where(x => (string?)x.Attribute("isDoc") == string.Empty).ToList(); - if (grandChildren.Any()) - { - list.AddRange(CreateContentFromXml(grandChildren, content, importedContentTypes, typeService, - service)); - } + partialView = new PartialView(PartialViewType.PartialView, path) {Content = content}; + _fileService.SavePartialView(partialView, userId); + result.Add(partialView); } - - return list; } - private bool TryCreateContentFromXml( - XElement element, - TContentTypeComposition contentType, - TContentBase? parent, - int parentId, - IContentServiceBase service, - out TContentBase output) - where TContentBase : class?, IContentBase - where TContentTypeComposition : IContentTypeComposition - { - Guid key = element.RequiredAttributeValue("key"); - - // we need to check if the content already exists and if so we ignore the installation for this item - var value = service.GetById(key); - if (value != null) - { - output = value; - return false; - } + return result; + } - var level = element.Attribute("level")?.Value ?? string.Empty; - var sortOrder = element.Attribute("sortOrder")?.Value ?? string.Empty; - var nodeName = element.Attribute("nodeName")?.Value ?? string.Empty; - var path = element.Attribute("path")?.Value; - var templateId = element.AttributeValue("template"); + #region Stylesheets - var properties = from property in element.Elements() - where property.Attribute("isDoc") == null - select property; + public IReadOnlyList ImportStylesheets(IEnumerable stylesheetElements, int userId) + { + var result = new List(); - //TODO: This will almost never work, we can't reference a template by an INT Id within a package manifest, we need to change the - // packager to package templates by UDI and resolve by the same, in 98% of cases, this isn't going to work, or it will resolve the wrong template. - var template = templateId.HasValue ? _fileService.GetTemplate(templateId.Value) : null; + foreach (XElement n in stylesheetElements) + { + var stylesheetPath = n.Element("FileName")?.Value; + if (stylesheetPath.IsNullOrWhiteSpace()) + { + continue; + } - //now double check this is correct since its an INT it could very well be pointing to an invalid template :/ - if (template != null && contentType is IContentType contentTypex) + IStylesheet? s = _fileService.GetStylesheet(stylesheetPath!); + if (s == null) { - if (!contentTypex.IsAllowedTemplate(template.Alias)) + var content = n.Element("Content")?.Value; + if (content == null) { - //well this is awkward, we'll set the template to null and it will be wired up to the default template - // when it's persisted in the document repository - template = null; + continue; } - } - TContentBase? content = CreateContent( - nodeName, - parent, - parentId, - contentType, - key, - int.Parse(level, CultureInfo.InvariantCulture), - int.Parse(sortOrder, CultureInfo.InvariantCulture), - template?.Id); - - if (content is null) - { - throw new InvalidOperationException("Cloud not create content"); + s = new Stylesheet(stylesheetPath!) {Content = content}; + _fileService.SaveStylesheet(s, userId); } - // Handle culture specific node names - const string nodeNamePrefix = "nodeName-"; - // Get the installed culture iso names, we create a localized content node with a culture that does not exist in the project - // We have to use Invariant comparisons, because when we get them from ContentBase in EntityXmlSerializer they're all lowercase. - var installedLanguages = _localizationService.GetAllLanguages().Select(l => l.IsoCode).ToArray(); - foreach (var localizedNodeName in element.Attributes() - .Where(a => a.Name.LocalName.InvariantStartsWith(nodeNamePrefix))) + foreach (XElement prop in n.XPathSelectElements("Properties/Property")) { - var newCulture = localizedNodeName.Name.LocalName.Substring(nodeNamePrefix.Length); - // Skip the culture if it does not exist in the current project - if (installedLanguages.InvariantContains(newCulture)) + var alias = prop.Element("Alias")!.Value; + IStylesheetProperty? sp = s.Properties?.SingleOrDefault(p => p != null && p.Alias == alias); + var name = prop.Element("Name")!.Value; + if (sp == null) { - content.SetCultureName(localizedNodeName.Value, newCulture); + sp = new StylesheetProperty(name, "#" + name.ToSafeAlias(_shortStringHelper), string.Empty); + s.AddProperty(sp); } - } - - //Here we make sure that we take composition properties in account as well - //otherwise we would skip them and end up losing content - var propTypes = contentType.CompositionPropertyTypes.Any() - ? contentType.CompositionPropertyTypes.ToDictionary(x => x.Alias, x => x) - : contentType.PropertyTypes.ToDictionary(x => x.Alias, x => x); - - var foundLanguages = new HashSet(); - foreach (var property in properties) - { - string propertyTypeAlias = property.Name.LocalName; - if (content.HasProperty(propertyTypeAlias)) + else { - var propertyValue = property.Value; - - // Handle properties language attributes - var propertyLang = property.Attribute(XName.Get("lang"))?.Value ?? null; - foundLanguages.Add(propertyLang); - if (propTypes.TryGetValue(propertyTypeAlias, out var propertyType)) + //sp.Text = name; + //Changing the name requires removing the current property and then adding another new one + if (sp.Name != name) { - // set property value - // Skip unsupported language variation, otherwise we'll get a "not supported error" - // We allow null, because that's invariant - if ( propertyLang is null || installedLanguages.InvariantContains(propertyLang)) - { - content.SetValue(propertyTypeAlias, propertyValue, propertyLang); - } + s.RemoveProperty(sp.Name); + var newProp = new StylesheetProperty(name, sp.Alias, sp.Value); + s.AddProperty(newProp); + sp = newProp; } } - } - foreach (var propertyLang in foundLanguages) - { - if (string.IsNullOrEmpty(content.GetCultureName(propertyLang)) && propertyLang is not null && - installedLanguages.InvariantContains(propertyLang)) - { - content.SetCultureName(nodeName, propertyLang); - } + sp.Alias = alias; + sp.Value = prop.Element("Value")!.Value; } - output = content; - return true; + _fileService.SaveStylesheet(s, userId); + result.Add(s); } - private TContentBase? CreateContent(string name, TContentBase? parent, - int parentId, TContentTypeComposition contentType, Guid key, int level, int sortOrder, int? templateId) - where TContentBase : class?, IContentBase - where TContentTypeComposition : IContentTypeComposition - { - switch (contentType) - { - case IContentType c: - if (parent is null) - { - return new Content(name, parentId, c) - { - Key = key, Level = level, SortOrder = sortOrder, TemplateId = templateId, - } as TContentBase; - } - else - { - return new Content(name, (IContent)parent, c) - { - Key = key, Level = level, SortOrder = sortOrder, TemplateId = templateId, - } as TContentBase; - } + return result; + } - case IMediaType m: - if (parent is null) - { - return new Core.Models.Media(name, parentId, m) - { - Key = key, Level = level, SortOrder = sortOrder, - } as TContentBase; - } - else - { - return new Core.Models.Media(name, (IMedia)parent, m) - { - Key = key, Level = level, SortOrder = sortOrder, - } as TContentBase; - } + #endregion - default: - throw new NotSupportedException($"Type {typeof(TContentTypeComposition)} is not supported"); - } + #region Install/Uninstall + + public InstallationSummary InstallPackageData(CompiledPackage compiledPackage, int userId) + { + using (IScope scope = _scopeProvider.CreateScope()) + { + var installationSummary = new InstallationSummary(compiledPackage.Name) + { + Warnings = compiledPackage.Warnings, + DataTypesInstalled = + ImportDataTypes(compiledPackage.DataTypes.ToList(), userId, + out IEnumerable dataTypeEntityContainersInstalled), + LanguagesInstalled = ImportLanguages(compiledPackage.Languages, userId), + DictionaryItemsInstalled = ImportDictionaryItems(compiledPackage.DictionaryItems, userId), + MacrosInstalled = ImportMacros(compiledPackage.Macros, userId), + MacroPartialViewsInstalled = ImportMacroPartialViews(compiledPackage.MacroPartialViews, userId), + TemplatesInstalled = ImportTemplates(compiledPackage.Templates.ToList(), userId), + DocumentTypesInstalled = + ImportDocumentTypes(compiledPackage.DocumentTypes, userId, + out IEnumerable documentTypeEntityContainersInstalled), + MediaTypesInstalled = + ImportMediaTypes(compiledPackage.MediaTypes, userId, + out IEnumerable mediaTypeEntityContainersInstalled), + StylesheetsInstalled = ImportStylesheets(compiledPackage.Stylesheets, userId), + ScriptsInstalled = ImportScripts(compiledPackage.Scripts, userId), + PartialViewsInstalled = ImportPartialViews(compiledPackage.PartialViews, userId) + }; + + var entityContainersInstalled = new List(); + entityContainersInstalled.AddRange(dataTypeEntityContainersInstalled); + entityContainersInstalled.AddRange(documentTypeEntityContainersInstalled); + entityContainersInstalled.AddRange(mediaTypeEntityContainersInstalled); + installationSummary.EntityContainersInstalled = entityContainersInstalled; + + // We need a reference to the imported doc types to continue + var importedDocTypes = installationSummary.DocumentTypesInstalled.ToDictionary(x => x.Alias, x => x); + var importedMediaTypes = installationSummary.MediaTypesInstalled.ToDictionary(x => x.Alias, x => x); + + installationSummary.ContentInstalled = ImportContentBase(compiledPackage.Documents, importedDocTypes, + userId, _contentTypeService, _contentService); + installationSummary.MediaInstalled = ImportContentBase(compiledPackage.Media, importedMediaTypes, + userId, _mediaTypeService, _mediaService); + + scope.Complete(); + + return installationSummary; } + } - #endregion - - #region DocumentTypes - - public IReadOnlyList ImportDocumentType(XElement docTypeElement, int userId) - => ImportDocumentTypes(new[] {docTypeElement}, userId, out _); - - /// - /// Imports and saves package xml as - /// - /// Xml to import - /// Optional id of the User performing the operation. Default is zero (admin). - /// An enumerable list of generated ContentTypes - public IReadOnlyList ImportDocumentTypes(IEnumerable docTypeElements, int userId) - => ImportDocumentTypes(docTypeElements.ToList(), true, userId, _contentTypeService, out _); - - /// - /// Imports and saves package xml as - /// - /// Xml to import - /// Optional id of the User performing the operation. Default is zero (admin). - /// Collection of entity containers installed by the package to be populated with those created in installing data types. - /// An enumerable list of generated ContentTypes - public IReadOnlyList ImportDocumentTypes(IEnumerable docTypeElements, int userId, - out IEnumerable entityContainersInstalled) - => ImportDocumentTypes(docTypeElements.ToList(), true, userId, _contentTypeService, - out entityContainersInstalled); - - /// - /// Imports and saves package xml as - /// - /// Xml to import - /// Boolean indicating whether or not to import the - /// Optional id of the User performing the operation. Default is zero (admin). - /// An enumerable list of generated ContentTypes - public IReadOnlyList ImportDocumentTypes(IReadOnlyCollection unsortedDocumentTypes, - bool importStructure, int userId, IContentTypeBaseService service) - where T : class, IContentTypeComposition - => ImportDocumentTypes(unsortedDocumentTypes, importStructure, userId, service); - - /// - /// Imports and saves package xml as - /// - /// Xml to import - /// Boolean indicating whether or not to import the - /// Optional id of the User performing the operation. Default is zero (admin). - /// Collection of entity containers installed by the package to be populated with those created in installing data types. - /// An enumerable list of generated ContentTypes - public IReadOnlyList ImportDocumentTypes(IReadOnlyCollection unsortedDocumentTypes, - bool importStructure, int userId, IContentTypeBaseService service, - out IEnumerable entityContainersInstalled) - where T : class, IContentTypeComposition - { - var importedContentTypes = new Dictionary(); - - //When you are importing a single doc type we have to assume that the dependencies are already there. - //Otherwise something like uSync won't work. - var graph = new TopoGraph>(x => x.Key, x => x.Dependencies); - var isSingleDocTypeImport = unsortedDocumentTypes.Count == 1; - - var importedFolders = - CreateContentTypeFolderStructure(unsortedDocumentTypes, out entityContainersInstalled); - - if (isSingleDocTypeImport == false) - { - //NOTE Here we sort the doctype XElements based on dependencies - //before creating the doc types - this should also allow for a better structure/inheritance support. - foreach (var documentType in unsortedDocumentTypes) - { - var elementCopy = documentType; - var infoElement = elementCopy.Element("Info"); - var dependencies = new HashSet(); + /// + /// Imports and saves package xml as + /// + /// Xml to import + /// Optional id of the User performing the operation. Default is zero (admin). + /// An enumerable list of generated ContentTypes + public IReadOnlyList ImportMediaTypes(IEnumerable docTypeElements, int userId) + => ImportMediaTypes(docTypeElements, userId, out _); + + /// + /// Imports and saves package xml as + /// + /// Xml to import + /// Optional id of the User performing the operation. Default is zero (admin). + /// + /// Collection of entity containers installed by the package to be populated with + /// those created in installing data types. + /// + /// An enumerable list of generated ContentTypes + public IReadOnlyList ImportMediaTypes(IEnumerable docTypeElements, int userId, + out IEnumerable entityContainersInstalled) + => ImportDocumentTypes(docTypeElements.ToList(), true, userId, _mediaTypeService, + out entityContainersInstalled); + + #endregion + + #region Content + + public IReadOnlyList ImportContentBase( + IEnumerable docs, + IDictionary importedDocumentTypes, + int userId, + IContentTypeBaseService typeService, + IContentServiceBase service) + where TContentBase : class, IContentBase + where TContentTypeComposition : IContentTypeComposition + => docs.SelectMany(x => + ImportContentBase(x.XmlData.Elements().Where(doc => (string?)doc.Attribute("isDoc") == string.Empty), -1, + importedDocumentTypes, userId, typeService, service)).ToList(); + + /// + /// Imports and saves package xml as + /// + /// Xml to import + /// Optional parent Id for the content being imported + /// A dictionary of already imported document types (basically used as a cache) + /// Optional Id of the user performing the import + /// An enumerable list of generated content + public IEnumerable ImportContentBase( + IEnumerable roots, + int parentId, + IDictionary importedDocumentTypes, + int userId, + IContentTypeBaseService typeService, + IContentServiceBase service) + where TContentBase : class, IContentBase + where TContentTypeComposition : IContentTypeComposition + { + var contents = ParseContentBaseRootXml(roots, parentId, importedDocumentTypes, typeService, service) + .ToList(); + if (contents.Any()) + { + service.Save(contents, userId); + } - //Add the Master as a dependency - if (string.IsNullOrEmpty((string?)infoElement?.Element("Master")) == false) - { - dependencies.Add(infoElement.Element("Master")?.Value!); - } + return contents; - //Add compositions as dependencies - var compositionsElement = infoElement?.Element("Compositions"); - if (compositionsElement != null && compositionsElement.HasElements) - { - var compositions = compositionsElement.Elements("Composition"); - if (compositions.Any()) - { - foreach (var composition in compositions) - { - dependencies.Add(composition.Value); - } - } - } + //var attribute = element.Attribute("isDoc"); + //if (attribute != null) + //{ + // //This is a single doc import + // var elements = new List { element }; + // var contents = ParseContentBaseRootXml(elements, parentId, importedDocumentTypes).ToList(); + // if (contents.Any()) + // _contentService.Save(contents, userId); - graph.AddItem(TopoGraph.CreateNode(infoElement!.Element("Alias")!.Value, elementCopy, - dependencies.ToArray())); - } - } + // return contents; + //} - //Sorting the Document Types based on dependencies - if its not a single doc type import ref. #U4-5921 - List documentTypes = isSingleDocTypeImport - ? unsortedDocumentTypes.ToList() - : graph.GetSortedItems().Select(x => x.Item).ToList(); + //throw new ArgumentException( + // "The passed in XElement is not valid! It does not contain a root element called " + + // "'DocumentSet' (for structured imports) nor is the first element a Document (for single document import)."); + } - //Iterate the sorted document types and create them as IContentType objects - foreach (XElement documentType in documentTypes) - { - var alias = documentType.Element("Info")?.Element("Alias")?.Value; + private IEnumerable ParseContentBaseRootXml( + IEnumerable roots, + int parentId, + IDictionary importedContentTypes, + IContentTypeBaseService typeService, + IContentServiceBase service) + where TContentBase : class, IContentBase + where TContentTypeComposition : IContentTypeComposition + { + var contents = new List(); + foreach (XElement root in roots) + { + var contentTypeAlias = root.Name.LocalName; - if (alias is not null && importedContentTypes.ContainsKey(alias) == false) + if (!importedContentTypes.ContainsKey(contentTypeAlias)) + { + TContentTypeComposition contentType = FindContentTypeByAlias(contentTypeAlias, typeService); + if (contentType == null) { - T? contentType = service.Get(alias); - - importedContentTypes.Add(alias, contentType == null - ? CreateContentTypeFromXml(documentType, importedContentTypes, service) - : UpdateContentTypeFromXml(documentType, contentType, importedContentTypes, service)); + throw new InvalidOperationException( + "Could not find content type with alias " + contentTypeAlias); } + + importedContentTypes.Add(contentTypeAlias, contentType); } - foreach (var contentType in importedContentTypes) + if (TryCreateContentFromXml(root, importedContentTypes[contentTypeAlias], null, parentId, service, + out TContentBase content)) { - var ct = contentType.Value; - if (importedFolders.ContainsKey(ct.Alias)) - { - ct.ParentId = importedFolders[ct.Alias]; - } + contents.Add(content); } - //Save the newly created/updated IContentType objects - var list = importedContentTypes.Select(x => x.Value).ToList(); - service.Save(list, userId); - - //Now we can finish the import by updating the 'structure', - //which requires the doc types to be saved/available in the db - if (importStructure) + var children = root.Elements().Where(doc => (string?)doc.Attribute("isDoc") == string.Empty).ToList(); + if (children.Count > 0) { - var updatedContentTypes = new List(); - //Update the structure here - we can't do it until all DocTypes have been created - foreach (var documentType in documentTypes) - { - var alias = documentType.Element("Info")?.Element("Alias")?.Value; - var structureElement = documentType.Element("Structure"); - //Ensure that we only update ContentTypes which has actual structure-elements - if (structureElement == null || structureElement.Elements().Any() == false || alias is null) - continue; - - var updated = UpdateContentTypesStructure(importedContentTypes[alias], structureElement, - importedContentTypes, service); - updatedContentTypes.Add(updated); - } - - //Update ContentTypes with a newly added structure/list of allowed children - if (updatedContentTypes.Any()) - { - service.Save(updatedContentTypes, userId); - } + contents.AddRange( + CreateContentFromXml(children, content, importedContentTypes, typeService, service) + .WhereNotNull()); } - - return list; } - private Dictionary CreateContentTypeFolderStructure(IEnumerable unsortedDocumentTypes, - out IEnumerable entityContainersInstalled) - { - var importedFolders = new Dictionary(); - var trackEntityContainersInstalled = new List(); + return contents; + } - foreach (XElement documentType in unsortedDocumentTypes) + private IEnumerable CreateContentFromXml( + IEnumerable children, + TContentBase parent, + IDictionary importedContentTypes, + IContentTypeBaseService typeService, + IContentServiceBase service) + where TContentBase : class, IContentBase + where TContentTypeComposition : IContentTypeComposition + { + var list = new List(); + + foreach (XElement child in children) + { + var contentTypeAlias = child.Name.LocalName; + if (importedContentTypes.ContainsKey(contentTypeAlias) == false) { - XAttribute? foldersAttribute = documentType.Attribute("Folders"); - XElement? infoElement = documentType.Element("Info"); - if (foldersAttribute != null && infoElement != null - // don't import any folder if this is a child doc type - the parent doc type will need to - // exist which contains it's folders - && ((string?)infoElement.Element("Master")).IsNullOrWhiteSpace()) - { - var alias = documentType.Element("Info")?.Element("Alias")?.Value; - var folders = foldersAttribute.Value.Split(Constants.CharArrays.ForwardSlash); + TContentTypeComposition contentType = FindContentTypeByAlias(contentTypeAlias, typeService); - XAttribute? folderKeysAttribute = documentType.Attribute("FolderKeys"); + importedContentTypes.Add(contentTypeAlias, contentType); + } - Guid[] folderKeys = Array.Empty(); - if (folderKeysAttribute != null) - { - folderKeys = folderKeysAttribute.Value.Split(Constants.CharArrays.ForwardSlash) - .Select(x => Guid.Parse(x)).ToArray(); - } + // Create and add the child to the list + if (TryCreateContentFromXml(child, importedContentTypes[contentTypeAlias], parent, default, service, + out TContentBase content)) + { + list.Add(content); + } - var rootFolder = WebUtility.UrlDecode(folders[0]); + // Recursive call + var grandChildren = child.Elements().Where(x => (string?)x.Attribute("isDoc") == string.Empty).ToList(); + if (grandChildren.Any()) + { + list.AddRange(CreateContentFromXml(grandChildren, content, importedContentTypes, typeService, + service)); + } + } - EntityContainer? current = null; - Guid? rootFolderKey = null; - if (folderKeys.Length == folders.Length && folderKeys.Length > 0) - { - rootFolderKey = folderKeys[0]; - current = _contentTypeService.GetContainer(rootFolderKey.Value); - } + return list; + } - // The folder might already exist, but with a different key, so check if it exists, even if there is a key. - // Level 1 = root level folders, there can only be one with the same name - current ??= _contentTypeService.GetContainers(rootFolder, 1)?.FirstOrDefault(); + private bool TryCreateContentFromXml( + XElement element, + TContentTypeComposition contentType, + TContentBase? parent, + int parentId, + IContentServiceBase service, + out TContentBase output) + where TContentBase : class?, IContentBase + where TContentTypeComposition : IContentTypeComposition + { + Guid key = element.RequiredAttributeValue("key"); - if (current == null) - { - Attempt?> tryCreateFolder = - _contentTypeService.CreateContainer(-1, rootFolderKey ?? Guid.NewGuid(), rootFolder); + // we need to check if the content already exists and if so we ignore the installation for this item + TContentBase? value = service.GetById(key); + if (value != null) + { + output = value; + return false; + } - if (tryCreateFolder == false) - { - _logger.LogError(tryCreateFolder.Exception, "Could not create folder: {FolderName}", - rootFolder); - throw tryCreateFolder.Exception!; - } + var level = element.Attribute("level")?.Value ?? string.Empty; + var sortOrder = element.Attribute("sortOrder")?.Value ?? string.Empty; + var nodeName = element.Attribute("nodeName")?.Value ?? string.Empty; + var path = element.Attribute("path")?.Value; + var templateId = element.AttributeValue("template"); - var rootFolderId = tryCreateFolder.Result?.Entity?.Id; - if (rootFolderId is not null) - { - current = _contentTypeService.GetContainer(rootFolderId.Value); - trackEntityContainersInstalled.Add(current!); - } - } + IEnumerable properties = from property in element.Elements() + where property.Attribute("isDoc") == null + select property; - importedFolders.Add(alias!, current!.Id); + //TODO: This will almost never work, we can't reference a template by an INT Id within a package manifest, we need to change the + // packager to package templates by UDI and resolve by the same, in 98% of cases, this isn't going to work, or it will resolve the wrong template. + ITemplate? template = templateId.HasValue ? _fileService.GetTemplate(templateId.Value) : null; - for (var i = 1; i < folders.Length; i++) - { - var folderName = WebUtility.UrlDecode(folders[i]); - Guid? folderKey = (folderKeys.Length == folders.Length) ? folderKeys[i] : null; - current = CreateContentTypeChildFolder(folderName, folderKey ?? Guid.NewGuid(), current); - trackEntityContainersInstalled.Add(current!); - importedFolders[alias!] = current!.Id; - } - } + //now double check this is correct since its an INT it could very well be pointing to an invalid template :/ + if (template != null && contentType is IContentType contentTypex) + { + if (!contentTypex.IsAllowedTemplate(template.Alias)) + { + //well this is awkward, we'll set the template to null and it will be wired up to the default template + // when it's persisted in the document repository + template = null; } - - entityContainersInstalled = trackEntityContainersInstalled; - return importedFolders; } - private EntityContainer? CreateContentTypeChildFolder(string folderName, Guid folderKey, IUmbracoEntity current) + TContentBase? content = CreateContent( + nodeName, + parent, + parentId, + contentType, + key, + int.Parse(level, CultureInfo.InvariantCulture), + int.Parse(sortOrder, CultureInfo.InvariantCulture), + template?.Id); + + if (content is null) { - var children = _entityService.GetChildren(current.Id).ToArray(); - var found = children.Any(x => x.Name.InvariantEquals(folderName) || x.Key.Equals(folderKey)); - if (found) - { - var containerId = children.Single(x => x.Name.InvariantEquals(folderName)).Id; - return _contentTypeService.GetContainer(containerId); - } + throw new InvalidOperationException("Cloud not create content"); + } - var tryCreateFolder = _contentTypeService.CreateContainer(current.Id, folderKey, folderName); - if (tryCreateFolder == false) + // Handle culture specific node names + const string nodeNamePrefix = "nodeName-"; + // Get the installed culture iso names, we create a localized content node with a culture that does not exist in the project + // We have to use Invariant comparisons, because when we get them from ContentBase in EntityXmlSerializer they're all lowercase. + var installedLanguages = _localizationService.GetAllLanguages().Select(l => l.IsoCode).ToArray(); + foreach (XAttribute localizedNodeName in element.Attributes() + .Where(a => a.Name.LocalName.InvariantStartsWith(nodeNamePrefix))) + { + var newCulture = localizedNodeName.Name.LocalName.Substring(nodeNamePrefix.Length); + // Skip the culture if it does not exist in the current project + if (installedLanguages.InvariantContains(newCulture)) { - _logger.LogError(tryCreateFolder.Exception, "Could not create folder: {FolderName}", folderName); - throw tryCreateFolder.Exception!; + content.SetCultureName(localizedNodeName.Value, newCulture); } - - return _contentTypeService.GetContainer(tryCreateFolder.Result!.Entity!.Id); } - private T CreateContentTypeFromXml( - XElement documentType, - IReadOnlyDictionary importedContentTypes, - IContentTypeBaseService service) - where T : class, IContentTypeComposition - { - var key = Guid.Parse(documentType.Element("Info")!.Element("Key")!.Value); - - XElement infoElement = documentType.Element("Info")!; + //Here we make sure that we take composition properties in account as well + //otherwise we would skip them and end up losing content + Dictionary propTypes = contentType.CompositionPropertyTypes.Any() + ? contentType.CompositionPropertyTypes.ToDictionary(x => x.Alias, x => x) + : contentType.PropertyTypes.ToDictionary(x => x.Alias, x => x); - //Name of the master corresponds to the parent - XElement? masterElement = infoElement.Element("Master"); - - T? parent = default; - if (masterElement != null) + var foundLanguages = new HashSet(); + foreach (XElement? property in properties) + { + var propertyTypeAlias = property.Name.LocalName; + if (content.HasProperty(propertyTypeAlias)) { - var masterAlias = masterElement.Value; - parent = importedContentTypes.ContainsKey(masterAlias) - ? importedContentTypes[masterAlias] - : service.Get(masterAlias); - } + var propertyValue = property.Value; - var alias = infoElement?.Element("Alias")?.Value; - var contentType = CreateContentType(key, parent, -1, alias!); + // Handle properties language attributes + var propertyLang = property.Attribute(XName.Get("lang"))?.Value ?? null; + foundLanguages.Add(propertyLang); + if (propTypes.TryGetValue(propertyTypeAlias, out IPropertyType? propertyType)) + { + // set property value + // Skip unsupported language variation, otherwise we'll get a "not supported error" + // We allow null, because that's invariant + if (propertyLang is null || installedLanguages.InvariantContains(propertyLang)) + { + content.SetValue(propertyTypeAlias, propertyValue, propertyLang); + } + } + } + } - if (parent != null) + foreach (var propertyLang in foundLanguages) + { + if (string.IsNullOrEmpty(content.GetCultureName(propertyLang)) && propertyLang is not null && + installedLanguages.InvariantContains(propertyLang)) { - contentType?.AddContentType(parent); + content.SetCultureName(nodeName, propertyLang); } - - return UpdateContentTypeFromXml(documentType, contentType, importedContentTypes, service); } - private T? CreateContentType(Guid key, T? parent, int parentId, string alias) - where T : class, IContentTypeComposition + output = content; + return true; + } + + private TContentBase? CreateContent(string name, TContentBase? parent, + int parentId, TContentTypeComposition contentType, Guid key, int level, int sortOrder, int? templateId) + where TContentBase : class?, IContentBase + where TContentTypeComposition : IContentTypeComposition + { + switch (contentType) { - if (typeof(T) == typeof(IContentType)) - { + case IContentType c: if (parent is null) { - return new ContentType(_shortStringHelper, parentId) {Alias = alias, Key = key} as T; + return new Content(name, parentId, c) + { + Key = key, Level = level, SortOrder = sortOrder, TemplateId = templateId + } as TContentBase; } - else + + return new Content(name, (IContent)parent, c) { - return new ContentType(_shortStringHelper, (IContentType)parent, alias) {Key = key} as T; - } - } + Key = key, Level = level, SortOrder = sortOrder, TemplateId = templateId + } as TContentBase; - if (typeof(T) == typeof(IMediaType)) - { + case IMediaType m: if (parent is null) { - return new MediaType(_shortStringHelper, parentId) {Alias = alias, Key = key} as T; - } - else - { - return new MediaType(_shortStringHelper, (IMediaType)parent, alias) {Key = key} as T; + return new Core.Models.Media(name, parentId, m) {Key = key, Level = level, SortOrder = sortOrder} as + TContentBase; } - } - throw new NotSupportedException($"Type {typeof(T)} is not supported"); + return new Core.Models.Media(name, (IMedia)parent, m) {Key = key, Level = level, SortOrder = sortOrder} + as TContentBase; + + default: + throw new NotSupportedException($"Type {typeof(TContentTypeComposition)} is not supported"); } + } - private T UpdateContentTypeFromXml( - XElement documentType, - T? contentType, - IReadOnlyDictionary importedContentTypes, - IContentTypeBaseService service) - where T : IContentTypeComposition - { - var key = Guid.Parse(documentType.Element("Info")!.Element("Key")!.Value); + #endregion + + #region DocumentTypes + + public IReadOnlyList ImportDocumentType(XElement docTypeElement, int userId) + => ImportDocumentTypes(new[] {docTypeElement}, userId, out _); + + /// + /// Imports and saves package xml as + /// + /// Xml to import + /// Optional id of the User performing the operation. Default is zero (admin). + /// An enumerable list of generated ContentTypes + public IReadOnlyList ImportDocumentTypes(IEnumerable docTypeElements, int userId) + => ImportDocumentTypes(docTypeElements.ToList(), true, userId, _contentTypeService, out _); + + /// + /// Imports and saves package xml as + /// + /// Xml to import + /// Optional id of the User performing the operation. Default is zero (admin). + /// + /// Collection of entity containers installed by the package to be populated with + /// those created in installing data types. + /// + /// An enumerable list of generated ContentTypes + public IReadOnlyList ImportDocumentTypes(IEnumerable docTypeElements, int userId, + out IEnumerable entityContainersInstalled) + => ImportDocumentTypes(docTypeElements.ToList(), true, userId, _contentTypeService, + out entityContainersInstalled); + + /// + /// Imports and saves package xml as + /// + /// Xml to import + /// Boolean indicating whether or not to import the + /// Optional id of the User performing the operation. Default is zero (admin). + /// An enumerable list of generated ContentTypes + public IReadOnlyList ImportDocumentTypes(IReadOnlyCollection unsortedDocumentTypes, + bool importStructure, int userId, IContentTypeBaseService service) + where T : class, IContentTypeComposition + => ImportDocumentTypes(unsortedDocumentTypes, importStructure, userId, service); + + /// + /// Imports and saves package xml as + /// + /// Xml to import + /// Boolean indicating whether or not to import the + /// Optional id of the User performing the operation. Default is zero (admin). + /// + /// Collection of entity containers installed by the package to be populated with + /// those created in installing data types. + /// + /// An enumerable list of generated ContentTypes + public IReadOnlyList ImportDocumentTypes(IReadOnlyCollection unsortedDocumentTypes, + bool importStructure, int userId, IContentTypeBaseService service, + out IEnumerable entityContainersInstalled) + where T : class, IContentTypeComposition + { + var importedContentTypes = new Dictionary(); - var infoElement = documentType.Element("Info"); - var defaultTemplateElement = infoElement?.Element("DefaultTemplate"); + //When you are importing a single doc type we have to assume that the dependencies are already there. + //Otherwise something like uSync won't work. + var graph = new TopoGraph>(x => x.Key, x => x.Dependencies); + var isSingleDocTypeImport = unsortedDocumentTypes.Count == 1; - if (contentType is null) - { - throw new InvalidOperationException("Content type was null"); - } - contentType.Key = key; - contentType.Name = infoElement!.Element("Name")!.Value; - if (infoElement.Element("Key") != null) - contentType.Key = new Guid(infoElement.Element("Key")!.Value); - contentType.Icon = infoElement.Element("Icon")?.Value; - contentType.Thumbnail = infoElement.Element("Thumbnail")?.Value; - contentType.Description = infoElement.Element("Description")?.Value; - - //NOTE AllowAtRoot, IsListView, IsElement and Variations are new properties in the package xml so we need to verify it exists before using it. - var allowAtRoot = infoElement.Element("AllowAtRoot"); - if (allowAtRoot != null) - contentType.AllowedAsRoot = allowAtRoot.Value.InvariantEquals("true"); - - var isListView = infoElement.Element("IsListView"); - if (isListView != null) - contentType.IsContainer = isListView.Value.InvariantEquals("true"); - - var isElement = infoElement.Element("IsElement"); - if (isElement != null) - contentType.IsElement = isElement.Value.InvariantEquals("true"); - - var variationsElement = infoElement.Element("Variations"); - if (variationsElement != null) - contentType.Variations = - (ContentVariation)Enum.Parse(typeof(ContentVariation), variationsElement.Value); - - //Name of the master corresponds to the parent and we need to ensure that the Parent Id is set - var masterElement = infoElement.Element("Master"); - if (masterElement != null) + Dictionary importedFolders = + CreateContentTypeFolderStructure(unsortedDocumentTypes, out entityContainersInstalled); + + if (isSingleDocTypeImport == false) + { + //NOTE Here we sort the doctype XElements based on dependencies + //before creating the doc types - this should also allow for a better structure/inheritance support. + foreach (XElement documentType in unsortedDocumentTypes) { - var masterAlias = masterElement.Value; - T? parent = importedContentTypes.ContainsKey(masterAlias) - ? importedContentTypes[masterAlias] - : service.Get(masterAlias); + XElement elementCopy = documentType; + XElement? infoElement = elementCopy.Element("Info"); + var dependencies = new HashSet(); - contentType.SetParent(parent); - } + //Add the Master as a dependency + if (string.IsNullOrEmpty((string?)infoElement?.Element("Master")) == false) + { + dependencies.Add(infoElement.Element("Master")?.Value!); + } - //Update Compositions on the ContentType to ensure that they are as is defined in the package xml - var compositionsElement = infoElement.Element("Compositions"); - if (compositionsElement != null && compositionsElement.HasElements) - { - var compositions = compositionsElement.Elements("Composition"); - if (compositions.Any()) + //Add compositions as dependencies + XElement? compositionsElement = infoElement?.Element("Compositions"); + if (compositionsElement != null && compositionsElement.HasElements) { - foreach (var composition in compositions) + IEnumerable compositions = compositionsElement.Elements("Composition"); + if (compositions.Any()) { - var compositionAlias = composition.Value; - var compositionContentType = importedContentTypes.ContainsKey(compositionAlias) - ? importedContentTypes[compositionAlias] - : service.Get(compositionAlias); - contentType.AddContentType(compositionContentType); + foreach (XElement composition in compositions) + { + dependencies.Add(composition.Value); + } } } - } - - if (contentType is IContentType contentTypex) - { - UpdateContentTypesAllowedTemplates(contentTypex, infoElement.Element("AllowedTemplates"), - defaultTemplateElement); - } - UpdateContentTypesPropertyGroups(contentType, documentType.Element("Tabs")); - UpdateContentTypesProperties(contentType, documentType.Element("GenericProperties")); - - if (contentType is IContentTypeWithHistoryCleanup withCleanup) - { - UpdateHistoryCleanupPolicy(withCleanup, documentType.Element("HistoryCleanupPolicy")); + graph.AddItem(TopoGraph.CreateNode(infoElement!.Element("Alias")!.Value, elementCopy, + dependencies.ToArray())); } - - return contentType; } - private void UpdateHistoryCleanupPolicy(IContentTypeWithHistoryCleanup withCleanup, XElement? element) - { - if (element == null) - { - return; - } + //Sorting the Document Types based on dependencies - if its not a single doc type import ref. #U4-5921 + List documentTypes = isSingleDocTypeImport + ? unsortedDocumentTypes.ToList() + : graph.GetSortedItems().Select(x => x.Item).ToList(); - withCleanup.HistoryCleanup ??= new Core.Models.ContentEditing.HistoryCleanup(); + //Iterate the sorted document types and create them as IContentType objects + foreach (XElement documentType in documentTypes) + { + var alias = documentType.Element("Info")?.Element("Alias")?.Value; - if (bool.TryParse(element.Attribute("preventCleanup")?.Value, out var preventCleanup)) + if (alias is not null && importedContentTypes.ContainsKey(alias) == false) { - withCleanup.HistoryCleanup.PreventCleanup = preventCleanup; - } + T? contentType = service.Get(alias); - if (int.TryParse(element.Attribute("keepAllVersionsNewerThanDays")?.Value, out var keepAll)) - { - withCleanup.HistoryCleanup.KeepAllVersionsNewerThanDays = keepAll; - } - else - { - withCleanup.HistoryCleanup.KeepAllVersionsNewerThanDays = null; + importedContentTypes.Add(alias, contentType == null + ? CreateContentTypeFromXml(documentType, importedContentTypes, service) + : UpdateContentTypeFromXml(documentType, contentType, importedContentTypes, service)); } + } - if (int.TryParse(element.Attribute("keepLatestVersionPerDayForDays")?.Value, out var keepLatest)) - { - withCleanup.HistoryCleanup.KeepLatestVersionPerDayForDays = keepLatest; - } - else + foreach (KeyValuePair contentType in importedContentTypes) + { + T ct = contentType.Value; + if (importedFolders.ContainsKey(ct.Alias)) { - withCleanup.HistoryCleanup.KeepLatestVersionPerDayForDays = null; + ct.ParentId = importedFolders[ct.Alias]; } } - private void UpdateContentTypesAllowedTemplates(IContentType contentType, XElement? allowedTemplatesElement, - XElement? defaultTemplateElement) + //Save the newly created/updated IContentType objects + var list = importedContentTypes.Select(x => x.Value).ToList(); + service.Save(list, userId); + + //Now we can finish the import by updating the 'structure', + //which requires the doc types to be saved/available in the db + if (importStructure) { - if (allowedTemplatesElement != null && allowedTemplatesElement.Elements("Template").Any()) + var updatedContentTypes = new List(); + //Update the structure here - we can't do it until all DocTypes have been created + foreach (XElement documentType in documentTypes) { - var allowedTemplates = contentType.AllowedTemplates?.ToList(); - foreach (var templateElement in allowedTemplatesElement.Elements("Template")) + var alias = documentType.Element("Info")?.Element("Alias")?.Value; + XElement? structureElement = documentType.Element("Structure"); + //Ensure that we only update ContentTypes which has actual structure-elements + if (structureElement == null || structureElement.Elements().Any() == false || alias is null) { - var alias = templateElement.Value; - var template = _fileService.GetTemplate(alias.ToSafeAlias(_shortStringHelper)); - if (template != null) - { - if (allowedTemplates?.Any(x => x.Id == template.Id) ?? true) - continue; - allowedTemplates.Add(template); - } - else - { - _logger.LogWarning( - "Packager: Error handling allowed templates. Template with alias '{TemplateAlias}' could not be found.", - alias); - } + continue; } - contentType.AllowedTemplates = allowedTemplates; + T updated = UpdateContentTypesStructure(importedContentTypes[alias], structureElement, + importedContentTypes, service); + updatedContentTypes.Add(updated); } - if (string.IsNullOrEmpty((string?)defaultTemplateElement) == false) + //Update ContentTypes with a newly added structure/list of allowed children + if (updatedContentTypes.Any()) { - var defaultTemplate = - _fileService.GetTemplate(defaultTemplateElement.Value.ToSafeAlias(_shortStringHelper)); - if (defaultTemplate != null) - { - contentType.SetDefaultTemplate(defaultTemplate); - } - else - { - _logger.LogWarning( - "Packager: Error handling default template. Default template with alias '{DefaultTemplateAlias}' could not be found.", - defaultTemplateElement.Value); - } + service.Save(updatedContentTypes, userId); } } - private void UpdateContentTypesPropertyGroups(T contentType, XElement? propertyGroupsContainer) - where T : IContentTypeComposition - { - if (propertyGroupsContainer == null) - return; + return list; + } + + private Dictionary CreateContentTypeFolderStructure(IEnumerable unsortedDocumentTypes, + out IEnumerable entityContainersInstalled) + { + var importedFolders = new Dictionary(); + var trackEntityContainersInstalled = new List(); - var propertyGroupElements = propertyGroupsContainer.Elements("Tab"); - foreach (var propertyGroupElement in propertyGroupElements) + foreach (XElement documentType in unsortedDocumentTypes) + { + XAttribute? foldersAttribute = documentType.Attribute("Folders"); + XElement? infoElement = documentType.Element("Info"); + if (foldersAttribute != null && infoElement != null + // don't import any folder if this is a child doc type - the parent doc type will need to + // exist which contains it's folders + && ((string?)infoElement.Element("Master")).IsNullOrWhiteSpace()) { - var name = propertyGroupElement.Element("Caption")! - .Value; // TODO Rename to Name (same in EntityXmlSerializer) + var alias = documentType.Element("Info")?.Element("Alias")?.Value; + var folders = foldersAttribute.Value.Split(Constants.CharArrays.ForwardSlash); + + XAttribute? folderKeysAttribute = documentType.Attribute("FolderKeys"); - var alias = propertyGroupElement.Element("Alias")?.Value; - if (string.IsNullOrEmpty(alias)) + Guid[] folderKeys = Array.Empty(); + if (folderKeysAttribute != null) { - alias = name.ToSafeAlias(_shortStringHelper, true); + folderKeys = folderKeysAttribute.Value.Split(Constants.CharArrays.ForwardSlash) + .Select(x => Guid.Parse(x)).ToArray(); } - contentType.AddPropertyGroup(alias, name); - var propertyGroup = contentType.PropertyGroups[alias]; + var rootFolder = WebUtility.UrlDecode(folders[0]); - if (Guid.TryParse(propertyGroupElement.Element("Key")?.Value, out var key)) + EntityContainer? current = null; + Guid? rootFolderKey = null; + if (folderKeys.Length == folders.Length && folderKeys.Length > 0) { - propertyGroup.Key = key; + rootFolderKey = folderKeys[0]; + current = _contentTypeService.GetContainer(rootFolderKey.Value); } - if (Enum.TryParse(propertyGroupElement.Element("Type")?.Value, out var type)) + // The folder might already exist, but with a different key, so check if it exists, even if there is a key. + // Level 1 = root level folders, there can only be one with the same name + current ??= _contentTypeService.GetContainers(rootFolder, 1)?.FirstOrDefault(); + + if (current == null) { - propertyGroup.Type = type; + Attempt?> tryCreateFolder = + _contentTypeService.CreateContainer(-1, rootFolderKey ?? Guid.NewGuid(), rootFolder); + + if (tryCreateFolder == false) + { + _logger.LogError(tryCreateFolder.Exception, "Could not create folder: {FolderName}", + rootFolder); + throw tryCreateFolder.Exception!; + } + + var rootFolderId = tryCreateFolder.Result?.Entity?.Id; + if (rootFolderId is not null) + { + current = _contentTypeService.GetContainer(rootFolderId.Value); + trackEntityContainersInstalled.Add(current!); + } } - if (int.TryParse(propertyGroupElement.Element("SortOrder")?.Value, NumberStyles.Integer, - CultureInfo.InvariantCulture, out var sortOrder)) + importedFolders.Add(alias!, current!.Id); + + for (var i = 1; i < folders.Length; i++) { - // Override the sort order with the imported value - propertyGroup.SortOrder = sortOrder; + var folderName = WebUtility.UrlDecode(folders[i]); + Guid? folderKey = folderKeys.Length == folders.Length ? folderKeys[i] : null; + current = CreateContentTypeChildFolder(folderName, folderKey ?? Guid.NewGuid(), current); + trackEntityContainersInstalled.Add(current!); + importedFolders[alias!] = current!.Id; } } } - private void UpdateContentTypesProperties(T contentType, XElement? genericPropertiesElement) - where T : IContentTypeComposition + entityContainersInstalled = trackEntityContainersInstalled; + return importedFolders; + } + + private EntityContainer? CreateContentTypeChildFolder(string folderName, Guid folderKey, IUmbracoEntity current) + { + IEntitySlim[] children = _entityService.GetChildren(current.Id).ToArray(); + var found = children.Any(x => x.Name.InvariantEquals(folderName) || x.Key.Equals(folderKey)); + if (found) { - if (genericPropertiesElement is null) - { - return; - } - var properties = genericPropertiesElement.Elements("GenericProperty"); - foreach (var property in properties) - { - var dataTypeDefinitionId = - new Guid(property.Element("Definition")!.Value); //Unique Id for a DataTypeDefinition + var containerId = children.Single(x => x.Name.InvariantEquals(folderName)).Id; + return _contentTypeService.GetContainer(containerId); + } - var dataTypeDefinition = _dataTypeService.GetDataType(dataTypeDefinitionId); + Attempt?> tryCreateFolder = + _contentTypeService.CreateContainer(current.Id, folderKey, folderName); + if (tryCreateFolder == false) + { + _logger.LogError(tryCreateFolder.Exception, "Could not create folder: {FolderName}", folderName); + throw tryCreateFolder.Exception!; + } - //If no DataTypeDefinition with the guid from the xml wasn't found OR the ControlId on the DataTypeDefinition didn't match the DataType Id - //We look up a DataTypeDefinition that matches + return _contentTypeService.GetContainer(tryCreateFolder.Result!.Entity!.Id); + } + private T CreateContentTypeFromXml( + XElement documentType, + IReadOnlyDictionary importedContentTypes, + IContentTypeBaseService service) + where T : class, IContentTypeComposition + { + var key = Guid.Parse(documentType.Element("Info")!.Element("Key")!.Value); - //get the alias as a string for use below - var propertyEditorAlias = property.Element("Type")!.Value.Trim(); + XElement infoElement = documentType.Element("Info")!; - //If no DataTypeDefinition with the guid from the xml wasn't found OR the ControlId on the DataTypeDefinition didn't match the DataType Id - //We look up a DataTypeDefinition that matches + //Name of the master corresponds to the parent + XElement? masterElement = infoElement.Element("Master"); - if (dataTypeDefinition == null) - { - var dataTypeDefinitions = _dataTypeService.GetByEditorAlias(propertyEditorAlias); - if (dataTypeDefinitions != null && dataTypeDefinitions.Any()) - { - dataTypeDefinition = dataTypeDefinitions.FirstOrDefault(); - } - } - else if (dataTypeDefinition.EditorAlias != propertyEditorAlias) - { - var dataTypeDefinitions = _dataTypeService.GetByEditorAlias(propertyEditorAlias); - if (dataTypeDefinitions != null && dataTypeDefinitions.Any()) - { - dataTypeDefinition = dataTypeDefinitions.FirstOrDefault(); - } - } - - // For backwards compatibility, if no datatype with that ID can be found, we're letting this fail silently. - // This means that the property will not be created. - if (dataTypeDefinition == null) - { - // TODO: We should expose this to the UI during install! - _logger.LogWarning( - "Packager: Error handling creation of PropertyType '{PropertyType}'. Could not find DataTypeDefintion with unique id '{DataTypeDefinitionId}' nor one referencing the DataType with a property editor alias (or legacy control id) '{PropertyEditorAlias}'. Did the package creator forget to package up custom datatypes? This property will be converted to a label/readonly editor if one exists.", - property.Element("Name")?.Value, dataTypeDefinitionId, property.Element("Type")?.Value.Trim()); - - //convert to a label! - dataTypeDefinition = _dataTypeService.GetByEditorAlias(Constants.PropertyEditors.Aliases.Label)? - .FirstOrDefault(); - //if for some odd reason this isn't there then ignore - if (dataTypeDefinition == null) - continue; - } + T? parent = default; + if (masterElement != null) + { + var masterAlias = masterElement.Value; + parent = importedContentTypes.ContainsKey(masterAlias) + ? importedContentTypes[masterAlias] + : service.Get(masterAlias); + } - var sortOrder = 0; - var sortOrderElement = property.Element("SortOrder"); - if (sortOrderElement != null) - { - int.TryParse(sortOrderElement.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, - out sortOrder); - } + var alias = infoElement?.Element("Alias")?.Value; + T? contentType = CreateContentType(key, parent, -1, alias!); - var propertyType = - new PropertyType(_shortStringHelper, dataTypeDefinition, property.Element("Alias")!.Value) - { - Name = property.Element("Name")!.Value, - Description = (string?)property.Element("Description"), - Mandatory = property.Element("Mandatory") is not null && property.Element("Mandatory")!.Value.ToLowerInvariant().Equals("true"), - MandatoryMessage = property.Element("MandatoryMessage") != null - ? (string?)property.Element("MandatoryMessage") - : string.Empty, - ValidationRegExp = (string?)property.Element("Validation"), - ValidationRegExpMessage = property.Element("ValidationRegExpMessage") != null - ? (string?)property.Element("ValidationRegExpMessage") - : string.Empty, - SortOrder = sortOrder, - Variations = property.Element("Variations") != null - ? (ContentVariation)Enum.Parse(typeof(ContentVariation), - property.Element("Variations")!.Value) - : ContentVariation.Nothing, - LabelOnTop = property.Element("LabelOnTop") != null && property.Element("LabelOnTop")!.Value.ToLowerInvariant().Equals("true") - }; - - if (property.Element("Key") != null) - { - propertyType.Key = new Guid(property.Element("Key")!.Value); - } + if (parent != null) + { + contentType?.AddContentType(parent); + } - var propertyGroupElement = property.Element("Tab"); - if (propertyGroupElement == null || string.IsNullOrEmpty(propertyGroupElement.Value)) - { - contentType.AddPropertyType(propertyType); - } - else - { - var propertyGroupName = propertyGroupElement.Value; - var propertyGroupAlias = propertyGroupElement.Attribute("Alias")?.Value; - if (string.IsNullOrEmpty(propertyGroupAlias)) - { - propertyGroupAlias = propertyGroupName.ToSafeAlias(_shortStringHelper, true); - } + return UpdateContentTypeFromXml(documentType, contentType, importedContentTypes, service); + } - contentType.AddPropertyType(propertyType, propertyGroupAlias, propertyGroupName); - } + private T? CreateContentType(Guid key, T? parent, int parentId, string alias) + where T : class, IContentTypeComposition + { + if (typeof(T) == typeof(IContentType)) + { + if (parent is null) + { + return new ContentType(_shortStringHelper, parentId) {Alias = alias, Key = key} as T; } + + return new ContentType(_shortStringHelper, (IContentType)parent, alias) {Key = key} as T; } - private T UpdateContentTypesStructure(T contentType, XElement structureElement, - IReadOnlyDictionary importedContentTypes, IContentTypeBaseService service) - where T : IContentTypeComposition + if (typeof(T) == typeof(IMediaType)) { - var allowedChildren = contentType.AllowedContentTypes?.ToList(); - int sortOrder = allowedChildren?.Any() ?? false ? allowedChildren.Last().SortOrder : 0; - foreach (var element in structureElement.Elements()) + if (parent is null) { - var alias = element.Value; + return new MediaType(_shortStringHelper, parentId) {Alias = alias, Key = key} as T; + } - var allowedChild = importedContentTypes.ContainsKey(alias) - ? importedContentTypes[alias] - : service.Get(alias); - if (allowedChild == null) - { - _logger.LogWarning( - "Packager: Error handling DocumentType structure. DocumentType with alias '{DoctypeAlias}' could not be found and was not added to the structure for '{DoctypeStructureAlias}'.", - alias, contentType.Alias); - continue; - } + return new MediaType(_shortStringHelper, (IMediaType)parent, alias) {Key = key} as T; + } - if (allowedChildren?.Any(x => x.Id.IsValueCreated && x.Id.Value == allowedChild.Id) ?? false) - continue; + throw new NotSupportedException($"Type {typeof(T)} is not supported"); + } - allowedChildren?.Add(new ContentTypeSort(new Lazy(() => allowedChild.Id), sortOrder, - allowedChild.Alias)); - sortOrder++; - } + private T UpdateContentTypeFromXml( + XElement documentType, + T? contentType, + IReadOnlyDictionary importedContentTypes, + IContentTypeBaseService service) + where T : IContentTypeComposition + { + var key = Guid.Parse(documentType.Element("Info")!.Element("Key")!.Value); - contentType.AllowedContentTypes = allowedChildren; - return contentType; + XElement? infoElement = documentType.Element("Info"); + XElement? defaultTemplateElement = infoElement?.Element("DefaultTemplate"); + + if (contentType is null) + { + throw new InvalidOperationException("Content type was null"); } - /// - /// Used during Content import to ensure that the ContentType of a content item exists - /// - /// - /// - private T FindContentTypeByAlias(string contentTypeAlias, IContentTypeBaseService typeService) - where T : IContentTypeComposition + contentType.Key = key; + contentType.Name = infoElement!.Element("Name")!.Value; + if (infoElement.Element("Key") != null) { - var contentType = typeService.Get(contentTypeAlias); + contentType.Key = new Guid(infoElement.Element("Key")!.Value); + } - if (contentType == null) - throw new Exception($"ContentType matching the passed in Alias: '{contentTypeAlias}' was null"); + contentType.Icon = infoElement.Element("Icon")?.Value; + contentType.Thumbnail = infoElement.Element("Thumbnail")?.Value; + contentType.Description = infoElement.Element("Description")?.Value; - return contentType; + //NOTE AllowAtRoot, IsListView, IsElement and Variations are new properties in the package xml so we need to verify it exists before using it. + XElement? allowAtRoot = infoElement.Element("AllowAtRoot"); + if (allowAtRoot != null) + { + contentType.AllowedAsRoot = allowAtRoot.Value.InvariantEquals("true"); } - #endregion + XElement? isListView = infoElement.Element("IsListView"); + if (isListView != null) + { + contentType.IsContainer = isListView.Value.InvariantEquals("true"); + } - #region DataTypes + XElement? isElement = infoElement.Element("IsElement"); + if (isElement != null) + { + contentType.IsElement = isElement.Value.InvariantEquals("true"); + } - /// - /// Imports and saves package xml as - /// - /// Xml to import - /// Optional id of the user - /// An enumerable list of generated DataTypeDefinitions - public IReadOnlyList ImportDataTypes(IReadOnlyCollection dataTypeElements, int userId) - => ImportDataTypes(dataTypeElements, userId, out _); + XElement? variationsElement = infoElement.Element("Variations"); + if (variationsElement != null) + { + contentType.Variations = + (ContentVariation)Enum.Parse(typeof(ContentVariation), variationsElement.Value); + } - /// - /// Imports and saves package xml as - /// - /// Xml to import - /// Optional id of the user - /// Collection of entity containers installed by the package to be populated with those created in installing data types. - /// An enumerable list of generated DataTypeDefinitions - public IReadOnlyList ImportDataTypes(IReadOnlyCollection dataTypeElements, int userId, - out IEnumerable entityContainersInstalled) + //Name of the master corresponds to the parent and we need to ensure that the Parent Id is set + XElement? masterElement = infoElement.Element("Master"); + if (masterElement != null) { - var dataTypes = new List(); + var masterAlias = masterElement.Value; + T? parent = importedContentTypes.ContainsKey(masterAlias) + ? importedContentTypes[masterAlias] + : service.Get(masterAlias); - var importedFolders = CreateDataTypeFolderStructure(dataTypeElements, out entityContainersInstalled); + contentType.SetParent(parent); + } - foreach (var dataTypeElement in dataTypeElements) + //Update Compositions on the ContentType to ensure that they are as is defined in the package xml + XElement? compositionsElement = infoElement.Element("Compositions"); + if (compositionsElement != null && compositionsElement.HasElements) + { + IEnumerable compositions = compositionsElement.Elements("Composition"); + if (compositions.Any()) { - var dataTypeDefinitionName = dataTypeElement.AttributeValue("Name"); - - var dataTypeDefinitionId = dataTypeElement.RequiredAttributeValue("Definition"); - var databaseTypeAttribute = dataTypeElement.Attribute("DatabaseType"); - - var parentId = -1; - if (dataTypeDefinitionName is not null && importedFolders.ContainsKey(dataTypeDefinitionName)) - parentId = importedFolders[dataTypeDefinitionName]; - - var definition = _dataTypeService.GetDataType(dataTypeDefinitionId); - //If the datatype definition doesn't already exist we create a new according to the one in the package xml - if (definition == null) + foreach (XElement composition in compositions) { - var databaseType = databaseTypeAttribute?.Value.EnumParse(true) ?? - ValueStorageType.Ntext; - - // the Id field is actually the string property editor Alias - // however, the actual editor with this alias could be installed with the package, and - // therefore not yet part of the _propertyEditors collection, so we cannot try and get - // the actual editor - going with a void editor - - var editorAlias = dataTypeElement.Attribute("Id")?.Value?.Trim(); - if (!_propertyEditors.TryGet(editorAlias, out var editor)) - editor = new VoidEditor(_dataValueEditorFactory) {Alias = editorAlias ?? string.Empty}; - - var dataType = new DataType(editor, _serializer) - { - Key = dataTypeDefinitionId, - Name = dataTypeDefinitionName, - DatabaseType = databaseType, - ParentId = parentId - }; - - var configurationAttributeValue = dataTypeElement.Attribute("Configuration")?.Value; - if (!string.IsNullOrWhiteSpace(configurationAttributeValue)) - dataType.Configuration = editor.GetConfigurationEditor() - .FromDatabase(configurationAttributeValue, _serializer); - - dataTypes.Add(dataType); + var compositionAlias = composition.Value; + T? compositionContentType = importedContentTypes.ContainsKey(compositionAlias) + ? importedContentTypes[compositionAlias] + : service.Get(compositionAlias); + contentType.AddContentType(compositionContentType); } - else - { - definition.ParentId = parentId; - _dataTypeService.Save(definition, userId); - } - } - - if (dataTypes.Count > 0) - { - _dataTypeService.Save(dataTypes, userId); } + } - return dataTypes; + if (contentType is IContentType contentTypex) + { + UpdateContentTypesAllowedTemplates(contentTypex, infoElement.Element("AllowedTemplates"), + defaultTemplateElement); } - private Dictionary CreateDataTypeFolderStructure(IEnumerable datatypeElements, - out IEnumerable entityContainersInstalled) + UpdateContentTypesPropertyGroups(contentType, documentType.Element("Tabs")); + UpdateContentTypesProperties(contentType, documentType.Element("GenericProperties")); + + if (contentType is IContentTypeWithHistoryCleanup withCleanup) { - var importedFolders = new Dictionary(); - var trackEntityContainersInstalled = new List(); - foreach (var datatypeElement in datatypeElements) - { - var foldersAttribute = datatypeElement.Attribute("Folders"); + UpdateHistoryCleanupPolicy(withCleanup, documentType.Element("HistoryCleanupPolicy")); + } - if (foldersAttribute != null) - { - var name = datatypeElement.Attribute("Name")?.Value; - var folders = foldersAttribute.Value.Split(Constants.CharArrays.ForwardSlash); - var folderKeysAttribute = datatypeElement.Attribute("FolderKeys"); + return contentType; + } - var folderKeys = Array.Empty(); - if (folderKeysAttribute != null) - { - folderKeys = folderKeysAttribute.Value.Split(Constants.CharArrays.ForwardSlash) - .Select(x => Guid.Parse(x)).ToArray(); - } + private void UpdateHistoryCleanupPolicy(IContentTypeWithHistoryCleanup withCleanup, XElement? element) + { + if (element == null) + { + return; + } - var rootFolder = WebUtility.UrlDecode(folders[0]); - var rootFolderKey = folderKeys.Length > 0 ? folderKeys[0] : Guid.NewGuid(); - //there will only be a single result by name for level 1 (root) containers - var current = _dataTypeService.GetContainers(rootFolder, 1).FirstOrDefault(); + withCleanup.HistoryCleanup ??= new HistoryCleanup(); - if (current == null) - { - var tryCreateFolder = _dataTypeService.CreateContainer(-1, rootFolderKey, rootFolder); - if (tryCreateFolder == false) - { - _logger.LogError(tryCreateFolder.Exception, "Could not create folder: {FolderName}", - rootFolder); - throw tryCreateFolder.Exception!; - } + if (bool.TryParse(element.Attribute("preventCleanup")?.Value, out var preventCleanup)) + { + withCleanup.HistoryCleanup.PreventCleanup = preventCleanup; + } - current = _dataTypeService.GetContainer(tryCreateFolder.Result!.Entity!.Id); - trackEntityContainersInstalled.Add(current!); - } + if (int.TryParse(element.Attribute("keepAllVersionsNewerThanDays")?.Value, out var keepAll)) + { + withCleanup.HistoryCleanup.KeepAllVersionsNewerThanDays = keepAll; + } + else + { + withCleanup.HistoryCleanup.KeepAllVersionsNewerThanDays = null; + } - importedFolders.Add(name!, current!.Id); + if (int.TryParse(element.Attribute("keepLatestVersionPerDayForDays")?.Value, out var keepLatest)) + { + withCleanup.HistoryCleanup.KeepLatestVersionPerDayForDays = keepLatest; + } + else + { + withCleanup.HistoryCleanup.KeepLatestVersionPerDayForDays = null; + } + } - for (var i = 1; i < folders.Length; i++) + private void UpdateContentTypesAllowedTemplates(IContentType contentType, XElement? allowedTemplatesElement, + XElement? defaultTemplateElement) + { + if (allowedTemplatesElement != null && allowedTemplatesElement.Elements("Template").Any()) + { + var allowedTemplates = contentType.AllowedTemplates?.ToList(); + foreach (XElement templateElement in allowedTemplatesElement.Elements("Template")) + { + var alias = templateElement.Value; + ITemplate? template = _fileService.GetTemplate(alias.ToSafeAlias(_shortStringHelper)); + if (template != null) + { + if (allowedTemplates?.Any(x => x.Id == template.Id) ?? true) { - var folderName = WebUtility.UrlDecode(folders[i]); - Guid? folderKey = (folderKeys.Length == folders.Length) ? folderKeys[i] : null; - current = CreateDataTypeChildFolder(folderName, folderKey ?? Guid.NewGuid(), current!); - trackEntityContainersInstalled.Add(current!); - importedFolders[name!] = current!.Id; + continue; } + + allowedTemplates.Add(template); + } + else + { + _logger.LogWarning( + "Packager: Error handling allowed templates. Template with alias '{TemplateAlias}' could not be found.", + alias); } } - entityContainersInstalled = trackEntityContainersInstalled; - return importedFolders; + contentType.AllowedTemplates = allowedTemplates; } - private EntityContainer? CreateDataTypeChildFolder(string folderName, Guid folderKey, IUmbracoEntity current) + if (string.IsNullOrEmpty((string?)defaultTemplateElement) == false) { - var children = _entityService.GetChildren(current.Id).ToArray(); - var found = children.Any(x => x.Name.InvariantEquals(folderName) || x.Key.Equals(folderKey)); - if (found) + ITemplate? defaultTemplate = + _fileService.GetTemplate(defaultTemplateElement.Value.ToSafeAlias(_shortStringHelper)); + if (defaultTemplate != null) { - var containerId = children.Single(x => x.Name.InvariantEquals(folderName)).Id; - return _dataTypeService.GetContainer(containerId); + contentType.SetDefaultTemplate(defaultTemplate); } - - var tryCreateFolder = _dataTypeService.CreateContainer(current.Id, folderKey, folderName); - if (tryCreateFolder == false) + else { - _logger.LogError(tryCreateFolder.Exception, "Could not create folder: {FolderName}", folderName); - throw tryCreateFolder.Exception!; + _logger.LogWarning( + "Packager: Error handling default template. Default template with alias '{DefaultTemplateAlias}' could not be found.", + defaultTemplateElement.Value); } - - return _dataTypeService.GetContainer(tryCreateFolder.Result!.Entity!.Id); } + } - #endregion - - #region Dictionary Items - - /// - /// Imports and saves the 'DictionaryItems' part of the package xml as a list of - /// - /// Xml to import - /// - /// An enumerable list of dictionary items - public IReadOnlyList ImportDictionaryItems(IEnumerable dictionaryItemElementList, - int userId) + private void UpdateContentTypesPropertyGroups(T contentType, XElement? propertyGroupsContainer) + where T : IContentTypeComposition + { + if (propertyGroupsContainer == null) { - var languages = _localizationService.GetAllLanguages().ToList(); - return ImportDictionaryItems(dictionaryItemElementList, languages, null, userId); + return; } - private IReadOnlyList ImportDictionaryItems(IEnumerable dictionaryItemElementList, - List languages, Guid? parentId, int userId) + IEnumerable propertyGroupElements = propertyGroupsContainer.Elements("Tab"); + foreach (XElement propertyGroupElement in propertyGroupElements) { - var items = new List(); - foreach (XElement dictionaryItemElement in dictionaryItemElementList) + var name = propertyGroupElement.Element("Caption")! + .Value; // TODO Rename to Name (same in EntityXmlSerializer) + + var alias = propertyGroupElement.Element("Alias")?.Value; + if (string.IsNullOrEmpty(alias)) + { + alias = name.ToSafeAlias(_shortStringHelper, true); + } + + contentType.AddPropertyGroup(alias, name); + PropertyGroup propertyGroup = contentType.PropertyGroups[alias]; + + if (Guid.TryParse(propertyGroupElement.Element("Key")?.Value, out Guid key)) + { + propertyGroup.Key = key; + } + + if (Enum.TryParse(propertyGroupElement.Element("Type")?.Value, out PropertyGroupType type)) + { + propertyGroup.Type = type; + } + + if (int.TryParse(propertyGroupElement.Element("SortOrder")?.Value, NumberStyles.Integer, + CultureInfo.InvariantCulture, out var sortOrder)) { - items.AddRange(ImportDictionaryItem(dictionaryItemElement, languages, parentId, userId)); + // Override the sort order with the imported value + propertyGroup.SortOrder = sortOrder; } + } + } - return items; + private void UpdateContentTypesProperties(T contentType, XElement? genericPropertiesElement) + where T : IContentTypeComposition + { + if (genericPropertiesElement is null) + { + return; } - private IEnumerable ImportDictionaryItem(XElement dictionaryItemElement, - List languages, Guid? parentId, int userId) + IEnumerable properties = genericPropertiesElement.Elements("GenericProperty"); + foreach (XElement property in properties) { - var items = new List(); + var dataTypeDefinitionId = + new Guid(property.Element("Definition")!.Value); //Unique Id for a DataTypeDefinition + + IDataType? dataTypeDefinition = _dataTypeService.GetDataType(dataTypeDefinitionId); - IDictionaryItem? dictionaryItem; - var itemName = dictionaryItemElement.Attribute("Name")?.Value; - Guid key = dictionaryItemElement.RequiredAttributeValue("Key"); + //If no DataTypeDefinition with the guid from the xml wasn't found OR the ControlId on the DataTypeDefinition didn't match the DataType Id + //We look up a DataTypeDefinition that matches - dictionaryItem = _localizationService.GetDictionaryItemById(key); - if (dictionaryItem != null) + + //get the alias as a string for use below + var propertyEditorAlias = property.Element("Type")!.Value.Trim(); + + //If no DataTypeDefinition with the guid from the xml wasn't found OR the ControlId on the DataTypeDefinition didn't match the DataType Id + //We look up a DataTypeDefinition that matches + + if (dataTypeDefinition == null) { - dictionaryItem = UpdateDictionaryItem(dictionaryItem, dictionaryItemElement, languages); + IEnumerable? dataTypeDefinitions = _dataTypeService.GetByEditorAlias(propertyEditorAlias); + if (dataTypeDefinitions != null && dataTypeDefinitions.Any()) + { + dataTypeDefinition = dataTypeDefinitions.FirstOrDefault(); + } } - else + else if (dataTypeDefinition.EditorAlias != propertyEditorAlias) { - dictionaryItem = CreateNewDictionaryItem(key, itemName!, dictionaryItemElement, languages, parentId); + IEnumerable? dataTypeDefinitions = _dataTypeService.GetByEditorAlias(propertyEditorAlias); + if (dataTypeDefinitions != null && dataTypeDefinitions.Any()) + { + dataTypeDefinition = dataTypeDefinitions.FirstOrDefault(); + } } - _localizationService.Save(dictionaryItem, userId); - items.Add(dictionaryItem); + // For backwards compatibility, if no datatype with that ID can be found, we're letting this fail silently. + // This means that the property will not be created. + if (dataTypeDefinition == null) + { + // TODO: We should expose this to the UI during install! + _logger.LogWarning( + "Packager: Error handling creation of PropertyType '{PropertyType}'. Could not find DataTypeDefintion with unique id '{DataTypeDefinitionId}' nor one referencing the DataType with a property editor alias (or legacy control id) '{PropertyEditorAlias}'. Did the package creator forget to package up custom datatypes? This property will be converted to a label/readonly editor if one exists.", + property.Element("Name")?.Value, dataTypeDefinitionId, property.Element("Type")?.Value.Trim()); - items.AddRange(ImportDictionaryItems(dictionaryItemElement.Elements("DictionaryItem"), languages, - dictionaryItem.Key, userId)); - return items; - } + //convert to a label! + dataTypeDefinition = _dataTypeService.GetByEditorAlias(Constants.PropertyEditors.Aliases.Label)? + .FirstOrDefault(); + //if for some odd reason this isn't there then ignore + if (dataTypeDefinition == null) + { + continue; + } + } - private IDictionaryItem UpdateDictionaryItem(IDictionaryItem dictionaryItem, XElement dictionaryItemElement, - List languages) - { - var translations = dictionaryItem.Translations.ToList(); - foreach (var valueElement in dictionaryItemElement.Elements("Value") - .Where(v => DictionaryValueIsNew(translations, v))) + var sortOrder = 0; + XElement? sortOrderElement = property.Element("SortOrder"); + if (sortOrderElement != null) { - AddDictionaryTranslation(translations, valueElement, languages); + int.TryParse(sortOrderElement.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, + out sortOrder); } - dictionaryItem.Translations = translations; - return dictionaryItem; - } - - private static DictionaryItem CreateNewDictionaryItem(Guid itemId, string itemName, - XElement dictionaryItemElement, List languages, Guid? parentId) - { - DictionaryItem dictionaryItem = parentId.HasValue - ? new DictionaryItem(parentId.Value, itemName) - : new DictionaryItem(itemName); - dictionaryItem.Key = itemId; + var propertyType = + new PropertyType(_shortStringHelper, dataTypeDefinition, property.Element("Alias")!.Value) + { + Name = property.Element("Name")!.Value, + Description = (string?)property.Element("Description"), + Mandatory = property.Element("Mandatory") is not null && + property.Element("Mandatory")!.Value.ToLowerInvariant().Equals("true"), + MandatoryMessage = property.Element("MandatoryMessage") != null + ? (string?)property.Element("MandatoryMessage") + : string.Empty, + ValidationRegExp = (string?)property.Element("Validation"), + ValidationRegExpMessage = property.Element("ValidationRegExpMessage") != null + ? (string?)property.Element("ValidationRegExpMessage") + : string.Empty, + SortOrder = sortOrder, + Variations = property.Element("Variations") != null + ? (ContentVariation)Enum.Parse(typeof(ContentVariation), + property.Element("Variations")!.Value) + : ContentVariation.Nothing, + LabelOnTop = property.Element("LabelOnTop") != null && + property.Element("LabelOnTop")!.Value.ToLowerInvariant().Equals("true") + }; - var translations = new List(); + if (property.Element("Key") != null) + { + propertyType.Key = new Guid(property.Element("Key")!.Value); + } - foreach (XElement valueElement in dictionaryItemElement.Elements("Value")) + XElement? propertyGroupElement = property.Element("Tab"); + if (propertyGroupElement == null || string.IsNullOrEmpty(propertyGroupElement.Value)) { - AddDictionaryTranslation(translations, valueElement, languages); + contentType.AddPropertyType(propertyType); } + else + { + var propertyGroupName = propertyGroupElement.Value; + var propertyGroupAlias = propertyGroupElement.Attribute("Alias")?.Value; + if (string.IsNullOrEmpty(propertyGroupAlias)) + { + propertyGroupAlias = propertyGroupName.ToSafeAlias(_shortStringHelper, true); + } - dictionaryItem.Translations = translations; - return dictionaryItem; + contentType.AddPropertyType(propertyType, propertyGroupAlias, propertyGroupName); + } } + } - private static bool DictionaryValueIsNew(IEnumerable translations, - XElement valueElement) - => translations.All(t => string.Compare(t.Language?.IsoCode, - valueElement.Attribute("LanguageCultureAlias")?.Value, - StringComparison.InvariantCultureIgnoreCase) != - 0); - - private static void AddDictionaryTranslation(ICollection translations, - XElement valueElement, IEnumerable languages) + private T UpdateContentTypesStructure(T contentType, XElement structureElement, + IReadOnlyDictionary importedContentTypes, IContentTypeBaseService service) + where T : IContentTypeComposition + { + var allowedChildren = contentType.AllowedContentTypes?.ToList(); + var sortOrder = allowedChildren?.Any() ?? false ? allowedChildren.Last().SortOrder : 0; + foreach (XElement element in structureElement.Elements()) { - var languageId = valueElement.Attribute("LanguageCultureAlias")?.Value; - var language = languages.SingleOrDefault(l => l.IsoCode == languageId); - if (language == null) + var alias = element.Value; + + T? allowedChild = importedContentTypes.ContainsKey(alias) + ? importedContentTypes[alias] + : service.Get(alias); + if (allowedChild == null) { - return; + _logger.LogWarning( + "Packager: Error handling DocumentType structure. DocumentType with alias '{DoctypeAlias}' could not be found and was not added to the structure for '{DoctypeStructureAlias}'.", + alias, contentType.Alias); + continue; } - var translation = new DictionaryTranslation(language, valueElement.Value); - translations.Add(translation); + if (allowedChildren?.Any(x => x.Id.IsValueCreated && x.Id.Value == allowedChild.Id) ?? false) + { + continue; + } + + allowedChildren?.Add(new ContentTypeSort(new Lazy(() => allowedChild.Id), sortOrder, + allowedChild.Alias)); + sortOrder++; } - #endregion + contentType.AllowedContentTypes = allowedChildren; + return contentType; + } - #region Languages + /// + /// Used during Content import to ensure that the ContentType of a content item exists + /// + /// + /// + private T FindContentTypeByAlias(string contentTypeAlias, IContentTypeBaseService typeService) + where T : IContentTypeComposition + { + T? contentType = typeService.Get(contentTypeAlias); - /// - /// Imports and saves the 'Languages' part of a package xml as a list of - /// - /// Xml to import - /// Optional id of the User performing the operation - /// An enumerable list of generated languages - public IReadOnlyList ImportLanguages(IEnumerable languageElements, int userId) + if (contentType == null) { - var list = new List(); - foreach (var languageElement in languageElements) - { - var isoCode = languageElement.AttributeValue("CultureAlias"); - if (string.IsNullOrEmpty(isoCode)) - { - continue; - } + throw new Exception($"ContentType matching the passed in Alias: '{contentTypeAlias}' was null"); + } - var existingLanguage = _localizationService.GetLanguageByIsoCode(isoCode); - if (existingLanguage != null) - { - continue; - } + return contentType; + } + + #endregion + + #region DataTypes + + /// + /// Imports and saves package xml as + /// + /// Xml to import + /// Optional id of the user + /// An enumerable list of generated DataTypeDefinitions + public IReadOnlyList ImportDataTypes(IReadOnlyCollection dataTypeElements, int userId) + => ImportDataTypes(dataTypeElements, userId, out _); + + /// + /// Imports and saves package xml as + /// + /// Xml to import + /// Optional id of the user + /// + /// Collection of entity containers installed by the package to be populated with + /// those created in installing data types. + /// + /// An enumerable list of generated DataTypeDefinitions + public IReadOnlyList ImportDataTypes(IReadOnlyCollection dataTypeElements, int userId, + out IEnumerable entityContainersInstalled) + { + var dataTypes = new List(); - var cultureName = languageElement.AttributeValue("FriendlyName") ?? isoCode; + Dictionary importedFolders = + CreateDataTypeFolderStructure(dataTypeElements, out entityContainersInstalled); - var langauge = new Language(isoCode, cultureName); - _localizationService.Save(langauge, userId); + foreach (XElement dataTypeElement in dataTypeElements) + { + var dataTypeDefinitionName = dataTypeElement.AttributeValue("Name"); + + Guid dataTypeDefinitionId = dataTypeElement.RequiredAttributeValue("Definition"); + XAttribute? databaseTypeAttribute = dataTypeElement.Attribute("DatabaseType"); - list.Add(langauge); + var parentId = -1; + if (dataTypeDefinitionName is not null && importedFolders.ContainsKey(dataTypeDefinitionName)) + { + parentId = importedFolders[dataTypeDefinitionName]; } - return list; - } + IDataType? definition = _dataTypeService.GetDataType(dataTypeDefinitionId); + //If the datatype definition doesn't already exist we create a new according to the one in the package xml + if (definition == null) + { + ValueStorageType databaseType = databaseTypeAttribute?.Value.EnumParse(true) ?? + ValueStorageType.Ntext; - #endregion + // the Id field is actually the string property editor Alias + // however, the actual editor with this alias could be installed with the package, and + // therefore not yet part of the _propertyEditors collection, so we cannot try and get + // the actual editor - going with a void editor - #region Macros + var editorAlias = dataTypeElement.Attribute("Id")?.Value?.Trim(); + if (!_propertyEditors.TryGet(editorAlias, out IDataEditor? editor)) + { + editor = new VoidEditor(_dataValueEditorFactory) {Alias = editorAlias ?? string.Empty}; + } - /// - /// Imports and saves the 'Macros' part of a package xml as a list of - /// - /// Xml to import - /// Optional id of the User performing the operation - /// - public IReadOnlyList ImportMacros( - IEnumerable macroElements, - int userId) - { - var macros = macroElements.Select(ParseMacroElement).ToList(); + var dataType = new DataType(editor, _serializer) + { + Key = dataTypeDefinitionId, + Name = dataTypeDefinitionName, + DatabaseType = databaseType, + ParentId = parentId + }; - foreach (IMacro macro in macros) + var configurationAttributeValue = dataTypeElement.Attribute("Configuration")?.Value; + if (!string.IsNullOrWhiteSpace(configurationAttributeValue)) + { + dataType.Configuration = editor.GetConfigurationEditor() + .FromDatabase(configurationAttributeValue, _serializer); + } + + dataTypes.Add(dataType); + } + else { - _macroService.Save(macro, userId); + definition.ParentId = parentId; + _dataTypeService.Save(definition, userId); } + } - return macros; + if (dataTypes.Count > 0) + { + _dataTypeService.Save(dataTypes, userId); } - public IReadOnlyList ImportMacroPartialViews(IEnumerable macroPartialViewsElements, - int userId) + return dataTypes; + } + + private Dictionary CreateDataTypeFolderStructure(IEnumerable datatypeElements, + out IEnumerable entityContainersInstalled) + { + var importedFolders = new Dictionary(); + var trackEntityContainersInstalled = new List(); + foreach (XElement datatypeElement in datatypeElements) { - var result = new List(); + XAttribute? foldersAttribute = datatypeElement.Attribute("Folders"); - foreach (XElement macroPartialViewXml in macroPartialViewsElements) + if (foldersAttribute != null) { - var path = macroPartialViewXml.AttributeValue("path"); - if (path == null) - { - throw new InvalidOperationException("No path attribute found"); - } + var name = datatypeElement.Attribute("Name")?.Value; + var folders = foldersAttribute.Value.Split(Constants.CharArrays.ForwardSlash); + XAttribute? folderKeysAttribute = datatypeElement.Attribute("FolderKeys"); - // Remove prefix to maintain backwards compatibility - if (path.StartsWith(Constants.SystemDirectories.MacroPartials)) + Guid[] folderKeys = Array.Empty(); + if (folderKeysAttribute != null) { - path = path.Substring(Constants.SystemDirectories.MacroPartials.Length); + folderKeys = folderKeysAttribute.Value.Split(Constants.CharArrays.ForwardSlash) + .Select(x => Guid.Parse(x)).ToArray(); } - else if (path.StartsWith("~")) + + var rootFolder = WebUtility.UrlDecode(folders[0]); + Guid rootFolderKey = folderKeys.Length > 0 ? folderKeys[0] : Guid.NewGuid(); + //there will only be a single result by name for level 1 (root) containers + EntityContainer? current = _dataTypeService.GetContainers(rootFolder, 1).FirstOrDefault(); + + if (current == null) { - _logger.LogWarning( - "Importing macro partial views outside of the Views/MacroPartials directory is not supported: {Path}", - path); - continue; + Attempt?> tryCreateFolder = + _dataTypeService.CreateContainer(-1, rootFolderKey, rootFolder); + if (tryCreateFolder == false) + { + _logger.LogError(tryCreateFolder.Exception, "Could not create folder: {FolderName}", + rootFolder); + throw tryCreateFolder.Exception!; + } + + current = _dataTypeService.GetContainer(tryCreateFolder.Result!.Entity!.Id); + trackEntityContainersInstalled.Add(current!); } - IPartialView? macroPartialView = _fileService.GetPartialViewMacro(path); + importedFolders.Add(name!, current!.Id); - // only update if it doesn't exist - if (macroPartialView == null) + for (var i = 1; i < folders.Length; i++) { - var content = macroPartialViewXml.Value ?? string.Empty; - - macroPartialView = new PartialView(PartialViewType.PartialViewMacro, path) {Content = content}; - _fileService.SavePartialViewMacro(macroPartialView, userId); - result.Add(macroPartialView); + var folderName = WebUtility.UrlDecode(folders[i]); + Guid? folderKey = folderKeys.Length == folders.Length ? folderKeys[i] : null; + current = CreateDataTypeChildFolder(folderName, folderKey ?? Guid.NewGuid(), current!); + trackEntityContainersInstalled.Add(current!); + importedFolders[name!] = current!.Id; } } + } - return result; + entityContainersInstalled = trackEntityContainersInstalled; + return importedFolders; + } + + private EntityContainer? CreateDataTypeChildFolder(string folderName, Guid folderKey, IUmbracoEntity current) + { + IEntitySlim[] children = _entityService.GetChildren(current.Id).ToArray(); + var found = children.Any(x => x.Name.InvariantEquals(folderName) || x.Key.Equals(folderKey)); + if (found) + { + var containerId = children.Single(x => x.Name.InvariantEquals(folderName)).Id; + return _dataTypeService.GetContainer(containerId); } - private IMacro ParseMacroElement(XElement macroElement) + Attempt?> tryCreateFolder = + _dataTypeService.CreateContainer(current.Id, folderKey, folderName); + if (tryCreateFolder == false) { - var macroKey = Guid.Parse(macroElement.Element("key")!.Value); - var macroName = macroElement.Element("name")?.Value; - var macroAlias = macroElement.Element("alias")!.Value; - var macroSource = macroElement.Element("macroSource")!.Value; + _logger.LogError(tryCreateFolder.Exception, "Could not create folder: {FolderName}", folderName); + throw tryCreateFolder.Exception!; + } - //Following xml elements are treated as nullable properties - var useInEditorElement = macroElement.Element("useInEditor"); - var useInEditor = false; - if (useInEditorElement != null && string.IsNullOrEmpty((string)useInEditorElement) == false) - { - useInEditor = bool.Parse(useInEditorElement.Value); - } + return _dataTypeService.GetContainer(tryCreateFolder.Result!.Entity!.Id); + } - var cacheDurationElement = macroElement.Element("refreshRate"); - var cacheDuration = 0; - if (cacheDurationElement != null && string.IsNullOrEmpty((string)cacheDurationElement) == false) - { - cacheDuration = int.Parse(cacheDurationElement.Value, CultureInfo.InvariantCulture); - } + #endregion - var cacheByMemberElement = macroElement.Element("cacheByMember"); - var cacheByMember = false; - if (cacheByMemberElement != null && string.IsNullOrEmpty((string)cacheByMemberElement) == false) - { - cacheByMember = bool.Parse(cacheByMemberElement.Value); - } + #region Dictionary Items - var cacheByPageElement = macroElement.Element("cacheByPage"); - var cacheByPage = false; - if (cacheByPageElement != null && string.IsNullOrEmpty((string)cacheByPageElement) == false) - { - cacheByPage = bool.Parse(cacheByPageElement.Value); - } + /// + /// Imports and saves the 'DictionaryItems' part of the package xml as a list of + /// + /// Xml to import + /// + /// An enumerable list of dictionary items + public IReadOnlyList ImportDictionaryItems(IEnumerable dictionaryItemElementList, + int userId) + { + var languages = _localizationService.GetAllLanguages().ToList(); + return ImportDictionaryItems(dictionaryItemElementList, languages, null, userId); + } - var dontRenderElement = macroElement.Element("dontRender"); - var dontRender = true; - if (dontRenderElement != null && string.IsNullOrEmpty((string)dontRenderElement) == false) - { - dontRender = bool.Parse(dontRenderElement.Value); - } + private IReadOnlyList ImportDictionaryItems(IEnumerable dictionaryItemElementList, + List languages, Guid? parentId, int userId) + { + var items = new List(); + foreach (XElement dictionaryItemElement in dictionaryItemElementList) + { + items.AddRange(ImportDictionaryItem(dictionaryItemElement, languages, parentId, userId)); + } - var existingMacro = _macroService.GetById(macroKey) as Macro; - var macro = existingMacro ?? new Macro(_shortStringHelper, macroAlias, macroName, macroSource, - cacheByPage, cacheByMember, dontRender, useInEditor, cacheDuration) {Key = macroKey}; + return items; + } - var properties = macroElement.Element("properties"); - if (properties != null) - { - int sortOrder = 0; - foreach (XElement property in properties.Elements()) - { - var propertyKey = property.RequiredAttributeValue("key"); - var propertyName = property.Attribute("name")?.Value; - var propertyAlias = property.Attribute("alias")!.Value; - var editorAlias = property.Attribute("propertyType")!.Value; - XAttribute? sortOrderAttribute = property.Attribute("sortOrder"); - if (sortOrderAttribute != null) - { - sortOrder = int.Parse(sortOrderAttribute.Value, CultureInfo.InvariantCulture); - } + private IEnumerable ImportDictionaryItem(XElement dictionaryItemElement, + List languages, Guid? parentId, int userId) + { + var items = new List(); - if (macro.Properties.Values.Any(x => - string.Equals(x.Alias, propertyAlias, StringComparison.OrdinalIgnoreCase))) - { - continue; - } + IDictionaryItem? dictionaryItem; + var itemName = dictionaryItemElement.Attribute("Name")?.Value; + Guid key = dictionaryItemElement.RequiredAttributeValue("Key"); - macro.Properties.Add(new MacroProperty(propertyAlias, propertyName, sortOrder, editorAlias) - { - Key = propertyKey - }); + dictionaryItem = _localizationService.GetDictionaryItemById(key); + if (dictionaryItem != null) + { + dictionaryItem = UpdateDictionaryItem(dictionaryItem, dictionaryItemElement, languages); + } + else + { + dictionaryItem = CreateNewDictionaryItem(key, itemName!, dictionaryItemElement, languages, parentId); + } - sortOrder++; - } - } + _localizationService.Save(dictionaryItem, userId); + items.Add(dictionaryItem); + + items.AddRange(ImportDictionaryItems(dictionaryItemElement.Elements("DictionaryItem"), languages, + dictionaryItem.Key, userId)); + return items; + } - return macro; + private IDictionaryItem UpdateDictionaryItem(IDictionaryItem dictionaryItem, XElement dictionaryItemElement, + List languages) + { + var translations = dictionaryItem.Translations.ToList(); + foreach (XElement valueElement in dictionaryItemElement.Elements("Value") + .Where(v => DictionaryValueIsNew(translations, v))) + { + AddDictionaryTranslation(translations, valueElement, languages); } - #endregion + dictionaryItem.Translations = translations; + return dictionaryItem; + } - public IReadOnlyList ImportScripts(IEnumerable scriptElements, int userId) - { - var result = new List(); + private static DictionaryItem CreateNewDictionaryItem(Guid itemId, string itemName, + XElement dictionaryItemElement, List languages, Guid? parentId) + { + DictionaryItem dictionaryItem = parentId.HasValue + ? new DictionaryItem(parentId.Value, itemName) + : new DictionaryItem(itemName); + dictionaryItem.Key = itemId; - foreach (XElement scriptXml in scriptElements) - { - var path = scriptXml.AttributeValue("path"); - if (path.IsNullOrWhiteSpace()) - { - continue; - } + var translations = new List(); - IScript? script = _fileService.GetScript(path!); + foreach (XElement valueElement in dictionaryItemElement.Elements("Value")) + { + AddDictionaryTranslation(translations, valueElement, languages); + } - // only update if it doesn't exist - if (script == null) - { - var content = scriptXml.Value; - if (content == null) - { - continue; - } + dictionaryItem.Translations = translations; + return dictionaryItem; + } - script = new Script(path!) {Content = content}; - _fileService.SaveScript(script, userId); - result.Add(script); - } - } + private static bool DictionaryValueIsNew(IEnumerable translations, + XElement valueElement) + => translations.All(t => string.Compare(t.Language?.IsoCode, + valueElement.Attribute("LanguageCultureAlias")?.Value, + StringComparison.InvariantCultureIgnoreCase) != + 0); - return result; + private static void AddDictionaryTranslation(ICollection translations, + XElement valueElement, IEnumerable languages) + { + var languageId = valueElement.Attribute("LanguageCultureAlias")?.Value; + ILanguage? language = languages.SingleOrDefault(l => l.IsoCode == languageId); + if (language == null) + { + return; } - public IReadOnlyList ImportPartialViews(IEnumerable partialViewElements, int userId) + var translation = new DictionaryTranslation(language, valueElement.Value); + translations.Add(translation); + } + + #endregion + + #region Macros + + /// + /// Imports and saves the 'Macros' part of a package xml as a list of + /// + /// Xml to import + /// Optional id of the User performing the operation + /// + public IReadOnlyList ImportMacros( + IEnumerable macroElements, + int userId) + { + var macros = macroElements.Select(ParseMacroElement).ToList(); + + foreach (IMacro macro in macros) { - var result = new List(); + _macroService.Save(macro, userId); + } + + return macros; + } - foreach (XElement partialViewXml in partialViewElements) + public IReadOnlyList ImportMacroPartialViews(IEnumerable macroPartialViewsElements, + int userId) + { + var result = new List(); + + foreach (XElement macroPartialViewXml in macroPartialViewsElements) + { + var path = macroPartialViewXml.AttributeValue("path"); + if (path == null) { - var path = partialViewXml.AttributeValue("path"); + throw new InvalidOperationException("No path attribute found"); + } - if (path == null) - { - throw new InvalidOperationException("No path attribute found"); - } + // Remove prefix to maintain backwards compatibility + if (path.StartsWith(Constants.SystemDirectories.MacroPartials)) + { + path = path.Substring(Constants.SystemDirectories.MacroPartials.Length); + } + else if (path.StartsWith("~")) + { + _logger.LogWarning( + "Importing macro partial views outside of the Views/MacroPartials directory is not supported: {Path}", + path); + continue; + } - IPartialView? partialView = _fileService.GetPartialView(path); + IPartialView? macroPartialView = _fileService.GetPartialViewMacro(path); - // only update if it doesn't exist - if (partialView == null) - { - var content = partialViewXml.Value ?? string.Empty; + // only update if it doesn't exist + if (macroPartialView == null) + { + var content = macroPartialViewXml.Value ?? string.Empty; - partialView = new PartialView(PartialViewType.PartialView, path) {Content = content}; - _fileService.SavePartialView(partialView, userId); - result.Add(partialView); - } + macroPartialView = new PartialView(PartialViewType.PartialViewMacro, path) {Content = content}; + _fileService.SavePartialViewMacro(macroPartialView, userId); + result.Add(macroPartialView); } + } + + return result; + } - return result; + private IMacro ParseMacroElement(XElement macroElement) + { + var macroKey = Guid.Parse(macroElement.Element("key")!.Value); + var macroName = macroElement.Element("name")?.Value; + var macroAlias = macroElement.Element("alias")!.Value; + var macroSource = macroElement.Element("macroSource")!.Value; + + //Following xml elements are treated as nullable properties + XElement? useInEditorElement = macroElement.Element("useInEditor"); + var useInEditor = false; + if (useInEditorElement != null && string.IsNullOrEmpty((string)useInEditorElement) == false) + { + useInEditor = bool.Parse(useInEditorElement.Value); } - #region Stylesheets + XElement? cacheDurationElement = macroElement.Element("refreshRate"); + var cacheDuration = 0; + if (cacheDurationElement != null && string.IsNullOrEmpty((string)cacheDurationElement) == false) + { + cacheDuration = int.Parse(cacheDurationElement.Value, CultureInfo.InvariantCulture); + } - public IReadOnlyList ImportStylesheets(IEnumerable stylesheetElements, int userId) + XElement? cacheByMemberElement = macroElement.Element("cacheByMember"); + var cacheByMember = false; + if (cacheByMemberElement != null && string.IsNullOrEmpty((string)cacheByMemberElement) == false) { - var result = new List(); + cacheByMember = bool.Parse(cacheByMemberElement.Value); + } - foreach (XElement n in stylesheetElements) - { - var stylesheetPath = n.Element("FileName")?.Value; - if (stylesheetPath.IsNullOrWhiteSpace()) + XElement? cacheByPageElement = macroElement.Element("cacheByPage"); + var cacheByPage = false; + if (cacheByPageElement != null && string.IsNullOrEmpty((string)cacheByPageElement) == false) + { + cacheByPage = bool.Parse(cacheByPageElement.Value); + } + + XElement? dontRenderElement = macroElement.Element("dontRender"); + var dontRender = true; + if (dontRenderElement != null && string.IsNullOrEmpty((string)dontRenderElement) == false) + { + dontRender = bool.Parse(dontRenderElement.Value); + } + + var existingMacro = _macroService.GetById(macroKey) as Macro; + Macro macro = existingMacro ?? new Macro(_shortStringHelper, macroAlias, macroName, macroSource, + cacheByPage, cacheByMember, dontRender, useInEditor, cacheDuration) {Key = macroKey}; + + XElement? properties = macroElement.Element("properties"); + if (properties != null) + { + var sortOrder = 0; + foreach (XElement property in properties.Elements()) + { + Guid propertyKey = property.RequiredAttributeValue("key"); + var propertyName = property.Attribute("name")?.Value; + var propertyAlias = property.Attribute("alias")!.Value; + var editorAlias = property.Attribute("propertyType")!.Value; + XAttribute? sortOrderAttribute = property.Attribute("sortOrder"); + if (sortOrderAttribute != null) { - continue; + sortOrder = int.Parse(sortOrderAttribute.Value, CultureInfo.InvariantCulture); } - IStylesheet? s = _fileService.GetStylesheet(stylesheetPath!); - if (s == null) + if (macro.Properties.Values.Any(x => + string.Equals(x.Alias, propertyAlias, StringComparison.OrdinalIgnoreCase))) { - var content = n.Element("Content")?.Value; - if (content == null) - { - continue; - } - - s = new Stylesheet(stylesheetPath!) {Content = content}; - _fileService.SaveStylesheet(s, userId); + continue; } - foreach (var prop in n.XPathSelectElements("Properties/Property")) + macro.Properties.Add(new MacroProperty(propertyAlias, propertyName, sortOrder, editorAlias) { - var alias = prop.Element("Alias")!.Value; - var sp = s.Properties?.SingleOrDefault(p => p != null && p.Alias == alias); - var name = prop.Element("Name")!.Value; - if (sp == null) - { - sp = new StylesheetProperty(name, "#" + name.ToSafeAlias(_shortStringHelper), string.Empty); - s.AddProperty(sp); - } - else - { - //sp.Text = name; - //Changing the name requires removing the current property and then adding another new one - if (sp.Name != name) - { - s.RemoveProperty(sp.Name); - var newProp = new StylesheetProperty(name, sp.Alias, sp.Value); - s.AddProperty(newProp); - sp = newProp; - } - } + Key = propertyKey + }); - sp.Alias = alias; - sp.Value = prop.Element("Value")!.Value; - } - - _fileService.SaveStylesheet(s, userId); - result.Add(s); + sortOrder++; } - - return result; } - #endregion + return macro; + } - #region Templates + #endregion - public IEnumerable ImportTemplate(XElement templateElement, int userId) - => ImportTemplates(new[] {templateElement}, userId); + #region Templates - /// - /// Imports and saves package xml as - /// - /// Xml to import - /// Optional user id - /// An enumerable list of generated Templates - public IReadOnlyList ImportTemplates(IReadOnlyCollection templateElements, int userId) - { - var templates = new List(); + public IEnumerable ImportTemplate(XElement templateElement, int userId) + => ImportTemplates(new[] {templateElement}, userId); - var graph = new TopoGraph>(x => x.Key, x => x.Dependencies); + /// + /// Imports and saves package xml as + /// + /// Xml to import + /// Optional user id + /// An enumerable list of generated Templates + public IReadOnlyList ImportTemplates(IReadOnlyCollection templateElements, int userId) + { + var templates = new List(); - foreach (var tempElement in templateElements) - { - var dependencies = new List(); - var elementCopy = tempElement; - //Ensure that the Master of the current template is part of the import, otherwise we ignore this dependency as part of the dependency sorting. - if (string.IsNullOrEmpty((string?)elementCopy.Element("Master")) == false && - templateElements.Any(x => (string?)x.Element("Alias") == (string?)elementCopy.Element("Master"))) - { - dependencies.Add((string)elementCopy.Element("Master")!); - } - else if (string.IsNullOrEmpty((string?)elementCopy.Element("Master")) == false && - templateElements.Any(x => - (string?)x.Element("Alias") == (string?)elementCopy.Element("Master")) == false) - { - _logger.LogInformation( - "Template '{TemplateAlias}' has an invalid Master '{TemplateMaster}', so the reference has been ignored.", - (string?)elementCopy.Element("Alias"), - (string?)elementCopy.Element("Master")); - } + var graph = new TopoGraph>(x => x.Key, x => x.Dependencies); - graph.AddItem(TopoGraph.CreateNode((string)elementCopy.Element("Alias")!, elementCopy, dependencies)); + foreach (XElement tempElement in templateElements) + { + var dependencies = new List(); + XElement elementCopy = tempElement; + //Ensure that the Master of the current template is part of the import, otherwise we ignore this dependency as part of the dependency sorting. + if (string.IsNullOrEmpty((string?)elementCopy.Element("Master")) == false && + templateElements.Any(x => (string?)x.Element("Alias") == (string?)elementCopy.Element("Master"))) + { + dependencies.Add((string)elementCopy.Element("Master")!); } - - //Sort templates by dependencies to a potential master template - var sorted = graph.GetSortedItems(); - foreach (var item in sorted) + else if (string.IsNullOrEmpty((string?)elementCopy.Element("Master")) == false && + templateElements.Any(x => + (string?)x.Element("Alias") == (string?)elementCopy.Element("Master")) == false) { - var templateElement = item.Item; + _logger.LogInformation( + "Template '{TemplateAlias}' has an invalid Master '{TemplateMaster}', so the reference has been ignored.", + (string?)elementCopy.Element("Alias"), + (string?)elementCopy.Element("Master")); + } + + graph.AddItem(TopoGraph.CreateNode((string)elementCopy.Element("Alias")!, elementCopy, dependencies)); + } - var templateName = templateElement.Element("Name")?.Value; - var alias = templateElement.Element("Alias")!.Value; - var design = templateElement.Element("Design")?.Value; - var masterElement = templateElement.Element("Master"); + //Sort templates by dependencies to a potential master template + IEnumerable> sorted = graph.GetSortedItems(); + foreach (TopoGraph.Node item in sorted) + { + XElement templateElement = item.Item; - var existingTemplate = _fileService.GetTemplate(alias) as Template; + var templateName = templateElement.Element("Name")?.Value; + var alias = templateElement.Element("Alias")!.Value; + var design = templateElement.Element("Design")?.Value; + XElement? masterElement = templateElement.Element("Master"); - var template = existingTemplate ?? new Template(_shortStringHelper, templateName, alias); + var existingTemplate = _fileService.GetTemplate(alias) as Template; - // For new templates, use the serialized key if avaialble. - if (existingTemplate == null && Guid.TryParse(templateElement.Element("Key")?.Value, out var key)) - { - template.Key = key; - } + Template template = existingTemplate ?? new Template(_shortStringHelper, templateName, alias); + + // For new templates, use the serialized key if avaialble. + if (existingTemplate == null && Guid.TryParse(templateElement.Element("Key")?.Value, out Guid key)) + { + template.Key = key; + } - template.Content = design; + template.Content = design; - if (masterElement != null && string.IsNullOrEmpty((string)masterElement) == false) + if (masterElement != null && string.IsNullOrEmpty((string)masterElement) == false) + { + template.MasterTemplateAlias = masterElement.Value; + ITemplate? masterTemplate = templates.FirstOrDefault(x => x.Alias == masterElement.Value); + if (masterTemplate != null) { - template.MasterTemplateAlias = masterElement.Value; - var masterTemplate = templates.FirstOrDefault(x => x.Alias == masterElement.Value); - if (masterTemplate != null) - template.MasterTemplateId = new Lazy(() => masterTemplate.Id); + template.MasterTemplateId = new Lazy(() => masterTemplate.Id); } - - templates.Add(template); } - if (templates.Any()) - _fileService.SaveTemplate(templates, userId); + templates.Add(template); + } - return templates; + if (templates.Any()) + { + _fileService.SaveTemplate(templates, userId); } - #endregion + return templates; } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Packaging/PackageInstallation.cs b/src/Umbraco.Infrastructure/Packaging/PackageInstallation.cs index bb9866e11668..836cb0bd33af 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageInstallation.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageInstallation.cs @@ -1,84 +1,103 @@ -using System; -using System.Linq; using System.Xml.Linq; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Packaging; using Umbraco.Cms.Core.Packaging; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Packaging -{ +namespace Umbraco.Cms.Infrastructure.Packaging; - public class PackageInstallation : IPackageInstallation - { - private readonly PackageDataInstallation _packageDataInstallation; - private readonly CompiledPackageXmlParser _parser; +public class PackageInstallation : IPackageInstallation +{ + private readonly PackageDataInstallation _packageDataInstallation; + private readonly CompiledPackageXmlParser _parser; - /// - /// Initializes a new instance of the class. - /// - public PackageInstallation(PackageDataInstallation packageDataInstallation, CompiledPackageXmlParser parser) - { - _packageDataInstallation = packageDataInstallation ?? throw new ArgumentNullException(nameof(packageDataInstallation)); - _parser = parser ?? throw new ArgumentNullException(nameof(parser)); - } + /// + /// Initializes a new instance of the class. + /// + public PackageInstallation(PackageDataInstallation packageDataInstallation, CompiledPackageXmlParser parser) + { + _packageDataInstallation = + packageDataInstallation ?? throw new ArgumentNullException(nameof(packageDataInstallation)); + _parser = parser ?? throw new ArgumentNullException(nameof(parser)); + } - public CompiledPackage ReadPackage(XDocument? packageXmlFile) + public CompiledPackage ReadPackage(XDocument? packageXmlFile) + { + if (packageXmlFile == null) { - if (packageXmlFile == null) - throw new ArgumentNullException(nameof(packageXmlFile)); - - var compiledPackage = _parser.ToCompiledPackage(packageXmlFile); - return compiledPackage; + throw new ArgumentNullException(nameof(packageXmlFile)); } - public InstallationSummary InstallPackageData(CompiledPackage compiledPackage, int userId, out PackageDefinition packageDefinition) - { - packageDefinition = new PackageDefinition - { - Name = compiledPackage.Name - }; + var compiledPackage = _parser.ToCompiledPackage(packageXmlFile); + return compiledPackage; + } - InstallationSummary installationSummary = _packageDataInstallation.InstallPackageData(compiledPackage, userId); + public InstallationSummary InstallPackageData(CompiledPackage compiledPackage, int userId, + out PackageDefinition packageDefinition) + { + packageDefinition = new PackageDefinition {Name = compiledPackage.Name}; - // Make sure the definition is up to date with everything (note: macro partial views are embedded in macros) - foreach (var x in installationSummary.DataTypesInstalled) - packageDefinition.DataTypes.Add(x.Id.ToInvariantString()); + InstallationSummary installationSummary = _packageDataInstallation.InstallPackageData(compiledPackage, userId); - foreach (var x in installationSummary.LanguagesInstalled) - packageDefinition.Languages.Add(x.Id.ToInvariantString()); + // Make sure the definition is up to date with everything (note: macro partial views are embedded in macros) + foreach (IDataType x in installationSummary.DataTypesInstalled) + { + packageDefinition.DataTypes.Add(x.Id.ToInvariantString()); + } - foreach (var x in installationSummary.DictionaryItemsInstalled) - packageDefinition.DictionaryItems.Add(x.Id.ToInvariantString()); + foreach (ILanguage x in installationSummary.LanguagesInstalled) + { + packageDefinition.Languages.Add(x.Id.ToInvariantString()); + } - foreach (var x in installationSummary.MacrosInstalled) - packageDefinition.Macros.Add(x.Id.ToInvariantString()); + foreach (IDictionaryItem x in installationSummary.DictionaryItemsInstalled) + { + packageDefinition.DictionaryItems.Add(x.Id.ToInvariantString()); + } - foreach (var x in installationSummary.TemplatesInstalled) - packageDefinition.Templates.Add(x.Id.ToInvariantString()); + foreach (IMacro x in installationSummary.MacrosInstalled) + { + packageDefinition.Macros.Add(x.Id.ToInvariantString()); + } - foreach (var x in installationSummary.DocumentTypesInstalled) - packageDefinition.DocumentTypes.Add(x.Id.ToInvariantString()); + foreach (ITemplate x in installationSummary.TemplatesInstalled) + { + packageDefinition.Templates.Add(x.Id.ToInvariantString()); + } - foreach (var x in installationSummary.MediaTypesInstalled) - packageDefinition.MediaTypes.Add(x.Id.ToInvariantString()); + foreach (IContentType x in installationSummary.DocumentTypesInstalled) + { + packageDefinition.DocumentTypes.Add(x.Id.ToInvariantString()); + } - foreach (var x in installationSummary.StylesheetsInstalled) - packageDefinition.Stylesheets.Add(x.Path); + foreach (IMediaType x in installationSummary.MediaTypesInstalled) + { + packageDefinition.MediaTypes.Add(x.Id.ToInvariantString()); + } - foreach (var x in installationSummary.ScriptsInstalled) - packageDefinition.Scripts.Add(x.Path); + foreach (IFile x in installationSummary.StylesheetsInstalled) + { + packageDefinition.Stylesheets.Add(x.Path); + } - foreach (var x in installationSummary.PartialViewsInstalled) - packageDefinition.PartialViews.Add(x.Path); + foreach (IScript x in installationSummary.ScriptsInstalled) + { + packageDefinition.Scripts.Add(x.Path); + } - packageDefinition.ContentNodeId = installationSummary.ContentInstalled.FirstOrDefault()?.Id.ToInvariantString(); + foreach (IPartialView x in installationSummary.PartialViewsInstalled) + { + packageDefinition.PartialViews.Add(x.Path); + } - foreach (var x in installationSummary.MediaInstalled) - packageDefinition.MediaUdis.Add(x.GetUdi()); + packageDefinition.ContentNodeId = installationSummary.ContentInstalled.FirstOrDefault()?.Id.ToInvariantString(); - return installationSummary; + foreach (IMedia x in installationSummary.MediaInstalled) + { + packageDefinition.MediaUdis.Add(x.GetUdi()); } + return installationSummary; } } diff --git a/src/Umbraco.Infrastructure/Packaging/PackageMigrationBase.cs b/src/Umbraco.Infrastructure/Packaging/PackageMigrationBase.cs index 54b96955d40b..ae739c436193 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageMigrationBase.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageMigrationBase.cs @@ -1,4 +1,3 @@ -using System; using System.ComponentModel; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -10,70 +9,68 @@ using Umbraco.Cms.Infrastructure.Migrations; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Infrastructure.Packaging -{ - public abstract class PackageMigrationBase : MigrationBase - { - private readonly IPackagingService _packagingService; - private readonly IMediaService _mediaService; - private readonly MediaFileManager _mediaFileManager; - private readonly MediaUrlGeneratorCollection _mediaUrlGenerators; - private readonly IShortStringHelper _shortStringHelper; - private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; - private readonly IOptions _packageMigrationsSettings; +namespace Umbraco.Cms.Infrastructure.Packaging; - public PackageMigrationBase( - IPackagingService packagingService, - IMediaService mediaService, - MediaFileManager mediaFileManager, - MediaUrlGeneratorCollection mediaUrlGenerators, - IShortStringHelper shortStringHelper, - IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, - IMigrationContext context, - IOptions packageMigrationsSettings) - : base(context) - { - _packagingService = packagingService; - _mediaService = mediaService; - _mediaFileManager = mediaFileManager; - _mediaUrlGenerators = mediaUrlGenerators; - _shortStringHelper = shortStringHelper; - _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; - _packageMigrationsSettings = packageMigrationsSettings; - } - - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("Use ctor with all params")] - public PackageMigrationBase( - IPackagingService packagingService, - IMediaService mediaService, - MediaFileManager mediaFileManager, - MediaUrlGeneratorCollection mediaUrlGenerators, - IShortStringHelper shortStringHelper, - IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, - IMigrationContext context) - : this( - packagingService, - mediaService, - mediaFileManager, - mediaUrlGenerators, - shortStringHelper, - contentTypeBaseServiceProvider, - context, - StaticServiceProvider.Instance.GetRequiredService>()) - { - } +public abstract class PackageMigrationBase : MigrationBase +{ + private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; + private readonly MediaFileManager _mediaFileManager; + private readonly IMediaService _mediaService; + private readonly MediaUrlGeneratorCollection _mediaUrlGenerators; + private readonly IOptions _packageMigrationsSettings; + private readonly IPackagingService _packagingService; + private readonly IShortStringHelper _shortStringHelper; - public IImportPackageBuilder ImportPackage => BeginBuild( - new ImportPackageBuilder( - _packagingService, - _mediaService, - _mediaFileManager, - _mediaUrlGenerators, - _shortStringHelper, - _contentTypeBaseServiceProvider, - Context, - _packageMigrationsSettings)); + public PackageMigrationBase( + IPackagingService packagingService, + IMediaService mediaService, + MediaFileManager mediaFileManager, + MediaUrlGeneratorCollection mediaUrlGenerators, + IShortStringHelper shortStringHelper, + IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, + IMigrationContext context, + IOptions packageMigrationsSettings) + : base(context) + { + _packagingService = packagingService; + _mediaService = mediaService; + _mediaFileManager = mediaFileManager; + _mediaUrlGenerators = mediaUrlGenerators; + _shortStringHelper = shortStringHelper; + _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; + _packageMigrationsSettings = packageMigrationsSettings; + } + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use ctor with all params")] + public PackageMigrationBase( + IPackagingService packagingService, + IMediaService mediaService, + MediaFileManager mediaFileManager, + MediaUrlGeneratorCollection mediaUrlGenerators, + IShortStringHelper shortStringHelper, + IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, + IMigrationContext context) + : this( + packagingService, + mediaService, + mediaFileManager, + mediaUrlGenerators, + shortStringHelper, + contentTypeBaseServiceProvider, + context, + StaticServiceProvider.Instance.GetRequiredService>()) + { } + + public IImportPackageBuilder ImportPackage => BeginBuild( + new ImportPackageBuilder( + _packagingService, + _mediaService, + _mediaFileManager, + _mediaUrlGenerators, + _shortStringHelper, + _contentTypeBaseServiceProvider, + Context, + _packageMigrationsSettings)); } diff --git a/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlan.cs b/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlan.cs index d25c65cfb8a9..582ec908dc1d 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlan.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlan.cs @@ -1,53 +1,51 @@ -using System; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Infrastructure.Migrations; -namespace Umbraco.Cms.Core.Packaging -{ +namespace Umbraco.Cms.Core.Packaging; +/// +/// Base class for package migration plans +/// +public abstract class PackageMigrationPlan : MigrationPlan, IDiscoverable +{ /// - /// Base class for package migration plans + /// Creates a package migration plan /// - public abstract class PackageMigrationPlan : MigrationPlan, IDiscoverable + /// The name of the package. If the package has a package.manifest these must match. + protected PackageMigrationPlan(string packageName) : this(packageName, packageName) { - /// - /// Creates a package migration plan - /// - /// The name of the package. If the package has a package.manifest these must match. - protected PackageMigrationPlan(string packageName) : this(packageName, packageName) - { - - } - - /// - /// Create a plan for a Package Name - /// - /// The package name that the plan is for. If the package has a package.manifest these must match. - /// - /// The plan name for the package. This should be the same name as the - /// package name if there is only one plan in the package. - /// - protected PackageMigrationPlan(string packageName, string planName) : base(planName) - { - // A call to From must be done first - From(string.Empty); + } - DefinePlan(); - PackageName = packageName; - } + /// + /// Create a plan for a Package Name + /// + /// + /// The package name that the plan is for. If the package has a package.manifest these must + /// match. + /// + /// + /// The plan name for the package. This should be the same name as the + /// package name if there is only one plan in the package. + /// + protected PackageMigrationPlan(string packageName, string planName) : base(planName) + { + // A call to From must be done first + From(string.Empty); - /// - /// Inform the plan executor to ignore all saved package state and - /// run the migration from initial state to it's end state. - /// - public override bool IgnoreCurrentState => true; + DefinePlan(); + PackageName = packageName; + } - /// - /// Returns the Package Name for this plan - /// - public string PackageName { get; } + /// + /// Inform the plan executor to ignore all saved package state and + /// run the migration from initial state to it's end state. + /// + public override bool IgnoreCurrentState => true; - protected abstract void DefinePlan(); + /// + /// Returns the Package Name for this plan + /// + public string PackageName { get; } - } + protected abstract void DefinePlan(); } diff --git a/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlanCollection.cs b/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlanCollection.cs index aa390dcaa402..20d8f12f81b1 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlanCollection.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlanCollection.cs @@ -1,16 +1,13 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Packaging +namespace Umbraco.Cms.Core.Packaging; + +/// +/// A collection of +/// +public class PackageMigrationPlanCollection : BuilderCollectionBase { - /// - /// A collection of - /// - public class PackageMigrationPlanCollection : BuilderCollectionBase + public PackageMigrationPlanCollection(Func> items) : base(items) { - public PackageMigrationPlanCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlanCollectionBuilder.cs b/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlanCollectionBuilder.cs index bf496852c69b..b9dc1bcc2e16 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlanCollectionBuilder.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlanCollectionBuilder.cs @@ -1,9 +1,9 @@ using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Packaging +namespace Umbraco.Cms.Core.Packaging; + +public class PackageMigrationPlanCollectionBuilder : LazyCollectionBuilderBase { - public class PackageMigrationPlanCollectionBuilder : LazyCollectionBuilderBase - { - protected override PackageMigrationPlanCollectionBuilder This => this; - } + protected override PackageMigrationPlanCollectionBuilder This => this; } diff --git a/src/Umbraco.Infrastructure/Packaging/PendingPackageMigrations.cs b/src/Umbraco.Infrastructure/Packaging/PendingPackageMigrations.cs index efefcfcc7a10..2931011f3874 100644 --- a/src/Umbraco.Infrastructure/Packaging/PendingPackageMigrations.cs +++ b/src/Umbraco.Infrastructure/Packaging/PendingPackageMigrations.cs @@ -1,65 +1,61 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Packaging +namespace Umbraco.Cms.Core.Packaging; + +public class PendingPackageMigrations { - public class PendingPackageMigrations + private readonly ILogger _logger; + private readonly PackageMigrationPlanCollection _packageMigrationPlans; + + public PendingPackageMigrations( + ILogger logger, + PackageMigrationPlanCollection packageMigrationPlans) { - private readonly ILogger _logger; - private readonly PackageMigrationPlanCollection _packageMigrationPlans; + _logger = logger; + _packageMigrationPlans = packageMigrationPlans; + } - public PendingPackageMigrations( - ILogger logger, - PackageMigrationPlanCollection packageMigrationPlans) - { - _logger = logger; - _packageMigrationPlans = packageMigrationPlans; + /// + /// Returns what package migration names are pending + /// + /// + /// These are the key/value pairs from the keyvalue storage of migration names and their final values + /// + /// + public IReadOnlyList GetPendingPackageMigrations(IReadOnlyDictionary? keyValues) + { + var packageMigrationPlans = _packageMigrationPlans.ToList(); - } + var pendingMigrations = new List(packageMigrationPlans.Count); - /// - /// Returns what package migration names are pending - /// - /// - /// These are the key/value pairs from the keyvalue storage of migration names and their final values - /// - /// - public IReadOnlyList GetPendingPackageMigrations(IReadOnlyDictionary? keyValues) + foreach (PackageMigrationPlan plan in packageMigrationPlans) { - var packageMigrationPlans = _packageMigrationPlans.ToList(); - - var pendingMigrations = new List(packageMigrationPlans.Count); - - foreach (PackageMigrationPlan plan in packageMigrationPlans) + string? currentMigrationState = null; + var planKeyValueKey = Constants.Conventions.Migrations.KeyValuePrefix + plan.Name; + if (keyValues?.TryGetValue(planKeyValueKey, out var value) ?? false) { - string? currentMigrationState = null; - var planKeyValueKey = Constants.Conventions.Migrations.KeyValuePrefix + plan.Name; - if (keyValues?.TryGetValue(planKeyValueKey, out var value) ?? false) - { - currentMigrationState = value; + currentMigrationState = value; - if (!plan.FinalState.InvariantEquals(value)) - { - // Not equal so we need to run - pendingMigrations.Add(plan.Name); - } - } - else + if (!plan.FinalState.InvariantEquals(value)) { - // If there is nothing in the DB then we need to run + // Not equal so we need to run pendingMigrations.Add(plan.Name); } - - _logger.LogDebug("Final package migration for {PackagePlan} state is {FinalMigrationState}, database contains {DatabaseState}", - plan.Name, - plan.FinalState, - currentMigrationState ?? ""); + } + else + { + // If there is nothing in the DB then we need to run + pendingMigrations.Add(plan.Name); } - return pendingMigrations; + _logger.LogDebug( + "Final package migration for {PackagePlan} state is {FinalMigrationState}, database contains {DatabaseState}", + plan.Name, + plan.FinalState, + currentMigrationState ?? ""); } + + return pendingMigrations; } } diff --git a/src/Umbraco.Infrastructure/Persistence/CustomConnectionStringDatabaseProviderMetadata.cs b/src/Umbraco.Infrastructure/Persistence/CustomConnectionStringDatabaseProviderMetadata.cs index 3797d4a4337e..f266df71ffe9 100644 --- a/src/Umbraco.Infrastructure/Persistence/CustomConnectionStringDatabaseProviderMetadata.cs +++ b/src/Umbraco.Infrastructure/Persistence/CustomConnectionStringDatabaseProviderMetadata.cs @@ -1,11 +1,10 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Install.Models; namespace Umbraco.Cms.Infrastructure.Persistence; /// -/// Provider metadata for custom connection string setup. +/// Provider metadata for custom connection string setup. /// [DataContract] public class CustomConnectionStringDatabaseProviderMetadata : IDatabaseProviderMetadata diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/ConstraintAttribute.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/ConstraintAttribute.cs index 8b8386c93f32..907641c77294 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/ConstraintAttribute.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/ConstraintAttribute.cs @@ -1,25 +1,22 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +/// +/// Attribute that represents a db constraint +/// +[AttributeUsage(AttributeTargets.Property)] +public class ConstraintAttribute : Attribute { /// - /// Attribute that represents a db constraint + /// Gets or sets the name of the constraint /// - [AttributeUsage(AttributeTargets.Property)] - public class ConstraintAttribute : Attribute - { - /// - /// Gets or sets the name of the constraint - /// - /// - /// Overrides the default naming of a property constraint: - /// DF_tableName_propertyName - /// - public string? Name { get; set; } + /// + /// Overrides the default naming of a property constraint: + /// DF_tableName_propertyName + /// + public string? Name { get; set; } - /// - /// Gets or sets the Default value - /// - public object? Default { get; set; } - } + /// + /// Gets or sets the Default value + /// + public object? Default { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/ForeignKeyAttribute.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/ForeignKeyAttribute.cs index a2f053415c38..f496869e9baa 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/ForeignKeyAttribute.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/ForeignKeyAttribute.cs @@ -1,40 +1,39 @@ -using System; -using System.Data; +using System.Data; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; + +/// +/// Attribute that represents a Foreign Key reference +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] +public class ForeignKeyAttribute : ReferencesAttribute { - /// - /// Attribute that represents a Foreign Key reference - /// - [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] - public class ForeignKeyAttribute : ReferencesAttribute + public ForeignKeyAttribute(Type type) : base(type) { - public ForeignKeyAttribute(Type type) : base(type) - { } + } - /// - /// Gets or sets the cascade rule for deletions. - /// - public Rule OnDelete { get; set; } = Rule.None; + /// + /// Gets or sets the cascade rule for deletions. + /// + public Rule OnDelete { get; set; } = Rule.None; - /// - /// Gets or sets the cascade rule for updates. - /// - public Rule OnUpdate { get; set; } = Rule.None; + /// + /// Gets or sets the cascade rule for updates. + /// + public Rule OnUpdate { get; set; } = Rule.None; - /// - /// Gets or sets the name of the foreign key reference - /// - /// - /// Overrides the default naming of a foreign key reference: - /// FK_thisTableName_refTableName - /// - public string? Name { get; set; } + /// + /// Gets or sets the name of the foreign key reference + /// + /// + /// Overrides the default naming of a foreign key reference: + /// FK_thisTableName_refTableName + /// + public string? Name { get; set; } - /// - /// Gets or sets the name of the Column that this foreign key should reference. - /// - /// PrimaryKey column is used by default - public string? Column { get; set; } - } + /// + /// Gets or sets the name of the Column that this foreign key should reference. + /// + /// PrimaryKey column is used by default + public string? Column { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/IndexAttribute.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/IndexAttribute.cs index 053e5b825d10..4050378e3b78 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/IndexAttribute.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/IndexAttribute.cs @@ -1,40 +1,34 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +/// +/// Attribute that represents an Index +/// +[AttributeUsage(AttributeTargets.Property)] +public class IndexAttribute : Attribute { + public IndexAttribute(IndexTypes indexType) => IndexType = indexType; + /// - /// Attribute that represents an Index + /// Gets or sets the name of the Index /// - [AttributeUsage(AttributeTargets.Property)] - public class IndexAttribute : Attribute - { - public IndexAttribute(IndexTypes indexType) - { - IndexType = indexType; - } - - /// - /// Gets or sets the name of the Index - /// - /// - /// Overrides default naming of indexes: - /// IX_tableName - /// - public string? Name { get; set; }//Overrides default naming of indexes: IX_tableName + /// + /// Overrides default naming of indexes: + /// IX_tableName + /// + public string? Name { get; set; } //Overrides default naming of indexes: IX_tableName - /// - /// Gets or sets the type of index to create - /// - public IndexTypes IndexType { get; private set; } + /// + /// Gets or sets the type of index to create + /// + public IndexTypes IndexType { get; } - /// - /// Gets or sets the column name(s) for the current index - /// - public string? ForColumns { get; set; } + /// + /// Gets or sets the column name(s) for the current index + /// + public string? ForColumns { get; set; } - /// - /// Gets or sets the column name(s) for the columns to include in the index - /// - public string? IncludeColumns { get; set; } - } + /// + /// Gets or sets the column name(s) for the columns to include in the index + /// + public string? IncludeColumns { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/IndexTypes.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/IndexTypes.cs index 65516bb8c46f..be1260fb9b65 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/IndexTypes.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/IndexTypes.cs @@ -1,12 +1,11 @@ -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; + +/// +/// Enum for the 3 types of indexes that can be created +/// +public enum IndexTypes { - /// - /// Enum for the 3 types of indexes that can be created - /// - public enum IndexTypes - { - Clustered, - NonClustered, - UniqueNonClustered - } + Clustered, + NonClustered, + UniqueNonClustered } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/LengthAttribute.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/LengthAttribute.cs index 8e77b4bf96e1..347c78ff9ec2 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/LengthAttribute.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/LengthAttribute.cs @@ -1,22 +1,16 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +/// +/// Attribute that represents the length of a column +/// +/// Used to define the length of fixed sized columns - typically used for nvarchar +[AttributeUsage(AttributeTargets.Property)] +public class LengthAttribute : Attribute { + public LengthAttribute(int length) => Length = length; + /// - /// Attribute that represents the length of a column + /// Gets or sets the length of a column /// - /// Used to define the length of fixed sized columns - typically used for nvarchar - [AttributeUsage(AttributeTargets.Property)] - public class LengthAttribute : Attribute - { - public LengthAttribute(int length) - { - Length = length; - } - - /// - /// Gets or sets the length of a column - /// - public int Length { get; private set; } - } + public int Length { get; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/NullSettingAttribute.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/NullSettingAttribute.cs index 0db6433e9442..23bd4bda9bcb 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/NullSettingAttribute.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/NullSettingAttribute.cs @@ -1,20 +1,17 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +/// +/// Attribute that represents the Null-setting of a column +/// +/// +/// This should only be used for Columns that can be Null. +/// By convention the Columns will be "NOT NULL". +/// +[AttributeUsage(AttributeTargets.Property)] +public class NullSettingAttribute : Attribute { /// - /// Attribute that represents the Null-setting of a column + /// Gets or sets the for a column /// - /// - /// This should only be used for Columns that can be Null. - /// By convention the Columns will be "NOT NULL". - /// - [AttributeUsage(AttributeTargets.Property)] - public class NullSettingAttribute : Attribute - { - /// - /// Gets or sets the for a column - /// - public NullSettings NullSetting { get; set; } - } + public NullSettings NullSetting { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/NullSettings.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/NullSettings.cs index 70c901c61eca..d0e742e32e82 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/NullSettings.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/NullSettings.cs @@ -1,11 +1,10 @@ -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; + +/// +/// Enum with the 2 possible Null settings: Null or Not Null +/// +public enum NullSettings { - /// - /// Enum with the 2 possible Null settings: Null or Not Null - /// - public enum NullSettings - { - Null, - NotNull - } + Null, + NotNull } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/PrimaryKeyColumnAttribute.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/PrimaryKeyColumnAttribute.cs index c4c5579028a4..16ec9a03aded 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/PrimaryKeyColumnAttribute.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/PrimaryKeyColumnAttribute.cs @@ -1,59 +1,56 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +/// +/// Attribute that represents a Primary Key +/// +/// +/// By default, Clustered and AutoIncrement is set to true. +/// +[AttributeUsage(AttributeTargets.Property)] +public class PrimaryKeyColumnAttribute : Attribute { - /// - /// Attribute that represents a Primary Key - /// - /// - /// By default, Clustered and AutoIncrement is set to true. - /// - [AttributeUsage(AttributeTargets.Property)] - public class PrimaryKeyColumnAttribute : Attribute + public PrimaryKeyColumnAttribute() { - public PrimaryKeyColumnAttribute() - { - Clustered = true; - AutoIncrement = true; - } + Clustered = true; + AutoIncrement = true; + } - /// - /// Gets or sets a boolean indicating whether the primary key is clustered. - /// - /// Defaults to true - public bool Clustered { get; set; } + /// + /// Gets or sets a boolean indicating whether the primary key is clustered. + /// + /// Defaults to true + public bool Clustered { get; set; } - /// - /// Gets or sets a boolean indicating whether the primary key is auto incremented. - /// - /// Defaults to true - public bool AutoIncrement { get; set; } + /// + /// Gets or sets a boolean indicating whether the primary key is auto incremented. + /// + /// Defaults to true + public bool AutoIncrement { get; set; } - /// - /// Gets or sets the name of the PrimaryKey. - /// - /// - /// Overrides the default naming of a PrimaryKey constraint: - /// PK_tableName - /// - public string? Name { get; set; } + /// + /// Gets or sets the name of the PrimaryKey. + /// + /// + /// Overrides the default naming of a PrimaryKey constraint: + /// PK_tableName + /// + public string? Name { get; set; } - /// - /// Gets or sets the names of the columns for this PrimaryKey. - /// - /// - /// Should only be used if the PrimaryKey spans over multiple columns. - /// Usage: [nodeId], [otherColumn] - /// - public string? OnColumns { get; set; } + /// + /// Gets or sets the names of the columns for this PrimaryKey. + /// + /// + /// Should only be used if the PrimaryKey spans over multiple columns. + /// Usage: [nodeId], [otherColumn] + /// + public string? OnColumns { get; set; } - /// - /// Gets or sets the Identity Seed, which is used for Sql Ce databases. - /// - /// - /// We'll only look for changes to seeding and apply them if the configured database - /// is an Sql Ce database. - /// - public int IdentitySeed { get; set; } - } + /// + /// Gets or sets the Identity Seed, which is used for Sql Ce databases. + /// + /// + /// We'll only look for changes to seeding and apply them if the configured database + /// is an Sql Ce database. + /// + public int IdentitySeed { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/ReferencesAttribute.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/ReferencesAttribute.cs index f008aa7e226d..f1e821d6105f 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/ReferencesAttribute.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/ReferencesAttribute.cs @@ -1,21 +1,15 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +/// +/// Attribute that represents a reference between two tables/DTOs +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)] +public class ReferencesAttribute : Attribute { + public ReferencesAttribute(Type type) => Type = type; + /// - /// Attribute that represents a reference between two tables/DTOs + /// Gets or sets the Type of the referenced DTO/table /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)] - public class ReferencesAttribute : Attribute - { - public ReferencesAttribute(Type type) - { - Type = type; - } - - /// - /// Gets or sets the Type of the referenced DTO/table - /// - public Type Type { get; set; } - } + public Type Type { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbType.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbType.cs index 41570d7b951c..29599229da3f 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbType.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbType.cs @@ -1,43 +1,39 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +/// +/// Allows for specifying custom DB types that are not natively mapped. +/// +public struct SpecialDbType : IEquatable { - /// - /// Allows for specifying custom DB types that are not natively mapped. - /// - public struct SpecialDbType : IEquatable - { - private readonly string _dbType; + private readonly string _dbType; - public SpecialDbType(string dbType) + public SpecialDbType(string dbType) + { + if (string.IsNullOrWhiteSpace(dbType)) { - if (string.IsNullOrWhiteSpace(dbType)) - { - throw new ArgumentException($"'{nameof(dbType)}' cannot be null or whitespace.", nameof(dbType)); - } - - _dbType = dbType; + throw new ArgumentException($"'{nameof(dbType)}' cannot be null or whitespace.", nameof(dbType)); } - public SpecialDbType(SpecialDbTypes specialDbTypes) - => _dbType = specialDbTypes.ToString(); + _dbType = dbType; + } - public static SpecialDbType NTEXT { get; } = new SpecialDbType(SpecialDbTypes.NTEXT); - public static SpecialDbType NCHAR { get; } = new SpecialDbType(SpecialDbTypes.NCHAR); - public static SpecialDbType NVARCHARMAX { get; } = new SpecialDbType(SpecialDbTypes.NVARCHARMAX); + public SpecialDbType(SpecialDbTypes specialDbTypes) + => _dbType = specialDbTypes.ToString(); - public override bool Equals(object? obj) => obj is SpecialDbType types && Equals(types); - public bool Equals(SpecialDbType other) => _dbType == other._dbType; - public override int GetHashCode() => 1038481724 + EqualityComparer.Default.GetHashCode(_dbType); + public static SpecialDbType NTEXT { get; } = new(SpecialDbTypes.NTEXT); + public static SpecialDbType NCHAR { get; } = new(SpecialDbTypes.NCHAR); + public static SpecialDbType NVARCHARMAX { get; } = new(SpecialDbTypes.NVARCHARMAX); - public override string ToString() => _dbType.ToString(); + public override bool Equals(object? obj) => obj is SpecialDbType types && Equals(types); + public bool Equals(SpecialDbType other) => _dbType == other._dbType; + public override int GetHashCode() => 1038481724 + EqualityComparer.Default.GetHashCode(_dbType); - // Make this directly castable to string - public static implicit operator string(SpecialDbType dbType) => dbType.ToString(); + public override string ToString() => _dbType; - // direct equality operators with SpecialDbTypes enum - public static bool operator ==(SpecialDbTypes x, SpecialDbType y) => x.ToString() == y; - public static bool operator !=(SpecialDbTypes x, SpecialDbType y) => x.ToString() != y; - } + // Make this directly castable to string + public static implicit operator string(SpecialDbType dbType) => dbType.ToString(); + + // direct equality operators with SpecialDbTypes enum + public static bool operator ==(SpecialDbTypes x, SpecialDbType y) => x.ToString() == y; + public static bool operator !=(SpecialDbTypes x, SpecialDbType y) => x.ToString() != y; } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypeAttribute.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypeAttribute.cs index d7fd2ff34ff2..cfdd4d80aa99 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypeAttribute.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypeAttribute.cs @@ -1,25 +1,22 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +/// +/// Attribute that represents the usage of a special type +/// +/// +/// Should only be used when the .NET type can't be directly translated to a DbType. +/// +[AttributeUsage(AttributeTargets.Property)] +public class SpecialDbTypeAttribute : Attribute { - /// - /// Attribute that represents the usage of a special type - /// - /// - /// Should only be used when the .NET type can't be directly translated to a DbType. - /// - [AttributeUsage(AttributeTargets.Property)] - public class SpecialDbTypeAttribute : Attribute - { - public SpecialDbTypeAttribute(SpecialDbTypes databaseType) - => DatabaseType = new SpecialDbType(databaseType); + public SpecialDbTypeAttribute(SpecialDbTypes databaseType) + => DatabaseType = new SpecialDbType(databaseType); - public SpecialDbTypeAttribute(string databaseType) - => DatabaseType = new SpecialDbType(databaseType); + public SpecialDbTypeAttribute(string databaseType) + => DatabaseType = new SpecialDbType(databaseType); - /// - /// Gets or sets the for this column - /// - public SpecialDbType DatabaseType { get; private set; } - } + /// + /// Gets or sets the for this column + /// + public SpecialDbType DatabaseType { get; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypes.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypes.cs index d867d6f682cf..4dbbfb60111b 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypes.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypes.cs @@ -1,12 +1,11 @@ -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; + +/// +/// Known special DB types required for Umbraco. +/// +public enum SpecialDbTypes { - /// - /// Known special DB types required for Umbraco. - /// - public enum SpecialDbTypes - { - NTEXT, - NCHAR, - NVARCHARMAX, - } + NTEXT, + NCHAR, + NVARCHARMAX } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseDebugHelper.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseDebugHelper.cs index f54691994e13..e2efea425129 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseDebugHelper.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseDebugHelper.cs @@ -14,7 +14,8 @@ namespace Umbraco.Cms.Core.Persistence internal static class DatabaseDebugHelper { private const int CommandsSize = 100; - private static readonly Queue>> Commands = new Queue>>(); + private static readonly Queue>> Commands = + new Queue>>(); public static void SetCommand(IDbCommand command, string context) { @@ -122,7 +123,8 @@ public static string GetReferencedObjects(DbConnection con) //var rdr = objTarget as DbDataReader; try { - var commandProp = objTarget.GetType().GetProperty("Command", BindingFlags.Instance | BindingFlags.NonPublic); + var commandProp = + objTarget.GetType().GetProperty("Command", BindingFlags.Instance | BindingFlags.NonPublic); if (commandProp == null) throw new Exception($"panic: failed to get Command property of {objTarget.GetType().FullName}."); cmd = commandProp.GetValue(objTarget, null) as DbCommand; diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ColumnDefinition.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ColumnDefinition.cs index a80bbbe3f60a..b7914d980d38 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ColumnDefinition.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ColumnDefinition.cs @@ -1,38 +1,38 @@ -using System; using System.Data; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; + +public class ColumnDefinition { - public class ColumnDefinition - { - public virtual string Name { get; set; } = null!; - //This type is typically used as part of a migration - public virtual DbType? Type { get; set; } - //When DbType isn't set explicitly the Type will be used to find the right DbType in the SqlSyntaxProvider. - //This type is typically used as part of an initial table creation - public Type PropertyType { get; set; } = null!; + public virtual string Name { get; set; } = null!; + + //This type is typically used as part of a migration + public virtual DbType? Type { get; set; } + + //When DbType isn't set explicitly the Type will be used to find the right DbType in the SqlSyntaxProvider. + //This type is typically used as part of an initial table creation + public Type PropertyType { get; set; } = null!; - /// - /// Used for column types that cannot be natively mapped. - /// - public SpecialDbType? CustomDbType { get; set; } + /// + /// Used for column types that cannot be natively mapped. + /// + public SpecialDbType? CustomDbType { get; set; } - public virtual int Seeding { get; set; } - public virtual int Size { get; set; } - public virtual int Precision { get; set; } - public virtual string? CustomType { get; set; } - public virtual object? DefaultValue { get; set; } - public virtual string? ConstraintName { get; set; } - public virtual bool IsForeignKey { get; set; } - public virtual bool IsIdentity { get; set; } - public virtual bool IsIndexed { get; set; }//Clustered? - public virtual bool IsPrimaryKey { get; set; } - public virtual string? PrimaryKeyName { get; set; } - public virtual string? PrimaryKeyColumns { get; set; }//When the primary key spans multiple columns - public virtual bool IsNullable { get; set; } - public virtual bool IsUnique { get; set; } - public virtual string? TableName { get; set; } - public virtual ModificationType ModificationType { get; set; } - } + public virtual int Seeding { get; set; } + public virtual int Size { get; set; } + public virtual int Precision { get; set; } + public virtual string? CustomType { get; set; } + public virtual object? DefaultValue { get; set; } + public virtual string? ConstraintName { get; set; } + public virtual bool IsForeignKey { get; set; } + public virtual bool IsIdentity { get; set; } + public virtual bool IsIndexed { get; set; } //Clustered? + public virtual bool IsPrimaryKey { get; set; } + public virtual string? PrimaryKeyName { get; set; } + public virtual string? PrimaryKeyColumns { get; set; } //When the primary key spans multiple columns + public virtual bool IsNullable { get; set; } + public virtual bool IsUnique { get; set; } + public virtual string? TableName { get; set; } + public virtual ModificationType ModificationType { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ConstraintDefinition.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ConstraintDefinition.cs index 87066ce20656..fe0be199297e 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ConstraintDefinition.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ConstraintDefinition.cs @@ -1,23 +1,18 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions +public class ConstraintDefinition { - public class ConstraintDefinition - { - public ConstraintDefinition(ConstraintType type) - { - _constraintType = type; - } + private readonly ConstraintType _constraintType; + public ICollection Columns = new HashSet(); - private readonly ConstraintType _constraintType; - public bool IsPrimaryKeyConstraint => ConstraintType.PrimaryKey == _constraintType; - public bool IsUniqueConstraint => ConstraintType.Unique == _constraintType; - public bool IsNonUniqueConstraint => ConstraintType.NonUnique == _constraintType; + public ConstraintDefinition(ConstraintType type) => _constraintType = type; - public string? SchemaName { get; set; } - public string? ConstraintName { get; set; } - public string? TableName { get; set; } - public ICollection Columns = new HashSet(); - public bool IsPrimaryKeyClustered { get; set; } - } + public bool IsPrimaryKeyConstraint => ConstraintType.PrimaryKey == _constraintType; + public bool IsUniqueConstraint => ConstraintType.Unique == _constraintType; + public bool IsNonUniqueConstraint => ConstraintType.NonUnique == _constraintType; + + public string? SchemaName { get; set; } + public string? ConstraintName { get; set; } + public string? TableName { get; set; } + public bool IsPrimaryKeyClustered { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ConstraintType.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ConstraintType.cs index 4592f1f14f44..a5f25a50c014 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ConstraintType.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ConstraintType.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; + +public enum ConstraintType { - public enum ConstraintType - { - PrimaryKey, - Unique, - NonUnique - } + PrimaryKey, + Unique, + NonUnique } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DbIndexDefinition.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DbIndexDefinition.cs index df73074a35c5..50e345c52317 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DbIndexDefinition.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DbIndexDefinition.cs @@ -1,23 +1,20 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions +/// +/// Represents a database index definition retrieved by querying the database +/// +internal class DbIndexDefinition { - /// - /// Represents a database index definition retrieved by querying the database - /// - internal class DbIndexDefinition + public DbIndexDefinition(Tuple data) { - public DbIndexDefinition(Tuple data) - { - TableName = data.Item1; - IndexName = data.Item2; - ColumnName = data.Item3; - IsUnique = data.Item4; - } - - public string IndexName { get; } - public string TableName { get; } - public string ColumnName { get; } - public bool IsUnique { get; } + TableName = data.Item1; + IndexName = data.Item2; + ColumnName = data.Item3; + IsUnique = data.Item4; } + + public string IndexName { get; } + public string TableName { get; } + public string ColumnName { get; } + public bool IsUnique { get; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs index 9e26c2722a9f..9f1d1399e61f 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs @@ -1,5 +1,3 @@ -using System; -using System.Linq; using System.Reflection; using NPoco; using Umbraco.Cms.Core; @@ -7,174 +5,188 @@ using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; + +public static class DefinitionFactory { - public static class DefinitionFactory + public static TableDefinition GetTableDefinition(Type modelType, ISqlSyntaxProvider sqlSyntax) { - public static TableDefinition GetTableDefinition(Type modelType, ISqlSyntaxProvider sqlSyntax) + //Looks for NPoco's TableNameAtribute for the name of the table + //If no attribute is set we use the name of the Type as the default convention + TableNameAttribute? tableNameAttribute = modelType.FirstAttribute(); + var tableName = tableNameAttribute == null ? modelType.Name : tableNameAttribute.Value; + + var tableDefinition = new TableDefinition {Name = tableName}; + var objProperties = modelType.GetProperties().ToList(); + foreach (PropertyInfo propertyInfo in objProperties) { - //Looks for NPoco's TableNameAtribute for the name of the table - //If no attribute is set we use the name of the Type as the default convention - var tableNameAttribute = modelType.FirstAttribute(); - string tableName = tableNameAttribute == null ? modelType.Name : tableNameAttribute.Value; - - var tableDefinition = new TableDefinition {Name = tableName}; - var objProperties = modelType.GetProperties().ToList(); - foreach (var propertyInfo in objProperties) + //If current property has an IgnoreAttribute then skip it + IgnoreAttribute? ignoreAttribute = propertyInfo.FirstAttribute(); + if (ignoreAttribute != null) { - //If current property has an IgnoreAttribute then skip it - var ignoreAttribute = propertyInfo.FirstAttribute(); - if (ignoreAttribute != null) continue; - - //If current property has a ResultColumnAttribute then skip it - var resultColumnAttribute = propertyInfo.FirstAttribute(); - if (resultColumnAttribute != null) continue; - - //Looks for ColumnAttribute with the name of the column, which would exist with ExplicitColumns - //Otherwise use the name of the property itself as the default convention - var columnAttribute = propertyInfo.FirstAttribute(); - string columnName = columnAttribute != null ? columnAttribute.Name : propertyInfo.Name; - var columnDefinition = GetColumnDefinition(modelType, propertyInfo, columnName, tableName, sqlSyntax); - tableDefinition.Columns.Add(columnDefinition); - - //Creates a foreignkey definition and adds it to the collection on the table definition - var foreignKeyAttributes = propertyInfo.MultipleAttribute(); - if (foreignKeyAttributes != null) - { - foreach (var foreignKeyAttribute in foreignKeyAttributes) - { - var foreignKeyDefinition = GetForeignKeyDefinition(modelType, propertyInfo, foreignKeyAttribute, columnName, tableName); - tableDefinition.ForeignKeys.Add(foreignKeyDefinition); - } - } - - //Creates an index definition and adds it to the collection on the table definition - var indexAttribute = propertyInfo.FirstAttribute(); - if (indexAttribute != null) - { - var indexDefinition = GetIndexDefinition(modelType, propertyInfo, indexAttribute, columnName, tableName); - tableDefinition.Indexes.Add(indexDefinition); - } + continue; } - return tableDefinition; - } - - public static ColumnDefinition GetColumnDefinition(Type modelType, PropertyInfo propertyInfo, string columnName, string tableName, ISqlSyntaxProvider sqlSyntax) - { - var definition = new ColumnDefinition{ Name = columnName, TableName = tableName, ModificationType = ModificationType.Create }; - - //Look for specific Null setting attributed a column - var nullSettingAttribute = propertyInfo.FirstAttribute(); - if (nullSettingAttribute != null) + //If current property has a ResultColumnAttribute then skip it + ResultColumnAttribute? resultColumnAttribute = propertyInfo.FirstAttribute(); + if (resultColumnAttribute != null) { - definition.IsNullable = nullSettingAttribute.NullSetting == NullSettings.Null; + continue; } - //Look for specific DbType attributed a column - var databaseTypeAttribute = propertyInfo.FirstAttribute(); - if (databaseTypeAttribute != null) - { - definition.CustomDbType = databaseTypeAttribute.DatabaseType; - } - else + //Looks for ColumnAttribute with the name of the column, which would exist with ExplicitColumns + //Otherwise use the name of the property itself as the default convention + ColumnAttribute? columnAttribute = propertyInfo.FirstAttribute(); + var columnName = columnAttribute != null ? columnAttribute.Name : propertyInfo.Name; + ColumnDefinition columnDefinition = + GetColumnDefinition(modelType, propertyInfo, columnName, tableName, sqlSyntax); + tableDefinition.Columns.Add(columnDefinition); + + //Creates a foreignkey definition and adds it to the collection on the table definition + IEnumerable? foreignKeyAttributes = + propertyInfo.MultipleAttribute(); + if (foreignKeyAttributes != null) { - definition.PropertyType = propertyInfo.PropertyType; + foreach (ForeignKeyAttribute foreignKeyAttribute in foreignKeyAttributes) + { + ForeignKeyDefinition foreignKeyDefinition = GetForeignKeyDefinition(modelType, propertyInfo, + foreignKeyAttribute, columnName, tableName); + tableDefinition.ForeignKeys.Add(foreignKeyDefinition); + } } - //Look for Primary Key for the current column - var primaryKeyColumnAttribute = propertyInfo.FirstAttribute(); - if (primaryKeyColumnAttribute != null) + //Creates an index definition and adds it to the collection on the table definition + IndexAttribute? indexAttribute = propertyInfo.FirstAttribute(); + if (indexAttribute != null) { - string primaryKeyName = string.IsNullOrEmpty(primaryKeyColumnAttribute.Name) - ? string.Format("PK_{0}", tableName) - : primaryKeyColumnAttribute.Name; - - definition.IsPrimaryKey = true; - definition.IsIdentity = primaryKeyColumnAttribute.AutoIncrement; - definition.IsIndexed = primaryKeyColumnAttribute.Clustered; - definition.PrimaryKeyName = primaryKeyName; - definition.PrimaryKeyColumns = primaryKeyColumnAttribute.OnColumns ?? string.Empty; - definition.Seeding = primaryKeyColumnAttribute.IdentitySeed; + IndexDefinition indexDefinition = + GetIndexDefinition(modelType, propertyInfo, indexAttribute, columnName, tableName); + tableDefinition.Indexes.Add(indexDefinition); } + } - //Look for Size/Length of DbType - var lengthAttribute = propertyInfo.FirstAttribute(); - if (lengthAttribute != null) - { - definition.Size = lengthAttribute.Length; - } + return tableDefinition; + } - //Look for Constraint for the current column - var constraintAttribute = propertyInfo.FirstAttribute(); - if (constraintAttribute != null) - { - definition.ConstraintName = constraintAttribute.Name ?? string.Empty; - definition.DefaultValue = constraintAttribute.Default ?? string.Empty; - } + public static ColumnDefinition GetColumnDefinition(Type modelType, PropertyInfo propertyInfo, string columnName, + string tableName, ISqlSyntaxProvider sqlSyntax) + { + var definition = new ColumnDefinition + { + Name = columnName, TableName = tableName, ModificationType = ModificationType.Create + }; - return definition; + //Look for specific Null setting attributed a column + NullSettingAttribute? nullSettingAttribute = propertyInfo.FirstAttribute(); + if (nullSettingAttribute != null) + { + definition.IsNullable = nullSettingAttribute.NullSetting == NullSettings.Null; } - public static ForeignKeyDefinition GetForeignKeyDefinition(Type modelType, PropertyInfo propertyInfo, - ForeignKeyAttribute attribute, string columnName, string tableName) + //Look for specific DbType attributed a column + SpecialDbTypeAttribute? databaseTypeAttribute = propertyInfo.FirstAttribute(); + if (databaseTypeAttribute != null) { - var referencedTable = attribute.Type.FirstAttribute(); - var referencedPrimaryKey = attribute.Type.FirstAttribute(); - - string referencedColumn = string.IsNullOrEmpty(attribute.Column) - ? referencedPrimaryKey!.Value - : attribute.Column; - - string foreignKeyName = string.IsNullOrEmpty(attribute.Name) - ? string.Format("FK_{0}_{1}_{2}", tableName, referencedTable!.Value, referencedColumn) - : attribute.Name; - - var definition = new ForeignKeyDefinition - { - Name = foreignKeyName, - ForeignTable = tableName, - PrimaryTable = referencedTable!.Value, - OnDelete = attribute.OnDelete, - OnUpdate = attribute.OnUpdate - }; - definition.ForeignColumns.Add(columnName); - definition.PrimaryColumns.Add(referencedColumn); - - return definition; + definition.CustomDbType = databaseTypeAttribute.DatabaseType; + } + else + { + definition.PropertyType = propertyInfo.PropertyType; + } + + //Look for Primary Key for the current column + PrimaryKeyColumnAttribute? primaryKeyColumnAttribute = propertyInfo.FirstAttribute(); + if (primaryKeyColumnAttribute != null) + { + var primaryKeyName = string.IsNullOrEmpty(primaryKeyColumnAttribute.Name) + ? string.Format("PK_{0}", tableName) + : primaryKeyColumnAttribute.Name; + + definition.IsPrimaryKey = true; + definition.IsIdentity = primaryKeyColumnAttribute.AutoIncrement; + definition.IsIndexed = primaryKeyColumnAttribute.Clustered; + definition.PrimaryKeyName = primaryKeyName; + definition.PrimaryKeyColumns = primaryKeyColumnAttribute.OnColumns ?? string.Empty; + definition.Seeding = primaryKeyColumnAttribute.IdentitySeed; } - public static IndexDefinition GetIndexDefinition(Type modelType, PropertyInfo propertyInfo, IndexAttribute attribute, string columnName, string tableName) + //Look for Size/Length of DbType + LengthAttribute? lengthAttribute = propertyInfo.FirstAttribute(); + if (lengthAttribute != null) { - string indexName = string.IsNullOrEmpty(attribute.Name) - ? string.Format("IX_{0}_{1}", tableName, columnName) - : attribute.Name; - - var definition = new IndexDefinition - { - Name = indexName, - IndexType = attribute.IndexType, - ColumnName = columnName, - TableName = tableName, - }; - - if (string.IsNullOrEmpty(attribute.ForColumns) == false) + definition.Size = lengthAttribute.Length; + } + + //Look for Constraint for the current column + ConstraintAttribute? constraintAttribute = propertyInfo.FirstAttribute(); + if (constraintAttribute != null) + { + definition.ConstraintName = constraintAttribute.Name ?? string.Empty; + definition.DefaultValue = constraintAttribute.Default ?? string.Empty; + } + + return definition; + } + + public static ForeignKeyDefinition GetForeignKeyDefinition(Type modelType, PropertyInfo propertyInfo, + ForeignKeyAttribute attribute, string columnName, string tableName) + { + TableNameAttribute? referencedTable = attribute.Type.FirstAttribute(); + PrimaryKeyAttribute? referencedPrimaryKey = attribute.Type.FirstAttribute(); + + var referencedColumn = string.IsNullOrEmpty(attribute.Column) + ? referencedPrimaryKey!.Value + : attribute.Column; + + var foreignKeyName = string.IsNullOrEmpty(attribute.Name) + ? string.Format("FK_{0}_{1}_{2}", tableName, referencedTable!.Value, referencedColumn) + : attribute.Name; + + var definition = new ForeignKeyDefinition + { + Name = foreignKeyName, + ForeignTable = tableName, + PrimaryTable = referencedTable!.Value, + OnDelete = attribute.OnDelete, + OnUpdate = attribute.OnUpdate + }; + definition.ForeignColumns.Add(columnName); + definition.PrimaryColumns.Add(referencedColumn); + + return definition; + } + + public static IndexDefinition GetIndexDefinition(Type modelType, PropertyInfo propertyInfo, + IndexAttribute attribute, string columnName, string tableName) + { + var indexName = string.IsNullOrEmpty(attribute.Name) + ? string.Format("IX_{0}_{1}", tableName, columnName) + : attribute.Name; + + var definition = new IndexDefinition + { + Name = indexName, IndexType = attribute.IndexType, ColumnName = columnName, TableName = tableName + }; + + if (string.IsNullOrEmpty(attribute.ForColumns) == false) + { + IEnumerable columns = attribute.ForColumns.Split(Constants.CharArrays.Comma).Select(p => p.Trim()); + foreach (var column in columns) { - var columns = attribute.ForColumns.Split(Constants.CharArrays.Comma).Select(p => p.Trim()); - foreach (var column in columns) - { - definition.Columns.Add(new IndexColumnDefinition {Name = column, Direction = Direction.Ascending}); - } + definition.Columns.Add(new IndexColumnDefinition {Name = column, Direction = Direction.Ascending}); } - if (string.IsNullOrEmpty(attribute.IncludeColumns) == false) + } + + if (string.IsNullOrEmpty(attribute.IncludeColumns) == false) + { + IEnumerable columns = attribute.IncludeColumns.Split(',').Select(p => p.Trim()); + foreach (var column in columns) { - var columns = attribute.IncludeColumns.Split(',').Select(p => p.Trim()); - foreach (var column in columns) - { - definition.IncludeColumns.Add(new IndexColumnDefinition { Name = column, Direction = Direction.Ascending }); - } + definition.IncludeColumns.Add( + new IndexColumnDefinition {Name = column, Direction = Direction.Ascending}); } - return definition; } + + return definition; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DeletionDataDefinition.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DeletionDataDefinition.cs index c6033f898dd0..3c2f70b611cb 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DeletionDataDefinition.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DeletionDataDefinition.cs @@ -1,9 +1,5 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions +public class DeletionDataDefinition : List> { - public class DeletionDataDefinition : List> - { - - } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ForeignKeyDefinition.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ForeignKeyDefinition.cs index e6752159de2c..6c0beae67ce6 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ForeignKeyDefinition.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ForeignKeyDefinition.cs @@ -1,27 +1,25 @@ -using System.Collections.Generic; -using System.Data; +using System.Data; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; + +public class ForeignKeyDefinition { - public class ForeignKeyDefinition + public ForeignKeyDefinition() { - public ForeignKeyDefinition() - { - ForeignColumns = new List(); - PrimaryColumns = new List(); - //Set to None by Default - OnDelete = Rule.None; - OnUpdate = Rule.None; - } - - public virtual string? Name { get; set; } - public virtual string? ForeignTable { get; set; } - public virtual string? ForeignTableSchema { get; set; } - public virtual string? PrimaryTable { get; set; } - public virtual string? PrimaryTableSchema { get; set; } - public virtual Rule OnDelete { get; set; } - public virtual Rule OnUpdate { get; set; } - public virtual ICollection ForeignColumns { get; set; } - public virtual ICollection PrimaryColumns { get; set; } + ForeignColumns = new List(); + PrimaryColumns = new List(); + //Set to None by Default + OnDelete = Rule.None; + OnUpdate = Rule.None; } + + public virtual string? Name { get; set; } + public virtual string? ForeignTable { get; set; } + public virtual string? ForeignTableSchema { get; set; } + public virtual string? PrimaryTable { get; set; } + public virtual string? PrimaryTableSchema { get; set; } + public virtual Rule OnDelete { get; set; } + public virtual Rule OnUpdate { get; set; } + public virtual ICollection ForeignColumns { get; set; } + public virtual ICollection PrimaryColumns { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/IndexColumnDefinition.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/IndexColumnDefinition.cs index 6f3e34e0e4d4..3e8411e47843 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/IndexColumnDefinition.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/IndexColumnDefinition.cs @@ -1,10 +1,9 @@ using Umbraco.Cms.Core; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; + +public class IndexColumnDefinition { - public class IndexColumnDefinition - { - public virtual string? Name { get; set; } - public virtual Direction Direction { get; set; } - } + public virtual string? Name { get; set; } + public virtual Direction Direction { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/IndexDefinition.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/IndexDefinition.cs index 8761ae2a29b8..65e0ab0f027f 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/IndexDefinition.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/IndexDefinition.cs @@ -1,17 +1,15 @@ -using System.Collections.Generic; -using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; + +public class IndexDefinition { - public class IndexDefinition - { - public virtual string? Name { get; set; } - public virtual string? SchemaName { get; set; } - public virtual string? TableName { get; set; } - public virtual string? ColumnName { get; set; } + public virtual string? Name { get; set; } + public virtual string? SchemaName { get; set; } + public virtual string? TableName { get; set; } + public virtual string? ColumnName { get; set; } - public virtual ICollection Columns { get; set; } = new List(); - public virtual ICollection IncludeColumns { get; set; } = new List(); - public IndexTypes IndexType { get; set; } - } + public virtual ICollection Columns { get; set; } = new List(); + public virtual ICollection IncludeColumns { get; set; } = new List(); + public IndexTypes IndexType { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/InsertionDataDefinition.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/InsertionDataDefinition.cs index a09bcaafdf3e..d7e7982204d4 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/InsertionDataDefinition.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/InsertionDataDefinition.cs @@ -1,9 +1,5 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions +public class InsertionDataDefinition : List> { - public class InsertionDataDefinition : List> - { - - } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ModificationType.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ModificationType.cs index 490b06e41d38..eb42b44d8fdc 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ModificationType.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ModificationType.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; + +public enum ModificationType { - public enum ModificationType - { - Create, - Alter, - Drop, - Rename, - Insert, - Update, - Delete - } + Create, + Alter, + Drop, + Rename, + Insert, + Update, + Delete } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/SystemMethods.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/SystemMethods.cs index 24daa49f350e..5409a6b315f1 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/SystemMethods.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/SystemMethods.cs @@ -1,10 +1,10 @@ -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; + +public enum SystemMethods { - public enum SystemMethods - { - NewGuid, - CurrentDateTime, - //NewSequentialId, - //CurrentUTCDateTime - } + NewGuid, + + CurrentDateTime + //NewSequentialId, + //CurrentUTCDateTime } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/TableDefinition.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/TableDefinition.cs index 4ab479b8fd06..feab9e02a71d 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/TableDefinition.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/TableDefinition.cs @@ -1,20 +1,17 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions +public class TableDefinition { - public class TableDefinition + public TableDefinition() { - public TableDefinition() - { - Columns = new List(); - ForeignKeys = new List(); - Indexes = new List(); - } - - public virtual string Name { get; set; } = null!; - public virtual string SchemaName { get; set; } = null!; - public virtual ICollection Columns { get; set; } - public virtual ICollection ForeignKeys { get; set; } - public virtual ICollection Indexes { get; set; } + Columns = new List(); + ForeignKeys = new List(); + Indexes = new List(); } + + public virtual string Name { get; set; } = null!; + public virtual string SchemaName { get; set; } = null!; + public virtual ICollection Columns { get; set; } + public virtual ICollection ForeignKeys { get; set; } + public virtual ICollection Indexes { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DbCommandExtensions.cs b/src/Umbraco.Infrastructure/Persistence/DbCommandExtensions.cs index f70da7c8fbe4..63d05dccd503 100644 --- a/src/Umbraco.Infrastructure/Persistence/DbCommandExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/DbCommandExtensions.cs @@ -1,29 +1,33 @@ using System.Data; +using StackExchange.Profiling.Data; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +internal static class DbCommandExtensions { - internal static class DbCommandExtensions + /// + /// Unwraps a database command. + /// + /// + /// UmbracoDatabase wraps the original database connection in various layers (see + /// OnConnectionOpened); this unwraps and returns the original database command. + /// + public static IDbCommand UnwrapUmbraco(this IDbCommand command) { - /// - /// Unwraps a database command. - /// - /// UmbracoDatabase wraps the original database connection in various layers (see - /// OnConnectionOpened); this unwraps and returns the original database command. - public static IDbCommand UnwrapUmbraco(this IDbCommand command) + IDbCommand unwrapped; + + IDbCommand c = command; + do { - IDbCommand unwrapped; + unwrapped = c; - var c = command; - do + var profiled = unwrapped as ProfiledDbCommand; + if (profiled != null) { - unwrapped = c; - - var profiled = unwrapped as StackExchange.Profiling.Data.ProfiledDbCommand; - if (profiled != null) unwrapped = profiled.InternalCommand; - - } while (c != unwrapped); + unwrapped = profiled.InternalCommand; + } + } while (c != unwrapped); - return unwrapped; - } + return unwrapped; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DbConnectionExtensions.cs b/src/Umbraco.Infrastructure/Persistence/DbConnectionExtensions.cs index 61601fef95ef..0a3155eb351d 100644 --- a/src/Umbraco.Infrastructure/Persistence/DbConnectionExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/DbConnectionExtensions.cs @@ -1,4 +1,3 @@ -using System; using System.Data; using System.Data.Common; using Microsoft.Extensions.Logging; @@ -6,68 +5,70 @@ using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.FaultHandling; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class DbConnectionExtensions { - public static class DbConnectionExtensions + public static bool IsConnectionAvailable(string? connectionString, DbProviderFactory? factory) { - public static bool IsConnectionAvailable(string? connectionString, DbProviderFactory? factory) - { - var connection = factory?.CreateConnection(); - - if (connection == null) - throw new InvalidOperationException($"Could not create a connection for provider \"{factory}\"."); + DbConnection? connection = factory?.CreateConnection(); - connection.ConnectionString = connectionString; - using (connection) - { - return connection.IsAvailable(); - } + if (connection == null) + { + throw new InvalidOperationException($"Could not create a connection for provider \"{factory}\"."); } - public static bool IsAvailable(this IDbConnection connection) + connection.ConnectionString = connectionString; + using (connection) { - try - { - connection.Open(); - connection.Close(); - } - catch (DbException e) - { - // Don't swallow this error, the exception is super handy for knowing "why" its not available - StaticApplicationLogging.Logger.LogWarning(e, "Configured database is reporting as not being available."); - return false; - } + return connection.IsAvailable(); + } + } - return true; + public static bool IsAvailable(this IDbConnection connection) + { + try + { + connection.Open(); + connection.Close(); + } + catch (DbException e) + { + // Don't swallow this error, the exception is super handy for knowing "why" its not available + StaticApplicationLogging.Logger.LogWarning(e, "Configured database is reporting as not being available."); + return false; } - /// - /// Unwraps a database connection. - /// - /// UmbracoDatabase wraps the original database connection in various layers (see - /// OnConnectionOpened); this unwraps and returns the original database connection. - internal static IDbConnection UnwrapUmbraco(this IDbConnection connection) + return true; + } + + /// + /// Unwraps a database connection. + /// + /// + /// UmbracoDatabase wraps the original database connection in various layers (see + /// OnConnectionOpened); this unwraps and returns the original database connection. + /// + internal static IDbConnection UnwrapUmbraco(this IDbConnection connection) + { + IDbConnection? unwrapped = connection; + + IDbConnection c; + do { - var unwrapped = connection; + c = unwrapped; - IDbConnection c; - do + if (unwrapped is ProfiledDbConnection profiled) { - c = unwrapped; - - if (unwrapped is ProfiledDbConnection profiled) - { - unwrapped = profiled.WrappedConnection; - } + unwrapped = profiled.WrappedConnection; + } - if (unwrapped is RetryDbConnection retrying) - { - unwrapped = retrying.Inner; - } + if (unwrapped is RetryDbConnection retrying) + { + unwrapped = retrying.Inner; } - while (c != unwrapped); + } while (c != unwrapped); - return unwrapped; - } + return unwrapped; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs b/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs index 017747560998..395c231680ea 100644 --- a/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs +++ b/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs @@ -1,28 +1,25 @@ -using System; -using System.Collections.Generic; using System.Data.Common; -using System.Linq; using NPoco; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +public class DbProviderFactoryCreator : IDbProviderFactoryCreator { - public class DbProviderFactoryCreator : IDbProviderFactoryCreator - { - private readonly Func _getFactory; - private readonly IEnumerable _providerSpecificInterceptors; - private readonly IDictionary _databaseCreators; - private readonly IDictionary _syntaxProviders; - private readonly IDictionary _bulkSqlInsertProviders; - private readonly IDictionary _providerSpecificMapperFactories; + private readonly IDictionary _bulkSqlInsertProviders; + private readonly IDictionary _databaseCreators; + private readonly Func _getFactory; + private readonly IEnumerable _providerSpecificInterceptors; + private readonly IDictionary _providerSpecificMapperFactories; + private readonly IDictionary _syntaxProviders; - [Obsolete("Please use an alternative constructor.")] - public DbProviderFactoryCreator( - Func getFactory, - IEnumerable syntaxProviders, - IEnumerable bulkSqlInsertProviders, - IEnumerable databaseCreators, - IEnumerable providerSpecificMapperFactories) + [Obsolete("Please use an alternative constructor.")] + public DbProviderFactoryCreator( + Func getFactory, + IEnumerable syntaxProviders, + IEnumerable bulkSqlInsertProviders, + IEnumerable databaseCreators, + IEnumerable providerSpecificMapperFactories) : this( getFactory, syntaxProviders, @@ -30,74 +27,76 @@ public DbProviderFactoryCreator( databaseCreators, providerSpecificMapperFactories, Enumerable.Empty()) - { - } + { + } - public DbProviderFactoryCreator( - Func getFactory, - IEnumerable syntaxProviders, - IEnumerable bulkSqlInsertProviders, - IEnumerable databaseCreators, - IEnumerable providerSpecificMapperFactories, - IEnumerable providerSpecificInterceptors) + public DbProviderFactoryCreator( + Func getFactory, + IEnumerable syntaxProviders, + IEnumerable bulkSqlInsertProviders, + IEnumerable databaseCreators, + IEnumerable providerSpecificMapperFactories, + IEnumerable providerSpecificInterceptors) - { - _getFactory = getFactory; - _providerSpecificInterceptors = providerSpecificInterceptors; - _databaseCreators = databaseCreators.ToDictionary(x => x.ProviderName); - _syntaxProviders = syntaxProviders.ToDictionary(x => x.ProviderName); - _bulkSqlInsertProviders = bulkSqlInsertProviders.ToDictionary(x => x.ProviderName); - _providerSpecificMapperFactories = providerSpecificMapperFactories.ToDictionary(x => x.ProviderName); - } + { + _getFactory = getFactory; + _providerSpecificInterceptors = providerSpecificInterceptors; + _databaseCreators = databaseCreators.ToDictionary(x => x.ProviderName); + _syntaxProviders = syntaxProviders.ToDictionary(x => x.ProviderName); + _bulkSqlInsertProviders = bulkSqlInsertProviders.ToDictionary(x => x.ProviderName); + _providerSpecificMapperFactories = providerSpecificMapperFactories.ToDictionary(x => x.ProviderName); + } - public DbProviderFactory? CreateFactory(string? providerName) + public DbProviderFactory? CreateFactory(string? providerName) + { + if (string.IsNullOrEmpty(providerName)) { - if (string.IsNullOrEmpty(providerName)) - return null; - return _getFactory(providerName); + return null; } - // gets the sql syntax provider that corresponds, from attribute - public ISqlSyntaxProvider GetSqlSyntaxProvider(string providerName) - { - - if (!_syntaxProviders.TryGetValue(providerName, out var result)) - { - throw new InvalidOperationException($"Unknown provider name \"{providerName}\""); - } + return _getFactory(providerName); + } - return result; + // gets the sql syntax provider that corresponds, from attribute + public ISqlSyntaxProvider GetSqlSyntaxProvider(string providerName) + { + if (!_syntaxProviders.TryGetValue(providerName, out ISqlSyntaxProvider? result)) + { + throw new InvalidOperationException($"Unknown provider name \"{providerName}\""); } - public IBulkSqlInsertProvider CreateBulkSqlInsertProvider(string providerName) - { - if (!_bulkSqlInsertProviders.TryGetValue(providerName, out var result)) - { - throw new InvalidOperationException($"Unknown provider name \"{providerName}\""); - } + return result; + } - return result; + public IBulkSqlInsertProvider CreateBulkSqlInsertProvider(string providerName) + { + if (!_bulkSqlInsertProviders.TryGetValue(providerName, out IBulkSqlInsertProvider? result)) + { + throw new InvalidOperationException($"Unknown provider name \"{providerName}\""); } - public void CreateDatabase(string providerName, string connectionString) + return result; + } + + public void CreateDatabase(string providerName, string connectionString) + { + if (_databaseCreators.TryGetValue(providerName, out IDatabaseCreator? creator)) { - if (_databaseCreators.TryGetValue(providerName, out var creator)) - { - creator.Create(connectionString); - } + creator.Create(connectionString); } + } - public NPocoMapperCollection ProviderSpecificMappers(string providerName) + public NPocoMapperCollection ProviderSpecificMappers(string providerName) + { + if (_providerSpecificMapperFactories.TryGetValue(providerName, + out IProviderSpecificMapperFactory? mapperFactory)) { - if (_providerSpecificMapperFactories.TryGetValue(providerName, out var mapperFactory)) - { - return mapperFactory.Mappers; - } - - return new NPocoMapperCollection(() => Enumerable.Empty()); + return mapperFactory.Mappers; } - public IEnumerable GetProviderSpecificInterceptors(string providerName) - => _providerSpecificInterceptors.Where(x => x.ProviderName == providerName); + return new NPocoMapperCollection(() => Enumerable.Empty()); } + + public IEnumerable GetProviderSpecificInterceptors(string providerName) + => _providerSpecificInterceptors.Where(x => x.ProviderName == providerName); } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/AccessDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/AccessDto.cs index e8842b7cfd9c..a76f3c16b49c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/AccessDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/AccessDto.cs @@ -1,43 +1,41 @@ -using System; -using System.Collections.Generic; -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.Access)] +[PrimaryKey("id", AutoIncrement = false)] +[ExplicitColumns] +internal class AccessDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.Access)] - [PrimaryKey("id", AutoIncrement = false)] - [ExplicitColumns] - internal class AccessDto - { - [Column("id")] - [PrimaryKeyColumn(Name = "PK_umbracoAccess", AutoIncrement = false)] - public Guid Id { get; set; } - - [Column("nodeId")] - [ForeignKey(typeof(NodeDto), Name = "FK_umbracoAccess_umbracoNode_id")] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoAccess_nodeId")] - public int NodeId { get; set; } - - [Column("loginNodeId")] - [ForeignKey(typeof(NodeDto), Name = "FK_umbracoAccess_umbracoNode_id1")] - public int LoginNodeId { get; set; } - - [Column("noAccessNodeId")] - [ForeignKey(typeof(NodeDto), Name = "FK_umbracoAccess_umbracoNode_id2")] - public int NoAccessNodeId { get; set; } - - [Column("createDate")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime CreateDate { get; set; } - - [Column("updateDate")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime UpdateDate { get; set; } - - [ResultColumn] - [Reference(ReferenceType.Many, ReferenceMemberName = "AccessId")] - public List Rules { get; set; } = null!; - } + [Column("id")] + [PrimaryKeyColumn(Name = "PK_umbracoAccess", AutoIncrement = false)] + public Guid Id { get; set; } + + [Column("nodeId")] + [ForeignKey(typeof(NodeDto), Name = "FK_umbracoAccess_umbracoNode_id")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoAccess_nodeId")] + public int NodeId { get; set; } + + [Column("loginNodeId")] + [ForeignKey(typeof(NodeDto), Name = "FK_umbracoAccess_umbracoNode_id1")] + public int LoginNodeId { get; set; } + + [Column("noAccessNodeId")] + [ForeignKey(typeof(NodeDto), Name = "FK_umbracoAccess_umbracoNode_id2")] + public int NoAccessNodeId { get; set; } + + [Column("createDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime CreateDate { get; set; } + + [Column("updateDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime UpdateDate { get; set; } + + [ResultColumn] + [Reference(ReferenceType.Many, ReferenceMemberName = "AccessId")] + public List Rules { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/AccessRuleDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/AccessRuleDto.cs index 307f91337bfc..fa1f9453a1eb 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/AccessRuleDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/AccessRuleDto.cs @@ -1,36 +1,34 @@ -using System; -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.AccessRule)] +[PrimaryKey("id", AutoIncrement = false)] +[ExplicitColumns] +internal class AccessRuleDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.AccessRule)] - [PrimaryKey("id", AutoIncrement = false)] - [ExplicitColumns] - internal class AccessRuleDto - { - [Column("id")] - [PrimaryKeyColumn(Name = "PK_umbracoAccessRule", AutoIncrement = false)] - public Guid Id { get; set; } + [Column("id")] + [PrimaryKeyColumn(Name = "PK_umbracoAccessRule", AutoIncrement = false)] + public Guid Id { get; set; } - [Column("accessId")] - [ForeignKey(typeof(AccessDto), Name = "FK_umbracoAccessRule_umbracoAccess_id")] - public Guid AccessId { get; set; } + [Column("accessId")] + [ForeignKey(typeof(AccessDto), Name = "FK_umbracoAccessRule_umbracoAccess_id")] + public Guid AccessId { get; set; } - [Column("ruleValue")] - [Index(IndexTypes.UniqueNonClustered, ForColumns = "ruleValue,ruleType,accessId", Name = "IX_umbracoAccessRule")] - public string? RuleValue { get; set; } + [Column("ruleValue")] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "ruleValue,ruleType,accessId", Name = "IX_umbracoAccessRule")] + public string? RuleValue { get; set; } - [Column("ruleType")] - public string? RuleType { get; set; } + [Column("ruleType")] public string? RuleType { get; set; } - [Column("createDate")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime CreateDate { get; set; } + [Column("createDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime CreateDate { get; set; } - [Column("updateDate")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime UpdateDate { get; set; } - } + [Column("updateDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime UpdateDate { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/AuditEntryDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/AuditEntryDto.cs index 18ffda302ebd..ed38b7b922c1 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/AuditEntryDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/AuditEntryDto.cs @@ -1,57 +1,50 @@ -using System; -using NPoco; +using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.AuditEntry)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class AuditEntryDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.AuditEntry)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class AuditEntryDto - { - - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } - - // there is NO foreign key to the users table here, neither for performing user nor for - // affected user, so we can delete users and NOT delete the associated audit trails, and - // users can still be identified via the details free-form text fields. - - [Column("performingUserId")] - public int PerformingUserId { get; set; } - - [Column("performingDetails")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(Constants.Audit.DetailsLength)] - public string? PerformingDetails { get; set; } - - [Column("performingIp")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(Constants.Audit.IpLength)] - public string? PerformingIp { get; set; } - - [Column("eventDateUtc")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime EventDateUtc { get; set; } - - [Column("affectedUserId")] - public int AffectedUserId { get; set; } - - [Column("affectedDetails")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(Constants.Audit.DetailsLength)] - public string? AffectedDetails { get; set; } - - [Column("eventType")] - [Length(Constants.Audit.EventTypeLength)] - public string? EventType { get; set; } - - [Column("eventDetails")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(Constants.Audit.DetailsLength)] - public string? EventDetails { get; set; } - } + [Column("id")] [PrimaryKeyColumn] public int Id { get; set; } + + // there is NO foreign key to the users table here, neither for performing user nor for + // affected user, so we can delete users and NOT delete the associated audit trails, and + // users can still be identified via the details free-form text fields. + + [Column("performingUserId")] public int PerformingUserId { get; set; } + + [Column("performingDetails")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(Constants.Audit.DetailsLength)] + public string? PerformingDetails { get; set; } + + [Column("performingIp")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(Constants.Audit.IpLength)] + public string? PerformingIp { get; set; } + + [Column("eventDateUtc")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime EventDateUtc { get; set; } + + [Column("affectedUserId")] public int AffectedUserId { get; set; } + + [Column("affectedDetails")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(Constants.Audit.DetailsLength)] + public string? AffectedDetails { get; set; } + + [Column("eventType")] + [Length(Constants.Audit.EventTypeLength)] + public string? EventType { get; set; } + + [Column("eventDetails")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(Constants.Audit.DetailsLength)] + public string? EventDetails { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/AxisDefintionDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/AxisDefintionDto.cs index 431502a8965c..abd20683d307 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/AxisDefintionDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/AxisDefintionDto.cs @@ -1,16 +1,12 @@ using NPoco; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +internal class AxisDefintionDto { - internal class AxisDefintionDto - { - [Column("nodeId")] - public int NodeId { get; set; } + [Column("nodeId")] public int NodeId { get; set; } - [Column("alias")] - public string? Alias { get; set; } + [Column("alias")] public string? Alias { get; set; } - [Column("ParentID")] - public int ParentId { get; set; } - } + [Column("ParentID")] public int ParentId { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/CacheInstructionDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/CacheInstructionDto.cs index 7d57fca606c4..94d01ef664b2 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/CacheInstructionDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/CacheInstructionDto.cs @@ -1,36 +1,35 @@ -using System; -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.CacheInstruction)] +[PrimaryKey("id")] +[ExplicitColumns] +public class CacheInstructionDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.CacheInstruction)] - [PrimaryKey("id")] - [ExplicitColumns] - public class CacheInstructionDto - { - [Column("id")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [PrimaryKeyColumn(AutoIncrement = true, Name = "PK_umbracoCacheInstruction")] - public int Id { get; set; } + [Column("id")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [PrimaryKeyColumn(AutoIncrement = true, Name = "PK_umbracoCacheInstruction")] + public int Id { get; set; } - [Column("utcStamp")] - [NullSetting(NullSetting = NullSettings.NotNull)] - public DateTime UtcStamp { get; set; } + [Column("utcStamp")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public DateTime UtcStamp { get; set; } - [Column("jsonInstruction")] - [SpecialDbType(SpecialDbTypes.NTEXT)] - [NullSetting(NullSetting = NullSettings.NotNull)] - public string Instructions { get; set; } = null!; + [Column("jsonInstruction")] + [SpecialDbType(SpecialDbTypes.NTEXT)] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string Instructions { get; set; } = null!; - [Column("originated")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Length(500)] - public string OriginIdentity { get; set; } = null!; + [Column("originated")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Length(500)] + public string OriginIdentity { get; set; } = null!; - [Column("instructionCount")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Constraint(Default = 1)] - public int InstructionCount { get; set; } - } + [Column("instructionCount")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = 1)] + public int InstructionCount { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ConsentDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ConsentDto.cs index e0c9b73c78bf..757bed50fd4c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ConsentDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ConsentDto.cs @@ -1,43 +1,32 @@ -using System; -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.Consent)] +[PrimaryKey("id")] +[ExplicitColumns] +public class ConsentDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.Consent)] - [PrimaryKey("id")] - [ExplicitColumns] - public class ConsentDto - { - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } - - [Column("current")] - public bool Current { get; set; } - - [Column("source")] - [Length(512)] - public string? Source { get; set; } - - [Column("context")] - [Length(128)] - public string? Context { get; set; } - - [Column("action")] - [Length(512)] - public string? Action { get; set; } - - [Column("createDate")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime CreateDate { get; set; } - - [Column("state")] - public int State { get; set; } - - [Column("comment")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Comment { get; set; } - } + [Column("id")] [PrimaryKeyColumn] public int Id { get; set; } + + [Column("current")] public bool Current { get; set; } + + [Column("source")] [Length(512)] public string? Source { get; set; } + + [Column("context")] [Length(128)] public string? Context { get; set; } + + [Column("action")] [Length(512)] public string? Action { get; set; } + + [Column("createDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime CreateDate { get; set; } + + [Column("state")] public int State { get; set; } + + [Column("comment")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Comment { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentDto.cs index 232f055e85de..3977d050e9dd 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentDto.cs @@ -1,33 +1,33 @@ using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("nodeId", AutoIncrement = false)] +[ExplicitColumns] +public class ContentDto { - [TableName(TableName)] - [PrimaryKey("nodeId", AutoIncrement = false)] - [ExplicitColumns] - public class ContentDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.Content; + public const string TableName = Constants.DatabaseSchema.Tables.Content; - [Column("nodeId")] - [PrimaryKeyColumn(AutoIncrement = false)] - [ForeignKey(typeof(NodeDto))] - public int NodeId { get; set; } + [Column("nodeId")] + [PrimaryKeyColumn(AutoIncrement = false)] + [ForeignKey(typeof(NodeDto))] + public int NodeId { get; set; } - [Column("contentTypeId")] - [ForeignKey(typeof(ContentTypeDto), Column = "NodeId")] - public int ContentTypeId { get; set; } + [Column("contentTypeId")] + [ForeignKey(typeof(ContentTypeDto), Column = "NodeId")] + public int ContentTypeId { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne, ColumnName = "NodeId")] - public NodeDto NodeDto { get; set; } = null!; + [ResultColumn] + [Reference(ReferenceType.OneToOne, ColumnName = "NodeId")] + public NodeDto NodeDto { get; set; } = null!; - // although a content has many content versions, - // they can only be loaded one by one (as several content), - // so this here is a OneToOne reference - [ResultColumn] - [Reference(ReferenceType.OneToOne, ReferenceMemberName = "NodeId")] - public ContentVersionDto ContentVersionDto { get; set; } = null!; - } + // although a content has many content versions, + // they can only be loaded one by one (as several content), + // so this here is a OneToOne reference + [ResultColumn] + [Reference(ReferenceType.OneToOne, ReferenceMemberName = "NodeId")] + public ContentVersionDto ContentVersionDto { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentNuDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentNuDto.cs index eb3077cb3b78..602d7243495c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentNuDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentNuDto.cs @@ -1,40 +1,36 @@ using System.Data; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos -{ - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.NodeData)] - [PrimaryKey("nodeId", AutoIncrement = false)] - [ExplicitColumns] - public class ContentNuDto - { - [Column("nodeId")] - [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_cmsContentNu", OnColumns = "nodeId, published")] - [ForeignKey(typeof(ContentDto), Column = "nodeId", OnDelete = Rule.Cascade)] - public int NodeId { get; set; } - - [Column("published")] - public bool Published { get; set; } +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; - /// - /// Stores serialized JSON representing the content item's property and culture name values - /// - /// - /// Pretty much anything that would require a 1:M lookup is serialized here - /// - [Column("data")] - [SpecialDbType(SpecialDbTypes.NTEXT)] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Data { get; set; } +[TableName(Constants.DatabaseSchema.Tables.NodeData)] +[PrimaryKey("nodeId", AutoIncrement = false)] +[ExplicitColumns] +public class ContentNuDto +{ + [Column("nodeId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_cmsContentNu", OnColumns = "nodeId, published")] + [ForeignKey(typeof(ContentDto), Column = "nodeId", OnDelete = Rule.Cascade)] + public int NodeId { get; set; } - [Column("rv")] - public long Rv { get; set; } + [Column("published")] public bool Published { get; set; } - [Column("dataRaw")] - [NullSetting(NullSetting = NullSettings.Null)] - public byte[]? RawData { get; set; } + /// + /// Stores serialized JSON representing the content item's property and culture name values + /// + /// + /// Pretty much anything that would require a 1:M lookup is serialized here + /// + [Column("data")] + [SpecialDbType(SpecialDbTypes.NTEXT)] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Data { get; set; } + [Column("rv")] public long Rv { get; set; } - } + [Column("dataRaw")] + [NullSetting(NullSetting = NullSettings.Null)] + public byte[]? RawData { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentScheduleDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentScheduleDto.cs index d50da8a12442..ed953dd5284c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentScheduleDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentScheduleDto.cs @@ -1,33 +1,30 @@ -using System; -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id", AutoIncrement = false)] +[ExplicitColumns] +internal class ContentScheduleDto { - [TableName(TableName)] - [PrimaryKey("id", AutoIncrement = false)] - [ExplicitColumns] - internal class ContentScheduleDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.ContentSchedule; + public const string TableName = Constants.DatabaseSchema.Tables.ContentSchedule; - [Column("id")] - [PrimaryKeyColumn(AutoIncrement = false)] - public Guid Id { get; set; } + [Column("id")] + [PrimaryKeyColumn(AutoIncrement = false)] + public Guid Id { get; set; } - [Column("nodeId")] - [ForeignKey(typeof(ContentDto))] - public int NodeId { get; set; } + [Column("nodeId")] + [ForeignKey(typeof(ContentDto))] + public int NodeId { get; set; } - [Column("languageId")] - [ForeignKey(typeof(LanguageDto))] - [NullSetting(NullSetting = NullSettings.Null)] // can be invariant - public int? LanguageId { get; set; } + [Column("languageId")] + [ForeignKey(typeof(LanguageDto))] + [NullSetting(NullSetting = NullSettings.Null)] // can be invariant + public int? LanguageId { get; set; } - [Column("date")] - public DateTime Date { get; set; } + [Column("date")] public DateTime Date { get; set; } - [Column("action")] - public string? Action { get; set; } - } + [Column("action")] public string? Action { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentType2ContentTypeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentType2ContentTypeDto.cs index 2bda31c1fc39..3b7d9e5a7fc9 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentType2ContentTypeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentType2ContentTypeDto.cs @@ -1,19 +1,20 @@ using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.ElementTypeTree)] +[ExplicitColumns] +internal class ContentType2ContentTypeDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.ElementTypeTree)] - [ExplicitColumns] - internal class ContentType2ContentTypeDto - { - [Column("parentContentTypeId")] - [PrimaryKeyColumn(AutoIncrement = false, Clustered = true, Name = "PK_cmsContentType2ContentType", OnColumns = "parentContentTypeId, childContentTypeId")] - [ForeignKey(typeof(NodeDto), Name = "FK_cmsContentType2ContentType_umbracoNode_parent")] - public int ParentId { get; set; } + [Column("parentContentTypeId")] + [PrimaryKeyColumn(AutoIncrement = false, Clustered = true, Name = "PK_cmsContentType2ContentType", + OnColumns = "parentContentTypeId, childContentTypeId")] + [ForeignKey(typeof(NodeDto), Name = "FK_cmsContentType2ContentType_umbracoNode_parent")] + public int ParentId { get; set; } - [Column("childContentTypeId")] - [ForeignKey(typeof(NodeDto), Name = "FK_cmsContentType2ContentType_umbracoNode_child")] - public int ChildId { get; set; } - } + [Column("childContentTypeId")] + [ForeignKey(typeof(NodeDto), Name = "FK_cmsContentType2ContentType_umbracoNode_child")] + public int ChildId { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeAllowedContentTypeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeAllowedContentTypeDto.cs index c9a4b274b7cf..6b1ddcf00dd4 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeAllowedContentTypeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeAllowedContentTypeDto.cs @@ -1,28 +1,30 @@ using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.ContentChildType)] +[PrimaryKey("Id", AutoIncrement = false)] +[ExplicitColumns] +internal class ContentTypeAllowedContentTypeDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.ContentChildType)] - [PrimaryKey("Id", AutoIncrement = false)] - [ExplicitColumns] - internal class ContentTypeAllowedContentTypeDto - { - [Column("Id")] - [ForeignKey(typeof(ContentTypeDto), Name = "FK_cmsContentTypeAllowedContentType_cmsContentType", Column = "nodeId")] - [PrimaryKeyColumn(AutoIncrement = false, Clustered = true, Name = "PK_cmsContentTypeAllowedContentType", OnColumns = "Id, AllowedId")] - public int Id { get; set; } + [Column("Id")] + [ForeignKey(typeof(ContentTypeDto), Name = "FK_cmsContentTypeAllowedContentType_cmsContentType", Column = "nodeId")] + [PrimaryKeyColumn(AutoIncrement = false, Clustered = true, Name = "PK_cmsContentTypeAllowedContentType", + OnColumns = "Id, AllowedId")] + public int Id { get; set; } - [Column("AllowedId")] - [ForeignKey(typeof(ContentTypeDto), Name = "FK_cmsContentTypeAllowedContentType_cmsContentType1", Column = "nodeId")] - public int AllowedId { get; set; } + [Column("AllowedId")] + [ForeignKey(typeof(ContentTypeDto), Name = "FK_cmsContentTypeAllowedContentType_cmsContentType1", + Column = "nodeId")] + public int AllowedId { get; set; } - [Column("SortOrder")] - [Constraint(Name = "df_cmsContentTypeAllowedContentType_sortOrder", Default = "0")] - public int SortOrder { get; set; } + [Column("SortOrder")] + [Constraint(Name = "df_cmsContentTypeAllowedContentType_sortOrder", Default = "0")] + public int SortOrder { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne)] - public ContentTypeDto? ContentTypeDto { get; set; } - } + [ResultColumn] + [Reference(ReferenceType.OneToOne)] + public ContentTypeDto? ContentTypeDto { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeDto.cs index 8d4019de096a..783cdcdb1d21 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeDto.cs @@ -1,61 +1,61 @@ using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("pk")] +[ExplicitColumns] +internal class ContentTypeDto { - [TableName(TableName)] - [PrimaryKey("pk")] - [ExplicitColumns] - internal class ContentTypeDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.ContentType; - private string? _alias; - - [Column("pk")] - [PrimaryKeyColumn(IdentitySeed = 700)] - public int PrimaryKey { get; set; } - - [Column("nodeId")] - [ForeignKey(typeof(NodeDto))] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsContentType")] - public int NodeId { get; set; } - - [Column("alias")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Alias { get => _alias; set => _alias = value == null ? null : string.Intern(value); } - - [Column("icon")] - [Index(IndexTypes.NonClustered)] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Icon { get; set; } - - [Column("thumbnail")] - [Constraint(Default = "folder.png")] - public string? Thumbnail { get; set; } - - [Column("description")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(1500)] - public string? Description { get; set; } - - [Column("isContainer")] - [Constraint(Default = "0")] - public bool IsContainer { get; set; } - - [Column("isElement")] - [Constraint(Default = "0")] - public bool IsElement { get; set; } - - [Column("allowAtRoot")] - [Constraint(Default = "0")] - public bool AllowAtRoot { get; set; } - - [Column("variations")] - [Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)] - public byte Variations { get; set; } - - [ResultColumn] - [Reference(ReferenceType.OneToOne, ColumnName = "NodeId")] - public NodeDto NodeDto { get; set; } = null!; - } + public const string TableName = Constants.DatabaseSchema.Tables.ContentType; + private string? _alias; + + [Column("pk")] + [PrimaryKeyColumn(IdentitySeed = 700)] + public int PrimaryKey { get; set; } + + [Column("nodeId")] + [ForeignKey(typeof(NodeDto))] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsContentType")] + public int NodeId { get; set; } + + [Column("alias")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Alias { get => _alias; set => _alias = value == null ? null : string.Intern(value); } + + [Column("icon")] + [Index(IndexTypes.NonClustered)] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Icon { get; set; } + + [Column("thumbnail")] + [Constraint(Default = "folder.png")] + public string? Thumbnail { get; set; } + + [Column("description")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(1500)] + public string? Description { get; set; } + + [Column("isContainer")] + [Constraint(Default = "0")] + public bool IsContainer { get; set; } + + [Column("isElement")] + [Constraint(Default = "0")] + public bool IsElement { get; set; } + + [Column("allowAtRoot")] + [Constraint(Default = "0")] + public bool AllowAtRoot { get; set; } + + [Column("variations")] + [Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)] + public byte Variations { get; set; } + + [ResultColumn] + [Reference(ReferenceType.OneToOne, ColumnName = "NodeId")] + public NodeDto NodeDto { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeTemplateDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeTemplateDto.cs index 0b79aeb7aa93..9c64e3f706ed 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeTemplateDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeTemplateDto.cs @@ -1,29 +1,30 @@ using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.DocumentType)] +[PrimaryKey("contentTypeNodeId", AutoIncrement = false)] +[ExplicitColumns] +internal class ContentTypeTemplateDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.DocumentType)] - [PrimaryKey("contentTypeNodeId", AutoIncrement = false)] - [ExplicitColumns] - internal class ContentTypeTemplateDto - { - [Column("contentTypeNodeId")] - [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_cmsDocumentType", OnColumns = "contentTypeNodeId, templateNodeId")] - [ForeignKey(typeof(ContentTypeDto), Column = "nodeId")] - [ForeignKey(typeof(NodeDto))] - public int ContentTypeNodeId { get; set; } + [Column("contentTypeNodeId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_cmsDocumentType", + OnColumns = "contentTypeNodeId, templateNodeId")] + [ForeignKey(typeof(ContentTypeDto), Column = "nodeId")] + [ForeignKey(typeof(NodeDto))] + public int ContentTypeNodeId { get; set; } - [Column("templateNodeId")] - [ForeignKey(typeof(TemplateDto), Column = "nodeId")] - public int TemplateNodeId { get; set; } + [Column("templateNodeId")] + [ForeignKey(typeof(TemplateDto), Column = "nodeId")] + public int TemplateNodeId { get; set; } - [Column("IsDefault")] - [Constraint(Default = "0")] - public bool IsDefault { get; set; } + [Column("IsDefault")] + [Constraint(Default = "0")] + public bool IsDefault { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne)] - public ContentTypeDto? ContentTypeDto { get; set; } - } + [ResultColumn] + [Reference(ReferenceType.OneToOne)] + public ContentTypeDto? ContentTypeDto { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCleanupPolicyDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCleanupPolicyDto.cs index 4b2faa166f5b..5f1d87d542f3 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCleanupPolicyDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCleanupPolicyDto.cs @@ -1,34 +1,30 @@ -using System; using System.Data; using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("contentTypeId", AutoIncrement = false)] +[ExplicitColumns] +internal class ContentVersionCleanupPolicyDto { - [TableName(TableName)] - [PrimaryKey("contentTypeId", AutoIncrement = false)] - [ExplicitColumns] - internal class ContentVersionCleanupPolicyDto - { - public const string TableName = Constants.DatabaseSchema.Tables.ContentVersionCleanupPolicy; + public const string TableName = Constants.DatabaseSchema.Tables.ContentVersionCleanupPolicy; - [Column("contentTypeId")] - [ForeignKey(typeof(ContentTypeDto), Column = "nodeId", OnDelete = Rule.Cascade)] - public int ContentTypeId { get; set; } + [Column("contentTypeId")] + [ForeignKey(typeof(ContentTypeDto), Column = "nodeId", OnDelete = Rule.Cascade)] + public int ContentTypeId { get; set; } - [Column("preventCleanup")] - public bool PreventCleanup { get; set; } + [Column("preventCleanup")] public bool PreventCleanup { get; set; } - [Column("keepAllVersionsNewerThanDays")] - [NullSetting(NullSetting = NullSettings.Null)] - public int? KeepAllVersionsNewerThanDays { get; set; } + [Column("keepAllVersionsNewerThanDays")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? KeepAllVersionsNewerThanDays { get; set; } - [Column("keepLatestVersionPerDayForDays")] - [NullSetting(NullSetting = NullSettings.Null)] - public int? KeepLatestVersionPerDayForDays { get; set; } + [Column("keepLatestVersionPerDayForDays")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? KeepLatestVersionPerDayForDays { get; set; } - [Column("updated")] - public DateTime Updated { get; set; } - } + [Column("updated")] public DateTime Updated { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCultureVariationDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCultureVariationDto.cs index 32307efb2ba6..df50ef7164f3 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCultureVariationDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCultureVariationDto.cs @@ -1,44 +1,43 @@ -using System; -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class ContentVersionCultureVariationDto { - [TableName(TableName)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class ContentVersionCultureVariationDto + public const string TableName = Constants.DatabaseSchema.Tables.ContentVersionCultureVariation; + private int? _updateUserId; + + [Column("id")] [PrimaryKeyColumn] public int Id { get; set; } + + [Column("versionId")] + [ForeignKey(typeof(ContentVersionDto))] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_VersionId", ForColumns = "versionId,languageId")] + public int VersionId { get; set; } + + [Column("languageId")] + [ForeignKey(typeof(LanguageDto))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] + public int LanguageId { get; set; } + + // this is convenient to carry the culture around, but has no db counterpart + [Ignore] public string? Culture { get; set; } + + [Column("name")] public string? Name { get; set; } + + [Column("date")] // TODO: db rename to 'updateDate' + public DateTime UpdateDate { get; set; } + + [Column("availableUserId")] // TODO: db rename to 'updateDate' + [ForeignKey(typeof(UserDto))] + [NullSetting(NullSetting = NullSettings.Null)] + public int? UpdateUserId { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.ContentVersionCultureVariation; - private int? _updateUserId; - - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } - - [Column("versionId")] - [ForeignKey(typeof(ContentVersionDto))] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_VersionId", ForColumns = "versionId,languageId")] - public int VersionId { get; set; } - - [Column("languageId")] - [ForeignKey(typeof(LanguageDto))] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] - public int LanguageId { get; set; } - - // this is convenient to carry the culture around, but has no db counterpart - [Ignore] - public string? Culture { get; set; } - - [Column("name")] - public string? Name { get; set; } - - [Column("date")] // TODO: db rename to 'updateDate' - public DateTime UpdateDate { get; set; } - - [Column("availableUserId")] // TODO: db rename to 'updateDate' - [ForeignKey(typeof(UserDto))] - [NullSetting(NullSetting = NullSettings.Null)] - public int? UpdateUserId { get => _updateUserId == 0 ? null : _updateUserId; set => _updateUserId = value; } //return null if zero - } + get => _updateUserId == 0 ? null : _updateUserId; + set => _updateUserId = value; + } //return null if zero } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionDto.cs index a000811c5565..35e96d741d4d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionDto.cs @@ -1,57 +1,55 @@ -using System; -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id")] +[ExplicitColumns] +public class ContentVersionDto { - [TableName(TableName)] - [PrimaryKey("id")] - [ExplicitColumns] - public class ContentVersionDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion; - private int? _userId; - - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } - - [Column("nodeId")] - [ForeignKey(typeof(ContentDto))] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_NodeId", ForColumns = "nodeId,current", IncludeColumns = "id,versionDate,text,userId")] - public int NodeId { get; set; } - - [Column("versionDate")] // TODO: db rename to 'updateDate' - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime VersionDate { get; set; } - - [Column("userId")] // TODO: db rename to 'updateUserId' - [ForeignKey(typeof(UserDto))] - [NullSetting(NullSetting = NullSettings.Null)] - public int? UserId { get => _userId == 0 ? null : _userId; set => _userId = value; } //return null if zero - - [Column("current")] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Current", IncludeColumns = "nodeId")] - public bool Current { get; set; } - - // about current: - // there is nothing in the DB that guarantees that there will be one, and exactly one, current version per content item. - // that would require circular FKs that are impossible (well, it is possible to create them, but not to insert). - // we could use a content.currentVersionId FK that would need to be nullable, or (better?) an additional table - // linking a content itemt to its current version (nodeId, versionId) - that would guarantee uniqueness BUT it would - // not guarantee existence - so, really... we are trusting our code to manage 'current' correctly. - - [Column("text")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Text { get; set; } - - [ResultColumn] - [Reference(ReferenceType.OneToOne, ColumnName = "NodeId", ReferenceMemberName = "NodeId")] - public ContentDto? ContentDto { get; set; } - - [Column("preventCleanup")] - [Constraint(Default = "0")] - public bool PreventCleanup { get; set; } - } + public const string TableName = Constants.DatabaseSchema.Tables.ContentVersion; + private int? _userId; + + [Column("id")] [PrimaryKeyColumn] public int Id { get; set; } + + [Column("nodeId")] + [ForeignKey(typeof(ContentDto))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_NodeId", ForColumns = "nodeId,current", + IncludeColumns = "id,versionDate,text,userId")] + public int NodeId { get; set; } + + [Column("versionDate")] // TODO: db rename to 'updateDate' + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime VersionDate { get; set; } + + [Column("userId")] // TODO: db rename to 'updateUserId' + [ForeignKey(typeof(UserDto))] + [NullSetting(NullSetting = NullSettings.Null)] + public int? UserId { get => _userId == 0 ? null : _userId; set => _userId = value; } //return null if zero + + [Column("current")] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Current", IncludeColumns = "nodeId")] + public bool Current { get; set; } + + // about current: + // there is nothing in the DB that guarantees that there will be one, and exactly one, current version per content item. + // that would require circular FKs that are impossible (well, it is possible to create them, but not to insert). + // we could use a content.currentVersionId FK that would need to be nullable, or (better?) an additional table + // linking a content itemt to its current version (nodeId, versionId) - that would guarantee uniqueness BUT it would + // not guarantee existence - so, really... we are trusting our code to manage 'current' correctly. + + [Column("text")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Text { get; set; } + + [ResultColumn] + [Reference(ReferenceType.OneToOne, ColumnName = "NodeId", ReferenceMemberName = "NodeId")] + public ContentDto? ContentDto { get; set; } + + [Column("preventCleanup")] + [Constraint(Default = "0")] + public bool PreventCleanup { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/CreatedPackageSchemaDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/CreatedPackageSchemaDto.cs index ae6b922657ee..cffe2202c789 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/CreatedPackageSchemaDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/CreatedPackageSchemaDto.cs @@ -1,38 +1,35 @@ -using System; -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[ExplicitColumns] +[PrimaryKey("id")] +public class CreatedPackageSchemaDto { - [TableName(TableName)] - [ExplicitColumns] - [PrimaryKey("id")] - public class CreatedPackageSchemaDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.CreatedPackageSchema; + public const string TableName = Constants.DatabaseSchema.Tables.CreatedPackageSchema; - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + [Column("id")] [PrimaryKeyColumn] public int Id { get; set; } - [Column("name")] - [Length(255)] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.UniqueNonClustered, ForColumns = "name", Name = "IX_" + TableName + "_Name")] - public string Name { get; set; } = null!; + [Column("name")] + [Length(255)] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "name", Name = "IX_" + TableName + "_Name")] + public string Name { get; set; } = null!; - [Column("value")] - [SpecialDbType(SpecialDbTypes.NVARCHARMAX)] - [NullSetting(NullSetting = NullSettings.NotNull)] - public string Value { get; set; } = null!; + [Column("value")] + [SpecialDbType(SpecialDbTypes.NVARCHARMAX)] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string Value { get; set; } = null!; - [Column("updateDate")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime UpdateDate { get; set; } + [Column("updateDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime UpdateDate { get; set; } - [Column("packageId")] - [NullSetting(NullSetting = NullSettings.NotNull)] - public Guid PackageId { get; set; } - } + [Column("packageId")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public Guid PackageId { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/DataTypeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/DataTypeDto.cs index c51ce4947cb9..4ecc58cc1deb 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/DataTypeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/DataTypeDto.cs @@ -1,32 +1,29 @@ using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.DataType)] +[PrimaryKey("nodeId", AutoIncrement = false)] +[ExplicitColumns] +public class DataTypeDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.DataType)] - [PrimaryKey("nodeId", AutoIncrement = false)] - [ExplicitColumns] - public class DataTypeDto - { - [Column("nodeId")] - [PrimaryKeyColumn(AutoIncrement = false)] - [ForeignKey(typeof(NodeDto))] - public int NodeId { get; set; } + [Column("nodeId")] + [PrimaryKeyColumn(AutoIncrement = false)] + [ForeignKey(typeof(NodeDto))] + public int NodeId { get; set; } - [Column("propertyEditorAlias")] - public string EditorAlias { get; set; } = null!; // TODO: should this have a length + [Column("propertyEditorAlias")] public string EditorAlias { get; set; } = null!; // TODO: should this have a length - [Column("dbType")] - [Length(50)] - public string DbType { get; set; } = null!; + [Column("dbType")] [Length(50)] public string DbType { get; set; } = null!; - [Column("config")] - [SpecialDbType(SpecialDbTypes.NTEXT)] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Configuration { get; set; } + [Column("config")] + [SpecialDbType(SpecialDbTypes.NTEXT)] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Configuration { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne, ColumnName = "NodeId")] - public NodeDto NodeDto { get; set; } = null!; - } + [ResultColumn] + [Reference(ReferenceType.OneToOne, ColumnName = "NodeId")] + public NodeDto NodeDto { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/DictionaryDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/DictionaryDto.cs index ad14f20c6b80..64fade27fb5a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/DictionaryDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/DictionaryDto.cs @@ -1,38 +1,34 @@ -using System; -using System.Collections.Generic; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("pk")] +[ExplicitColumns] +public class DictionaryDto // public as required to be accessible from Deploy for the RepairDictionaryIdsWorkItem. { - [TableName(TableName)] - [PrimaryKey("pk")] - [ExplicitColumns] - public class DictionaryDto // public as required to be accessible from Deploy for the RepairDictionaryIdsWorkItem. - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.DictionaryEntry; + public const string TableName = Constants.DatabaseSchema.Tables.DictionaryEntry; - [Column("pk")] - [PrimaryKeyColumn] - public int PrimaryKey { get; set; } + [Column("pk")] [PrimaryKeyColumn] public int PrimaryKey { get; set; } - [Column("id")] - [Index(IndexTypes.UniqueNonClustered)] - public Guid UniqueId { get; set; } + [Column("id")] + [Index(IndexTypes.UniqueNonClustered)] + public Guid UniqueId { get; set; } - [Column("parent")] - [NullSetting(NullSetting = NullSettings.Null)] - [ForeignKey(typeof(DictionaryDto), Column = "id")] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Parent")] - public Guid? Parent { get; set; } + [Column("parent")] + [NullSetting(NullSetting = NullSettings.Null)] + [ForeignKey(typeof(DictionaryDto), Column = "id")] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Parent")] + public Guid? Parent { get; set; } - [Column("key")] - [Length(450)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_key")] - public string Key { get; set; } = null!; + [Column("key")] + [Length(450)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_key")] + public string Key { get; set; } = null!; - [ResultColumn] - [Reference(ReferenceType.Many, ColumnName = "UniqueId", ReferenceMemberName = "UniqueId")] - public List? LanguageTextDtos { get; set; } - } + [ResultColumn] + [Reference(ReferenceType.Many, ColumnName = "UniqueId", ReferenceMemberName = "UniqueId")] + public List? LanguageTextDtos { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentCultureVariationDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentCultureVariationDto.cs index e13d19ae3495..2b6743d867a2 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentCultureVariationDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentCultureVariationDto.cs @@ -1,52 +1,46 @@ using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class DocumentCultureVariationDto { - [TableName(TableName)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class DocumentCultureVariationDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.DocumentCultureVariation; - - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } - - [Column("nodeId")] - [ForeignKey(typeof(NodeDto))] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_NodeId", ForColumns = "nodeId,languageId")] - public int NodeId { get; set; } - - [Column("languageId")] - [ForeignKey(typeof(LanguageDto))] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] - public int LanguageId { get; set; } - - // this is convenient to carry the culture around, but has no db counterpart - [Ignore] - public string? Culture { get; set; } - - // authority on whether a culture has been edited - [Column("edited")] - public bool Edited { get; set; } - - // de-normalized for perfs - // (means there is a current content version culture variation for the language) - [Column("available")] - public bool Available { get; set; } - - // de-normalized for perfs - // (means there is a published content version culture variation for the language) - [Column("published")] - public bool Published { get; set; } - - // de-normalized for perfs - // (when available, copies name from current content version culture variation for the language) - // (otherwise, it's the published one, 'cos we need to have one) - [Column("name")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Name { get; set; } - } + public const string TableName = Constants.DatabaseSchema.Tables.DocumentCultureVariation; + + [Column("id")] [PrimaryKeyColumn] public int Id { get; set; } + + [Column("nodeId")] + [ForeignKey(typeof(NodeDto))] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_NodeId", ForColumns = "nodeId,languageId")] + public int NodeId { get; set; } + + [Column("languageId")] + [ForeignKey(typeof(LanguageDto))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] + public int LanguageId { get; set; } + + // this is convenient to carry the culture around, but has no db counterpart + [Ignore] public string? Culture { get; set; } + + // authority on whether a culture has been edited + [Column("edited")] public bool Edited { get; set; } + + // de-normalized for perfs + // (means there is a current content version culture variation for the language) + [Column("available")] public bool Available { get; set; } + + // de-normalized for perfs + // (means there is a published content version culture variation for the language) + [Column("published")] public bool Published { get; set; } + + // de-normalized for perfs + // (when available, copies name from current content version culture variation for the language) + // (otherwise, it's the published one, 'cos we need to have one) + [Column("name")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Name { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentDto.cs index 39e4e933b20c..ad08fbb2c312 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentDto.cs @@ -1,58 +1,56 @@ using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos -{ +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; - [TableName(TableName)] - [PrimaryKey("nodeId", AutoIncrement = false)] - [ExplicitColumns] - public class DocumentDto - { - private const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.Document; - - [Column("nodeId")] - [PrimaryKeyColumn(AutoIncrement = false)] - [ForeignKey(typeof(ContentDto))] - public int NodeId { get; set; } - - [Column("published")] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Published")] - public bool Published { get; set; } - - [Column("edited")] - public bool Edited { get; set; } - - //[Column("publishDate")] - //[NullSetting(NullSetting = NullSettings.Null)] // is contentVersionDto.VersionDate for the published version - //public DateTime? PublishDate { get; set; } - - //[Column("publishUserId")] - //[NullSetting(NullSetting = NullSettings.Null)] // is contentVersionDto.UserId for the published version - //public int? PublishUserId { get; set; } - - //[Column("publishName")] - //[NullSetting(NullSetting = NullSettings.Null)] // is contentVersionDto.Text for the published version - //public string PublishName { get; set; } - - //[Column("publishTemplateId")] - //[NullSetting(NullSetting = NullSettings.Null)] // is documentVersionDto.TemplateId for the published version - //public int? PublishTemplateId { get; set; } - - [ResultColumn] - [Reference(ReferenceType.OneToOne, ReferenceMemberName = "NodeId")] - public ContentDto ContentDto { get; set; } = null!; - - // although a content has many content versions, - // they can only be loaded one by one (as several content), - // so this here is a OneToOne reference - [ResultColumn] - [Reference(ReferenceType.OneToOne)] - public DocumentVersionDto DocumentVersionDto { get; set; } = null!; - - // same - [ResultColumn] - [Reference(ReferenceType.OneToOne)] - public DocumentVersionDto PublishedVersionDto { get; set; } = null!; - } +[TableName(TableName)] +[PrimaryKey("nodeId", AutoIncrement = false)] +[ExplicitColumns] +public class DocumentDto +{ + private const string TableName = Constants.DatabaseSchema.Tables.Document; + + [Column("nodeId")] + [PrimaryKeyColumn(AutoIncrement = false)] + [ForeignKey(typeof(ContentDto))] + public int NodeId { get; set; } + + [Column("published")] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Published")] + public bool Published { get; set; } + + [Column("edited")] public bool Edited { get; set; } + + //[Column("publishDate")] + //[NullSetting(NullSetting = NullSettings.Null)] // is contentVersionDto.VersionDate for the published version + //public DateTime? PublishDate { get; set; } + + //[Column("publishUserId")] + //[NullSetting(NullSetting = NullSettings.Null)] // is contentVersionDto.UserId for the published version + //public int? PublishUserId { get; set; } + + //[Column("publishName")] + //[NullSetting(NullSetting = NullSettings.Null)] // is contentVersionDto.Text for the published version + //public string PublishName { get; set; } + + //[Column("publishTemplateId")] + //[NullSetting(NullSetting = NullSettings.Null)] // is documentVersionDto.TemplateId for the published version + //public int? PublishTemplateId { get; set; } + + [ResultColumn] + [Reference(ReferenceType.OneToOne, ReferenceMemberName = "NodeId")] + public ContentDto ContentDto { get; set; } = null!; + + // although a content has many content versions, + // they can only be loaded one by one (as several content), + // so this here is a OneToOne reference + [ResultColumn] + [Reference(ReferenceType.OneToOne)] + public DocumentVersionDto DocumentVersionDto { get; set; } = null!; + + // same + [ResultColumn] + [Reference(ReferenceType.OneToOne)] + public DocumentVersionDto PublishedVersionDto { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentPublishedReadOnlyDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentPublishedReadOnlyDto.cs index a6fcd6b3195e..3613227e5e42 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentPublishedReadOnlyDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentPublishedReadOnlyDto.cs @@ -1,26 +1,20 @@ -using System; -using NPoco; +using NPoco; +using Umbraco.Cms.Core; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.Document)] +[PrimaryKey("versionId", AutoIncrement = false)] +[ExplicitColumns] +internal class DocumentPublishedReadOnlyDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.Document)] - [PrimaryKey("versionId", AutoIncrement = false)] - [ExplicitColumns] - internal class DocumentPublishedReadOnlyDto - { - [Column("nodeId")] - public int NodeId { get; set; } + [Column("nodeId")] public int NodeId { get; set; } - [Column("published")] - public bool Published { get; set; } + [Column("published")] public bool Published { get; set; } - [Column("versionId")] - public Guid VersionId { get; set; } + [Column("versionId")] public Guid VersionId { get; set; } - [Column("newest")] - public bool Newest { get; set; } + [Column("newest")] public bool Newest { get; set; } - [Column("updateDate")] - public DateTime VersionDate { get; set; } - } + [Column("updateDate")] public DateTime VersionDate { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentVersionDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentVersionDto.cs index 2d06129ba602..a4e4ae156022 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentVersionDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentVersionDto.cs @@ -1,30 +1,29 @@ using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id", AutoIncrement = false)] +[ExplicitColumns] +public class DocumentVersionDto { - [TableName(TableName)] - [PrimaryKey("id", AutoIncrement = false)] - [ExplicitColumns] - public class DocumentVersionDto - { - private const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.DocumentVersion; + private const string TableName = Constants.DatabaseSchema.Tables.DocumentVersion; - [Column("id")] - [PrimaryKeyColumn(AutoIncrement = false)] - [ForeignKey(typeof(ContentVersionDto))] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn(AutoIncrement = false)] + [ForeignKey(typeof(ContentVersionDto))] + public int Id { get; set; } - [Column("templateId")] - [NullSetting(NullSetting = NullSettings.Null)] - [ForeignKey(typeof(TemplateDto), Column = "nodeId")] - public int? TemplateId { get; set; } + [Column("templateId")] + [NullSetting(NullSetting = NullSettings.Null)] + [ForeignKey(typeof(TemplateDto), Column = "nodeId")] + public int? TemplateId { get; set; } - [Column("published")] - public bool Published { get; set; } + [Column("published")] public bool Published { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne)] - public ContentVersionDto ContentVersionDto { get; set; } = null!; - } + [ResultColumn] + [Reference(ReferenceType.OneToOne)] + public ContentVersionDto ContentVersionDto { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/DomainDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/DomainDto.cs index 60f063503563..02fff25f25d3 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/DomainDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/DomainDto.cs @@ -1,33 +1,30 @@ using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.Domain)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class DomainDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.Domain)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class DomainDto - { - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + [Column("id")] [PrimaryKeyColumn] public int Id { get; set; } - [Column("domainDefaultLanguage")] - [NullSetting(NullSetting = NullSettings.Null)] - public int? DefaultLanguage { get; set; } + [Column("domainDefaultLanguage")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? DefaultLanguage { get; set; } - [Column("domainRootStructureID")] - [NullSetting(NullSetting = NullSettings.Null)] - [ForeignKey(typeof(NodeDto))] - public int? RootStructureId { get; set; } + [Column("domainRootStructureID")] + [NullSetting(NullSetting = NullSettings.Null)] + [ForeignKey(typeof(NodeDto))] + public int? RootStructureId { get; set; } - [Column("domainName")] - public string DomainName { get; set; } = null!; + [Column("domainName")] public string DomainName { get; set; } = null!; - /// - /// Used for a result on the query to get the associated language for a domain if there is one - /// - [ResultColumn("languageISOCode")] - public string IsoCode { get; set; } = null!; - } + /// + /// Used for a result on the query to get the associated language for a domain if there is one + /// + [ResultColumn("languageISOCode")] + public string IsoCode { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs index b6eae7f2346a..cb039352266c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs @@ -1,57 +1,57 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[ExplicitColumns] +[PrimaryKey("Id")] +internal class ExternalLoginDto { - [TableName(TableName)] - [ExplicitColumns] - [PrimaryKey("Id")] - internal class ExternalLoginDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.ExternalLogin; - - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } - - [Obsolete("This only exists to ensure you can upgrade using external logins from umbraco version where this was used to the new where it is not used")] - [ResultColumn("userId")] - public int? UserId { get; set; } - - [Column("userOrMemberKey")] - [Index(IndexTypes.NonClustered)] - public Guid UserOrMemberKey { get; set; } - - /// - /// Used to store the name of the provider (i.e. Facebook, Google) - /// - [Column("loginProvider")] - [Length(400)] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.UniqueNonClustered, ForColumns = "loginProvider,userOrMemberKey", Name = "IX_" + TableName + "_LoginProvider")] - public string LoginProvider { get; set; } = null!; - - /// - /// Stores the key the provider uses to lookup the login - /// - [Column("providerKey")] - [Length(4000)] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.NonClustered, ForColumns = "loginProvider,providerKey", Name = "IX_" + TableName + "_ProviderKey")] - public string ProviderKey { get; set; } = null!; - - [Column("createDate")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime CreateDate { get; set; } - - /// - /// Used to store any arbitrary data for the user and external provider - like user tokens returned from the provider - /// - [Column("userData")] - [NullSetting(NullSetting = NullSettings.Null)] - [SpecialDbType(SpecialDbTypes.NTEXT)] - public string? UserData { get; set; } - } + public const string TableName = Constants.DatabaseSchema.Tables.ExternalLogin; + + [Column("id")] [PrimaryKeyColumn] public int Id { get; set; } + + [Obsolete( + "This only exists to ensure you can upgrade using external logins from umbraco version where this was used to the new where it is not used")] + [ResultColumn("userId")] + public int? UserId { get; set; } + + [Column("userOrMemberKey")] + [Index(IndexTypes.NonClustered)] + public Guid UserOrMemberKey { get; set; } + + /// + /// Used to store the name of the provider (i.e. Facebook, Google) + /// + [Column("loginProvider")] + [Length(400)] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "loginProvider,userOrMemberKey", + Name = "IX_" + TableName + "_LoginProvider")] + public string LoginProvider { get; set; } = null!; + + /// + /// Stores the key the provider uses to lookup the login + /// + [Column("providerKey")] + [Length(4000)] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.NonClustered, ForColumns = "loginProvider,providerKey", + Name = "IX_" + TableName + "_ProviderKey")] + public string ProviderKey { get; set; } = null!; + + [Column("createDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime CreateDate { get; set; } + + /// + /// Used to store any arbitrary data for the user and external provider - like user tokens returned from the provider + /// + [Column("userData")] + [NullSetting(NullSetting = NullSettings.Null)] + [SpecialDbType(SpecialDbTypes.NTEXT)] + public string? UserData { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginTokenDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginTokenDto.cs index cd16703bdc5a..3282dc1b7014 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginTokenDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginTokenDto.cs @@ -1,42 +1,39 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[ExplicitColumns] +[PrimaryKey("Id")] +internal class ExternalLoginTokenDto { - [TableName(TableName)] - [ExplicitColumns] - [PrimaryKey("Id")] - internal class ExternalLoginTokenDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.ExternalLoginToken; - - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } - - [Column("externalLoginId")] - [ForeignKey(typeof(ExternalLoginDto), Column = "id")] - public int ExternalLoginId { get; set; } - - [Column("name")] - [Length(255)] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.UniqueNonClustered, ForColumns = "externalLoginId,name", Name = "IX_" + TableName + "_Name")] - public string Name { get; set; } = null!; - - [Column("value")] - [SpecialDbType(SpecialDbTypes.NVARCHARMAX)] - [NullSetting(NullSetting = NullSettings.NotNull)] - public string Value { get; set; } = null!; - - [Column("createDate")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime CreateDate { get; set; } - - [ResultColumn] - [Reference(ReferenceType.OneToOne, ColumnName = "ExternalLoginId")] - public ExternalLoginDto ExternalLoginDto { get; set; } = null!; - } + public const string TableName = Constants.DatabaseSchema.Tables.ExternalLoginToken; + + [Column("id")] [PrimaryKeyColumn] public int Id { get; set; } + + [Column("externalLoginId")] + [ForeignKey(typeof(ExternalLoginDto), Column = "id")] + public int ExternalLoginId { get; set; } + + [Column("name")] + [Length(255)] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "externalLoginId,name", Name = "IX_" + TableName + "_Name")] + public string Name { get; set; } = null!; + + [Column("value")] + [SpecialDbType(SpecialDbTypes.NVARCHARMAX)] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string Value { get; set; } = null!; + + [Column("createDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime CreateDate { get; set; } + + [ResultColumn] + [Reference(ReferenceType.OneToOne, ColumnName = "ExternalLoginId")] + public ExternalLoginDto ExternalLoginDto { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/KeyValueDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/KeyValueDto.cs index 654d3071b0b9..197efb49b5ab 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/KeyValueDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/KeyValueDto.cs @@ -1,26 +1,25 @@ -using System; -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.KeyValue)] +[PrimaryKey("key", AutoIncrement = false)] +[ExplicitColumns] +internal class KeyValueDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.KeyValue)] - [PrimaryKey("key", AutoIncrement = false)] - [ExplicitColumns] - internal class KeyValueDto - { - [Column("key")] - [Length(256)] - [PrimaryKeyColumn(AutoIncrement = false, Clustered = true)] - public string Key { get; set; } = null!; + [Column("key")] + [Length(256)] + [PrimaryKeyColumn(AutoIncrement = false, Clustered = true)] + public string Key { get; set; } = null!; - [Column("value")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Value { get; set; } + [Column("value")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Value { get; set; } - [Column("updated")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime UpdateDate { get; set; } - } + [Column("updated")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime UpdateDate { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/LanguageDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/LanguageDto.cs index e5b25fa16614..22d09597895b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/LanguageDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/LanguageDto.cs @@ -1,60 +1,60 @@ using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class LanguageDto { - [TableName(TableName)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class LanguageDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.Language; - - /// - /// Gets or sets the identifier of the language. - /// - [Column("id")] - [PrimaryKeyColumn(IdentitySeed = 2)] - public short Id { get; set; } - - /// - /// Gets or sets the ISO code of the language. - /// - [Column("languageISOCode")] - [Index(IndexTypes.UniqueNonClustered)] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(14)] - public string? IsoCode { get; set; } - - /// - /// Gets or sets the culture name of the language. - /// - [Column("languageCultureName")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(100)] - public string? CultureName { get; set; } - - /// - /// Gets or sets a value indicating whether the language is the default language. - /// - [Column("isDefaultVariantLang")] - [Constraint(Default = "0")] - public bool IsDefault { get; set; } - - /// - /// Gets or sets a value indicating whether the language is mandatory. - /// - [Column("mandatory")] - [Constraint(Default = "0")] - public bool IsMandatory { get; set; } - - /// - /// Gets or sets the identifier of a fallback language. - /// - [Column("fallbackLanguageId")] - [ForeignKey(typeof(LanguageDto), Column = "id")] - [Index(IndexTypes.NonClustered)] - [NullSetting(NullSetting = NullSettings.Null)] - public int? FallbackLanguageId { get; set; } - } + public const string TableName = Constants.DatabaseSchema.Tables.Language; + + /// + /// Gets or sets the identifier of the language. + /// + [Column("id")] + [PrimaryKeyColumn(IdentitySeed = 2)] + public short Id { get; set; } + + /// + /// Gets or sets the ISO code of the language. + /// + [Column("languageISOCode")] + [Index(IndexTypes.UniqueNonClustered)] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(14)] + public string? IsoCode { get; set; } + + /// + /// Gets or sets the culture name of the language. + /// + [Column("languageCultureName")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(100)] + public string? CultureName { get; set; } + + /// + /// Gets or sets a value indicating whether the language is the default language. + /// + [Column("isDefaultVariantLang")] + [Constraint(Default = "0")] + public bool IsDefault { get; set; } + + /// + /// Gets or sets a value indicating whether the language is mandatory. + /// + [Column("mandatory")] + [Constraint(Default = "0")] + public bool IsMandatory { get; set; } + + /// + /// Gets or sets the identifier of a fallback language. + /// + [Column("fallbackLanguageId")] + [ForeignKey(typeof(LanguageDto), Column = "id")] + [Index(IndexTypes.NonClustered)] + [NullSetting(NullSetting = NullSettings.Null)] + public int? FallbackLanguageId { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/LanguageTextDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/LanguageTextDto.cs index 3d08c9de982c..650ffd2b488f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/LanguageTextDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/LanguageTextDto.cs @@ -1,31 +1,26 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("pk")] +[ExplicitColumns] +public class LanguageTextDto { - [TableName(TableName)] - [PrimaryKey("pk")] - [ExplicitColumns] - public class LanguageTextDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.DictionaryValue; + public const string TableName = Constants.DatabaseSchema.Tables.DictionaryValue; - [Column("pk")] - [PrimaryKeyColumn] - public int PrimaryKey { get; set; } + [Column("pk")] [PrimaryKeyColumn] public int PrimaryKey { get; set; } - [Column("languageId")] - [ForeignKey(typeof(LanguageDto), Column = "id")] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_languageId", ForColumns = "languageId,UniqueId")] - public int LanguageId { get; set; } + [Column("languageId")] + [ForeignKey(typeof(LanguageDto), Column = "id")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_languageId", ForColumns = "languageId,UniqueId")] + public int LanguageId { get; set; } - [Column("UniqueId")] - [ForeignKey(typeof(DictionaryDto), Column = "id")] - public Guid UniqueId { get; set; } + [Column("UniqueId")] + [ForeignKey(typeof(DictionaryDto), Column = "id")] + public Guid UniqueId { get; set; } - [Column("value")] - [Length(1000)] - public string Value { get; set; } = null!; - } + [Column("value")] [Length(1000)] public string Value { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/LockDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/LockDto.cs index 21fe45c22be4..60296fffd7d4 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/LockDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/LockDto.cs @@ -1,24 +1,24 @@ using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.Lock)] +[PrimaryKey("id", AutoIncrement = false)] +[ExplicitColumns] +internal class LockDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.Lock)] - [PrimaryKey("id", AutoIncrement = false)] - [ExplicitColumns] - internal class LockDto - { - [Column("id")] - [PrimaryKeyColumn(Name = "PK_umbracoLock", AutoIncrement = false)] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn(Name = "PK_umbracoLock", AutoIncrement = false)] + public int Id { get; set; } - [Column("value")] - [NullSetting(NullSetting = NullSettings.NotNull)] - public int Value { get; set; } = 1; + [Column("value")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public int Value { get; set; } = 1; - [Column("name")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Length(64)] - public string Name { get; set; } = null!; - } + [Column("name")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Length(64)] + public string Name { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/LogDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/LogDto.cs index 3b2009f3dab4..9c7171b558ee 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/LogDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/LogDto.cs @@ -1,61 +1,56 @@ -using System; -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class LogDto { - [TableName(TableName)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class LogDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.Log; - - private int? _userId; - - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } - - [Column("userId")] - [ForeignKey(typeof(UserDto))] - [NullSetting(NullSetting = NullSettings.Null)] - public int? UserId { get => _userId == 0 ? null : _userId; set => _userId = value; } //return null if zero - - [Column("NodeId")] - [Index(IndexTypes.NonClustered, Name = "IX_umbracoLog")] - public int NodeId { get; set; } - - /// - /// This is the entity type associated with the log - /// - [Column("entityType")] - [Length(50)] - [NullSetting(NullSetting = NullSettings.Null)] - public string? EntityType { get; set; } - - // TODO: Should we have an index on this since we allow searching on it? - [Column("Datestamp")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime Datestamp { get; set; } - - // TODO: Should we have an index on this since we allow searching on it? - [Column("logHeader")] - [Length(50)] - public string Header { get; set; } = null!; - - [Column("logComment")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(4000)] - public string? Comment { get; set; } - - /// - /// Used to store additional data parameters for the log - /// - [Column("parameters")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(500)] - public string? Parameters { get; set; } - } + public const string TableName = Constants.DatabaseSchema.Tables.Log; + + private int? _userId; + + [Column("id")] [PrimaryKeyColumn] public int Id { get; set; } + + [Column("userId")] + [ForeignKey(typeof(UserDto))] + [NullSetting(NullSetting = NullSettings.Null)] + public int? UserId { get => _userId == 0 ? null : _userId; set => _userId = value; } //return null if zero + + [Column("NodeId")] + [Index(IndexTypes.NonClustered, Name = "IX_umbracoLog")] + public int NodeId { get; set; } + + /// + /// This is the entity type associated with the log + /// + [Column("entityType")] + [Length(50)] + [NullSetting(NullSetting = NullSettings.Null)] + public string? EntityType { get; set; } + + // TODO: Should we have an index on this since we allow searching on it? + [Column("Datestamp")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime Datestamp { get; set; } + + // TODO: Should we have an index on this since we allow searching on it? + [Column("logHeader")] [Length(50)] public string Header { get; set; } = null!; + + [Column("logComment")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(4000)] + public string? Comment { get; set; } + + /// + /// Used to store additional data parameters for the log + /// + [Column("parameters")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(500)] + public string? Parameters { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/LogViewerQueryDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/LogViewerQueryDto.cs index 66b4a1902c86..9a430a05e304 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/LogViewerQueryDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/LogViewerQueryDto.cs @@ -1,22 +1,19 @@ using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.LogViewerQuery)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class LogViewerQueryDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.LogViewerQuery)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class LogViewerQueryDto - { - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + [Column("id")] [PrimaryKeyColumn] public int Id { get; set; } - [Column("name")] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_LogViewerQuery_name")] - public string? Name { get; set; } + [Column("name")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_LogViewerQuery_name")] + public string? Name { get; set; } - [Column("query")] - public string? Query { get; set; } - } + [Column("query")] public string? Query { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/MacroDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/MacroDto.cs index 3f9dae2744cf..0516d4cd1c1e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/MacroDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/MacroDto.cs @@ -1,61 +1,57 @@ -using System; -using System.Collections.Generic; -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.Macro)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class MacroDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.Macro)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class MacroDto - { - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } - - [Column("uniqueId")] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsMacro_UniqueId")] - public Guid UniqueId { get; set; } - - [Column("macroUseInEditor")] - [Constraint(Default = "0")] - public bool UseInEditor { get; set; } - - [Column("macroRefreshRate")] - [Constraint(Default = "0")] - public int RefreshRate { get; set; } - - [Column("macroAlias")] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsMacroPropertyAlias")] - public string Alias { get; set; } = string.Empty; - - [Column("macroName")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Name { get; set; } - - [Column("macroCacheByPage")] - [Constraint(Default = "1")] - public bool CacheByPage { get; set; } - - [Column("macroCachePersonalized")] - [Constraint(Default = "0")] - public bool CachePersonalized { get; set; } - - [Column("macroDontRender")] - [Constraint(Default = "0")] - public bool DontRender { get; set; } - - [Column("macroSource")] - [NullSetting(NullSetting = NullSettings.NotNull)] - public string MacroSource { get; set; } = null!; - - [Column("macroType")] - [NullSetting(NullSetting = NullSettings.NotNull)] - public int MacroType { get; set; } - - [ResultColumn] - [Reference(ReferenceType.Many, ReferenceMemberName = "Macro")] - public List? MacroPropertyDtos { get; set; } - } + [Column("id")] [PrimaryKeyColumn] public int Id { get; set; } + + [Column("uniqueId")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsMacro_UniqueId")] + public Guid UniqueId { get; set; } + + [Column("macroUseInEditor")] + [Constraint(Default = "0")] + public bool UseInEditor { get; set; } + + [Column("macroRefreshRate")] + [Constraint(Default = "0")] + public int RefreshRate { get; set; } + + [Column("macroAlias")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsMacroPropertyAlias")] + public string Alias { get; set; } = string.Empty; + + [Column("macroName")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Name { get; set; } + + [Column("macroCacheByPage")] + [Constraint(Default = "1")] + public bool CacheByPage { get; set; } + + [Column("macroCachePersonalized")] + [Constraint(Default = "0")] + public bool CachePersonalized { get; set; } + + [Column("macroDontRender")] + [Constraint(Default = "0")] + public bool DontRender { get; set; } + + [Column("macroSource")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string MacroSource { get; set; } = null!; + + [Column("macroType")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public int MacroType { get; set; } + + [ResultColumn] + [Reference(ReferenceType.Many, ReferenceMemberName = "Macro")] + public List? MacroPropertyDtos { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/MacroPropertyDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/MacroPropertyDto.cs index 62e64e77a9df..0271d53012f4 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/MacroPropertyDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/MacroPropertyDto.cs @@ -1,40 +1,35 @@ -using System; -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.MacroProperty)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class MacroPropertyDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.MacroProperty)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class MacroPropertyDto - { - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } - - // important to use column name != cmsMacro.uniqueId (fix in v8) - [Column("uniquePropertyId")] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsMacroProperty_UniquePropertyId")] - public Guid UniqueId { get; set; } - - [Column("editorAlias")] - public string EditorAlias { get; set; } = null!; - - [Column("macro")] - [ForeignKey(typeof(MacroDto))] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsMacroProperty_Alias", ForColumns = "macro, macroPropertyAlias")] - public int Macro { get; set; } - - [Column("macroPropertySortOrder")] - [Constraint(Default = "0")] - public byte SortOrder { get; set; } - - [Column("macroPropertyAlias")] - [Length(50)] - public string Alias { get; set; } = null!; - - [Column("macroPropertyName")] - public string? Name { get; set; } - } + [Column("id")] [PrimaryKeyColumn] public int Id { get; set; } + + // important to use column name != cmsMacro.uniqueId (fix in v8) + [Column("uniquePropertyId")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsMacroProperty_UniquePropertyId")] + public Guid UniqueId { get; set; } + + [Column("editorAlias")] public string EditorAlias { get; set; } = null!; + + [Column("macro")] + [ForeignKey(typeof(MacroDto))] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsMacroProperty_Alias", ForColumns = "macro, macroPropertyAlias")] + public int Macro { get; set; } + + [Column("macroPropertySortOrder")] + [Constraint(Default = "0")] + public byte SortOrder { get; set; } + + [Column("macroPropertyAlias")] + [Length(50)] + public string Alias { get; set; } = null!; + + [Column("macroPropertyName")] public string? Name { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/MediaDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/MediaDto.cs index 374f2437ff6c..744d05730e88 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/MediaDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/MediaDto.cs @@ -1,21 +1,19 @@ using NPoco; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos -{ - // this is a special Dto that does not have a corresponding table - // and is only used in our code to represent a media item, similar - // to document items. +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; +// this is a special Dto that does not have a corresponding table +// and is only used in our code to represent a media item, similar +// to document items. - internal class MediaDto - { - public int NodeId { get; set; } +internal class MediaDto +{ + public int NodeId { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne, ReferenceMemberName = "NodeId")] - public ContentDto ContentDto { get; set; } = null!; + [ResultColumn] + [Reference(ReferenceType.OneToOne, ReferenceMemberName = "NodeId")] + public ContentDto ContentDto { get; set; } = null!; - [ResultColumn] - [Reference(ReferenceType.OneToOne)] - public MediaVersionDto MediaVersionDto { get; set; } = null!; - } + [ResultColumn] + [Reference(ReferenceType.OneToOne)] + public MediaVersionDto MediaVersionDto { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/MediaVersionDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/MediaVersionDto.cs index dabdb14ca7d4..31de48394d83 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/MediaVersionDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/MediaVersionDto.cs @@ -1,27 +1,27 @@ using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id", AutoIncrement = false)] +[ExplicitColumns] +internal class MediaVersionDto { - [TableName(TableName)] - [PrimaryKey("id", AutoIncrement = false)] - [ExplicitColumns] - internal class MediaVersionDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion; + public const string TableName = Constants.DatabaseSchema.Tables.MediaVersion; - [Column("id")] - [PrimaryKeyColumn(AutoIncrement = false)] - [ForeignKey(typeof(ContentVersionDto))] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName, ForColumns = "id, path")] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn(AutoIncrement = false)] + [ForeignKey(typeof(ContentVersionDto))] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName, ForColumns = "id, path")] + public int Id { get; set; } - [Column("path")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Path { get; set; } + [Column("path")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Path { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne)] - public ContentVersionDto ContentVersionDto { get; set; } = null!; - } + [ResultColumn] + [Reference(ReferenceType.OneToOne)] + public ContentVersionDto ContentVersionDto { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/Member2MemberGroupDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/Member2MemberGroupDto.cs index a32257a087b1..4679328f6769 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/Member2MemberGroupDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/Member2MemberGroupDto.cs @@ -1,20 +1,20 @@ using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.Member2MemberGroup)] +[PrimaryKey("Member", AutoIncrement = false)] +[ExplicitColumns] +internal class Member2MemberGroupDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.Member2MemberGroup)] - [PrimaryKey("Member", AutoIncrement = false)] - [ExplicitColumns] - internal class Member2MemberGroupDto - { - [Column("Member")] - [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_cmsMember2MemberGroup", OnColumns = "Member, MemberGroup")] - [ForeignKey(typeof(MemberDto))] - public int Member { get; set; } + [Column("Member")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_cmsMember2MemberGroup", OnColumns = "Member, MemberGroup")] + [ForeignKey(typeof(MemberDto))] + public int Member { get; set; } - [Column("MemberGroup")] - [ForeignKey(typeof(NodeDto))] - public int MemberGroup { get; set; } - } + [Column("MemberGroup")] + [ForeignKey(typeof(NodeDto))] + public int MemberGroup { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/MemberDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/MemberDto.cs index 6c24bad7c4a2..77b500eef5a0 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/MemberDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/MemberDto.cs @@ -1,85 +1,84 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("nodeId", AutoIncrement = false)] +[ExplicitColumns] +internal class MemberDto { - [TableName(TableName)] - [PrimaryKey("nodeId", AutoIncrement = false)] - [ExplicitColumns] - internal class MemberDto - { - private const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.Member; - - [Column("nodeId")] - [PrimaryKeyColumn(AutoIncrement = false)] - [ForeignKey(typeof(ContentDto))] - public int NodeId { get; set; } - - [Column("Email")] - [Length(1000)] - [Constraint(Default = "''")] - public string Email { get; set; } = null!; - - [Column("LoginName")] - [Length(1000)] - [Constraint(Default = "''")] - [Index(IndexTypes.NonClustered, Name = "IX_cmsMember_LoginName")] - public string LoginName { get; set; } = null!; - - [Column("Password")] - [Length(1000)] - [Constraint(Default = "''")] - public string? Password { get; set; } - - /// - /// This will represent a JSON structure of how the password has been created (i.e hash algorithm, iterations) - /// - [Column("passwordConfig")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(500)] - public string? PasswordConfig { get; set; } - - [Column("securityStampToken")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(255)] - public string? SecurityStampToken { get; set; } - - [Column("emailConfirmedDate")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? EmailConfirmedDate { get; set; } - - [Column("failedPasswordAttempts")] - [NullSetting(NullSetting = NullSettings.Null)] - public int? FailedPasswordAttempts { get; set; } - - [Column("isLockedOut")] - [Constraint(Default = 0)] - [NullSetting(NullSetting = NullSettings.Null)] - public bool IsLockedOut { get; set; } - - [Column("isApproved")] - [Constraint(Default = 1)] - public bool IsApproved { get; set; } - - [Column("lastLoginDate")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? LastLoginDate { get; set; } - - [Column("lastLockoutDate")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? LastLockoutDate { get; set; } - - [Column("lastPasswordChangeDate")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? LastPasswordChangeDate { get; set; } - - [ResultColumn] - [Reference(ReferenceType.OneToOne, ReferenceMemberName = "NodeId")] - public ContentDto ContentDto { get; set; } = null!; - - [ResultColumn] - [Reference(ReferenceType.OneToOne, ReferenceMemberName = "NodeId")] - public ContentVersionDto ContentVersionDto { get; set; } = null!; - } + private const string TableName = Constants.DatabaseSchema.Tables.Member; + + [Column("nodeId")] + [PrimaryKeyColumn(AutoIncrement = false)] + [ForeignKey(typeof(ContentDto))] + public int NodeId { get; set; } + + [Column("Email")] + [Length(1000)] + [Constraint(Default = "''")] + public string Email { get; set; } = null!; + + [Column("LoginName")] + [Length(1000)] + [Constraint(Default = "''")] + [Index(IndexTypes.NonClustered, Name = "IX_cmsMember_LoginName")] + public string LoginName { get; set; } = null!; + + [Column("Password")] + [Length(1000)] + [Constraint(Default = "''")] + public string? Password { get; set; } + + /// + /// This will represent a JSON structure of how the password has been created (i.e hash algorithm, iterations) + /// + [Column("passwordConfig")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(500)] + public string? PasswordConfig { get; set; } + + [Column("securityStampToken")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(255)] + public string? SecurityStampToken { get; set; } + + [Column("emailConfirmedDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? EmailConfirmedDate { get; set; } + + [Column("failedPasswordAttempts")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? FailedPasswordAttempts { get; set; } + + [Column("isLockedOut")] + [Constraint(Default = 0)] + [NullSetting(NullSetting = NullSettings.Null)] + public bool IsLockedOut { get; set; } + + [Column("isApproved")] + [Constraint(Default = 1)] + public bool IsApproved { get; set; } + + [Column("lastLoginDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? LastLoginDate { get; set; } + + [Column("lastLockoutDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? LastLockoutDate { get; set; } + + [Column("lastPasswordChangeDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? LastPasswordChangeDate { get; set; } + + [ResultColumn] + [Reference(ReferenceType.OneToOne, ReferenceMemberName = "NodeId")] + public ContentDto ContentDto { get; set; } = null!; + + [ResultColumn] + [Reference(ReferenceType.OneToOne, ReferenceMemberName = "NodeId")] + public ContentVersionDto ContentVersionDto { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/MemberPropertyTypeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/MemberPropertyTypeDto.cs index 9e9b97daf389..ef88d332eb9a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/MemberPropertyTypeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/MemberPropertyTypeDto.cs @@ -1,35 +1,32 @@ using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.MemberPropertyType)] +[PrimaryKey("pk")] +[ExplicitColumns] +internal class MemberPropertyTypeDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.MemberPropertyType)] - [PrimaryKey("pk")] - [ExplicitColumns] - internal class MemberPropertyTypeDto - { - [Column("pk")] - [PrimaryKeyColumn] - public int PrimaryKey { get; set; } + [Column("pk")] [PrimaryKeyColumn] public int PrimaryKey { get; set; } - [Column("NodeId")] - [ForeignKey(typeof(NodeDto))] - [ForeignKey(typeof(ContentTypeDto), Column = "nodeId")] - public int NodeId { get; set; } + [Column("NodeId")] + [ForeignKey(typeof(NodeDto))] + [ForeignKey(typeof(ContentTypeDto), Column = "nodeId")] + public int NodeId { get; set; } - [Column("propertytypeId")] - public int PropertyTypeId { get; set; } + [Column("propertytypeId")] public int PropertyTypeId { get; set; } - [Column("memberCanEdit")] - [Constraint(Default = "0")] - public bool CanEdit { get; set; } + [Column("memberCanEdit")] + [Constraint(Default = "0")] + public bool CanEdit { get; set; } - [Column("viewOnProfile")] - [Constraint(Default = "0")] - public bool ViewOnProfile { get; set; } + [Column("viewOnProfile")] + [Constraint(Default = "0")] + public bool ViewOnProfile { get; set; } - [Column("isSensitive")] - [Constraint(Default = "0")] - public bool IsSensitive { get; set; } - } + [Column("isSensitive")] + [Constraint(Default = "0")] + public bool IsSensitive { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/NodeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/NodeDto.cs index 621aba121ac1..622013817e94 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/NodeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/NodeDto.cs @@ -1,68 +1,70 @@ -using System; -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id")] +[ExplicitColumns] +public class NodeDto { - [TableName(TableName)] - [PrimaryKey("id")] - [ExplicitColumns] - public class NodeDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.Node; - public const int NodeIdSeed = 1060; - private int? _userId; + public const string TableName = Constants.DatabaseSchema.Tables.Node; + public const int NodeIdSeed = 1060; + private int? _userId; - [Column("id")] - [PrimaryKeyColumn(IdentitySeed = NodeIdSeed)] - public int NodeId { get; set; } + [Column("id")] + [PrimaryKeyColumn(IdentitySeed = NodeIdSeed)] + public int NodeId { get; set; } - [Column("uniqueId")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_UniqueId", IncludeColumns = "parentId,level,path,sortOrder,trashed,nodeUser,text,createDate")] - [Constraint(Default = SystemMethods.NewGuid)] - public Guid UniqueId { get; set; } + [Column("uniqueId")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_UniqueId", + IncludeColumns = "parentId,level,path,sortOrder,trashed,nodeUser,text,createDate")] + [Constraint(Default = SystemMethods.NewGuid)] + public Guid UniqueId { get; set; } - [Column("parentId")] - [ForeignKey(typeof(NodeDto))] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_ParentId")] - public int ParentId { get; set; } + [Column("parentId")] + [ForeignKey(typeof(NodeDto))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_ParentId")] + public int ParentId { get; set; } - // NOTE: This index is primarily for the nucache data lookup, see https://github.com/umbraco/Umbraco-CMS/pull/8365#issuecomment-673404177 - [Column("level")] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Level", ForColumns = "level,parentId,sortOrder,nodeObjectType,trashed", IncludeColumns = "nodeUser,path,uniqueId,createDate")] - public short Level { get; set; } + // NOTE: This index is primarily for the nucache data lookup, see https://github.com/umbraco/Umbraco-CMS/pull/8365#issuecomment-673404177 + [Column("level")] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Level", + ForColumns = "level,parentId,sortOrder,nodeObjectType,trashed", + IncludeColumns = "nodeUser,path,uniqueId,createDate")] + public short Level { get; set; } - [Column("path")] - [Length(150)] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Path")] - public string Path { get; set; } = null!; + [Column("path")] + [Length(150)] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Path")] + public string Path { get; set; } = null!; - [Column("sortOrder")] - public int SortOrder { get; set; } + [Column("sortOrder")] public int SortOrder { get; set; } - [Column("trashed")] - [Constraint(Default = "0")] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Trashed")] - public bool Trashed { get; set; } + [Column("trashed")] + [Constraint(Default = "0")] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Trashed")] + public bool Trashed { get; set; } - [Column("nodeUser")] // TODO: db rename to 'createUserId' - [ForeignKey(typeof(UserDto))] - [NullSetting(NullSetting = NullSettings.Null)] - public int? UserId { get => _userId == 0 ? null : _userId; set => _userId = value; } //return null if zero + [Column("nodeUser")] // TODO: db rename to 'createUserId' + [ForeignKey(typeof(UserDto))] + [NullSetting(NullSetting = NullSettings.Null)] + public int? UserId { get => _userId == 0 ? null : _userId; set => _userId = value; } //return null if zero - [Column("text")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Text { get; set; } + [Column("text")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Text { get; set; } - [Column("nodeObjectType")] // TODO: db rename to 'objectType' - [NullSetting(NullSetting = NullSettings.Null)] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_ObjectType", ForColumns = "nodeObjectType,trashed", IncludeColumns = "uniqueId,parentId,level,path,sortOrder,nodeUser,text,createDate")] - public Guid? NodeObjectType { get; set; } + [Column("nodeObjectType")] // TODO: db rename to 'objectType' + [NullSetting(NullSetting = NullSettings.Null)] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_ObjectType", ForColumns = "nodeObjectType,trashed", + IncludeColumns = "uniqueId,parentId,level,path,sortOrder,nodeUser,text,createDate")] + public Guid? NodeObjectType { get; set; } - [Column("createDate")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime CreateDate { get; set; } - } + [Column("createDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime CreateDate { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyDataDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyDataDto.cs index bd0c63a4124f..e3ec0bb5444d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyDataDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyDataDto.cs @@ -1,136 +1,134 @@ -using System; -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class PropertyDataDto { - [TableName(TableName)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class PropertyDataDto + public const string TableName = Constants.DatabaseSchema.Tables.PropertyData; + public const int VarcharLength = 512; + public const int SegmentLength = 256; + + private decimal? _decimalValue; + + // pk, not used at the moment (never updating) + [Column("id")] [PrimaryKeyColumn] public int Id { get; set; } + + [Column("versionId")] + [ForeignKey(typeof(ContentVersionDto))] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_VersionId", + ForColumns = "versionId,propertyTypeId,languageId,segment")] + public int VersionId { get; set; } + + [Column("propertyTypeId")] + [ForeignKey(typeof(PropertyTypeDto))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_PropertyTypeId")] + public int PropertyTypeId { get; set; } + + [Column("languageId")] + [ForeignKey(typeof(LanguageDto))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? LanguageId { get; set; } + + [Column("segment")] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Segment")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(SegmentLength)] + public string? Segment { get; set; } + + [Column("intValue")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? IntegerValue { get; set; } + + [Column("decimalValue")] + [NullSetting(NullSetting = NullSettings.Null)] + public decimal? DecimalValue { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.PropertyData; - public const int VarcharLength = 512; - public const int SegmentLength = 256; - - private decimal? _decimalValue; - - // pk, not used at the moment (never updating) - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } - - [Column("versionId")] - [ForeignKey(typeof(ContentVersionDto))] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_VersionId", ForColumns = "versionId,propertyTypeId,languageId,segment")] - public int VersionId { get; set; } - - [Column("propertyTypeId")] - [ForeignKey(typeof(PropertyTypeDto))] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_PropertyTypeId")] - public int PropertyTypeId { get; set; } - - [Column("languageId")] - [ForeignKey(typeof(LanguageDto))] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] - [NullSetting(NullSetting = NullSettings.Null)] - public int? LanguageId { get; set; } - - [Column("segment")] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Segment")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(SegmentLength)] - public string? Segment { get; set; } - - [Column("intValue")] - [NullSetting(NullSetting = NullSettings.Null)] - public int? IntegerValue { get; set; } - - [Column("decimalValue")] - [NullSetting(NullSetting = NullSettings.Null)] - public decimal? DecimalValue - { - get => _decimalValue; - set => _decimalValue = value?.Normalize(); - } + get => _decimalValue; + set => _decimalValue = value?.Normalize(); + } - [Column("dateValue")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? DateValue { get; set; } + [Column("dateValue")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? DateValue { get; set; } - [Column("varcharValue")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(VarcharLength)] - public string? VarcharValue { get; set; } + [Column("varcharValue")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(VarcharLength)] + public string? VarcharValue { get; set; } - [Column("textValue")] - [NullSetting(NullSetting = NullSettings.Null)] - [SpecialDbType(SpecialDbTypes.NTEXT)] - public string? TextValue { get; set; } + [Column("textValue")] + [NullSetting(NullSetting = NullSettings.Null)] + [SpecialDbType(SpecialDbTypes.NTEXT)] + public string? TextValue { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne, ColumnName = "PropertyTypeId")] - public PropertyTypeDto? PropertyTypeDto { get; set; } + [ResultColumn] + [Reference(ReferenceType.OneToOne, ColumnName = "PropertyTypeId")] + public PropertyTypeDto? PropertyTypeDto { get; set; } - [Ignore] - public object? Value + [Ignore] + public object? Value + { + get { - get + if (IntegerValue.HasValue) { - if (IntegerValue.HasValue) - return IntegerValue.Value; - - if (DecimalValue.HasValue) - return DecimalValue.Value; - - if (DateValue.HasValue) - return DateValue.Value; - - if (!string.IsNullOrEmpty(VarcharValue)) - return VarcharValue; + return IntegerValue.Value; + } - if (!string.IsNullOrEmpty(TextValue)) - return TextValue; + if (DecimalValue.HasValue) + { + return DecimalValue.Value; + } - return null; + if (DateValue.HasValue) + { + return DateValue.Value; } - } - public PropertyDataDto Clone(int versionId) - { - return new PropertyDataDto + if (!string.IsNullOrEmpty(VarcharValue)) { - VersionId = versionId, - PropertyTypeId = PropertyTypeId, - LanguageId = LanguageId, - Segment = Segment, - IntegerValue = IntegerValue, - DecimalValue = DecimalValue, - DateValue = DateValue, - VarcharValue = VarcharValue, - TextValue = TextValue, - PropertyTypeDto = PropertyTypeDto - }; - } + return VarcharValue; + } - protected bool Equals(PropertyDataDto other) - { - return Id == other.Id; - } + if (!string.IsNullOrEmpty(TextValue)) + { + return TextValue; + } - public override bool Equals(object? other) - { - return - !ReferenceEquals(null, other) // other is not null - && (ReferenceEquals(this, other) // and either ref-equals, or same id - || other is PropertyDataDto pdata && pdata.Id == Id); + return null; } + } - public override int GetHashCode() + public PropertyDataDto Clone(int versionId) => + new PropertyDataDto { - // ReSharper disable once NonReadonlyMemberInGetHashCode - return Id; - } - } + VersionId = versionId, + PropertyTypeId = PropertyTypeId, + LanguageId = LanguageId, + Segment = Segment, + IntegerValue = IntegerValue, + DecimalValue = DecimalValue, + DateValue = DateValue, + VarcharValue = VarcharValue, + TextValue = TextValue, + PropertyTypeDto = PropertyTypeDto + }; + + protected bool Equals(PropertyDataDto other) => Id == other.Id; + + public override bool Equals(object? other) => + !ReferenceEquals(null, other) // other is not null + && (ReferenceEquals(this, other) // and either ref-equals, or same id + || (other is PropertyDataDto pdata && pdata.Id == Id)); + + public override int GetHashCode() => + // ReSharper disable once NonReadonlyMemberInGetHashCode + Id; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeCommonDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeCommonDto.cs index 8e321fa962ba..4ae71654ca1b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeCommonDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeCommonDto.cs @@ -1,18 +1,14 @@ using NPoco; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +// this is PropertyTypeDto + the special property type fields for members +// it is used for querying everything needed for a property type, at once +internal class PropertyTypeCommonDto : PropertyTypeDto { - // this is PropertyTypeDto + the special property type fields for members - // it is used for querying everything needed for a property type, at once - internal class PropertyTypeCommonDto : PropertyTypeDto - { - [Column("memberCanEdit")] - public bool CanEdit { get; set; } + [Column("memberCanEdit")] public bool CanEdit { get; set; } - [Column("viewOnProfile")] - public bool ViewOnProfile { get; set; } + [Column("viewOnProfile")] public bool ViewOnProfile { get; set; } - [Column("isSensitive")] - public bool IsSensitive { get; set; } - } + [Column("isSensitive")] public bool IsSensitive { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeDto.cs index dd4652f36609..39a2473d3f24 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeDto.cs @@ -1,85 +1,84 @@ -using System; -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.PropertyType)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class PropertyTypeDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class PropertyTypeDto - { - private string? _alias; - - [Column("id")] - [PrimaryKeyColumn(IdentitySeed = 100)] - public int Id { get; set; } - - [Column("dataTypeId")] - [ForeignKey(typeof(DataTypeDto), Column = "nodeId")] - public int DataTypeId { get; set; } - - [Column("contentTypeId")] - [ForeignKey(typeof(ContentTypeDto), Column = "nodeId")] - public int ContentTypeId { get; set; } - - [Column("propertyTypeGroupId")] - [NullSetting(NullSetting = NullSettings.Null)] - [ForeignKey(typeof(PropertyTypeGroupDto))] - public int? PropertyTypeGroupId { get; set; } - - [Index(IndexTypes.NonClustered, Name = "IX_cmsPropertyTypeAlias")] - [Column("Alias")] - public string? Alias { get => _alias; set => _alias = value == null ? null : string.Intern(value); } - - [Column("Name")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Name { get; set; } - - [Column("sortOrder")] - [Constraint(Default = "0")] - public int SortOrder { get; set; } - - [Column("mandatory")] - [Constraint(Default = "0")] - public bool Mandatory { get; set; } - - [Column("mandatoryMessage")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(500)] - public string? MandatoryMessage { get; set; } - - [Column("validationRegExp")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? ValidationRegExp { get; set; } - - [Column("validationRegExpMessage")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(500)] - public string? ValidationRegExpMessage { get; set; } - - [Column("Description")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(2000)] - public string? Description { get; set; } - - [Column("labelOnTop")] - [Constraint(Default = "0")] - public bool LabelOnTop { get; set; } - - [Column("variations")] - [Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)] - public byte Variations { get; set; } - - [ResultColumn] - [Reference(ReferenceType.OneToOne, ColumnName = "DataTypeId")] - public DataTypeDto DataTypeDto { get; set; } = null!; - - [Column("UniqueID")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Constraint(Default = SystemMethods.NewGuid)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsPropertyTypeUniqueID")] - public Guid UniqueId { get; set; } - } + private string? _alias; + + [Column("id")] + [PrimaryKeyColumn(IdentitySeed = 100)] + public int Id { get; set; } + + [Column("dataTypeId")] + [ForeignKey(typeof(DataTypeDto), Column = "nodeId")] + public int DataTypeId { get; set; } + + [Column("contentTypeId")] + [ForeignKey(typeof(ContentTypeDto), Column = "nodeId")] + public int ContentTypeId { get; set; } + + [Column("propertyTypeGroupId")] + [NullSetting(NullSetting = NullSettings.Null)] + [ForeignKey(typeof(PropertyTypeGroupDto))] + public int? PropertyTypeGroupId { get; set; } + + [Index(IndexTypes.NonClustered, Name = "IX_cmsPropertyTypeAlias")] + [Column("Alias")] + public string? Alias { get => _alias; set => _alias = value == null ? null : string.Intern(value); } + + [Column("Name")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Name { get; set; } + + [Column("sortOrder")] + [Constraint(Default = "0")] + public int SortOrder { get; set; } + + [Column("mandatory")] + [Constraint(Default = "0")] + public bool Mandatory { get; set; } + + [Column("mandatoryMessage")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(500)] + public string? MandatoryMessage { get; set; } + + [Column("validationRegExp")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? ValidationRegExp { get; set; } + + [Column("validationRegExpMessage")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(500)] + public string? ValidationRegExpMessage { get; set; } + + [Column("Description")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(2000)] + public string? Description { get; set; } + + [Column("labelOnTop")] + [Constraint(Default = "0")] + public bool LabelOnTop { get; set; } + + [Column("variations")] + [Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)] + public byte Variations { get; set; } + + [ResultColumn] + [Reference(ReferenceType.OneToOne, ColumnName = "DataTypeId")] + public DataTypeDto DataTypeDto { get; set; } = null!; + + [Column("UniqueID")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.NewGuid)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsPropertyTypeUniqueID")] + public Guid UniqueId { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeGroupDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeGroupDto.cs index 489cb7fcb532..ec980ea0b886 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeGroupDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeGroupDto.cs @@ -1,47 +1,42 @@ -using System; -using System.Collections.Generic; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id", AutoIncrement = true)] +[ExplicitColumns] +internal class PropertyTypeGroupDto { - [TableName(TableName)] - [PrimaryKey("id", AutoIncrement = true)] - [ExplicitColumns] - internal class PropertyTypeGroupDto - { - public const string TableName = Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup; - - [Column("id")] - [PrimaryKeyColumn(IdentitySeed = 56)] - public int Id { get; set; } - - [Column("uniqueID")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Constraint(Default = SystemMethods.NewGuid)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsPropertyTypeGroupUniqueID")] - public Guid UniqueId { get; set; } - - [Column("contenttypeNodeId")] - [ForeignKey(typeof(ContentTypeDto), Column = "nodeId")] - public int ContentTypeNodeId { get; set; } - - [Column("type")] - [Constraint(Default = 0)] - public short Type { get; set; } - - [Column("text")] - public string? Text { get; set; } - - [Column("alias")] - public string Alias { get; set; } = null!; - - [Column("sortorder")] - public int SortOrder { get; set; } - - [ResultColumn] - [Reference(ReferenceType.Many, ReferenceMemberName = "PropertyTypeGroupId")] - public List? PropertyTypeDtos { get; set; } - } + public const string TableName = Constants.DatabaseSchema.Tables.PropertyTypeGroup; + + [Column("id")] + [PrimaryKeyColumn(IdentitySeed = 56)] + public int Id { get; set; } + + [Column("uniqueID")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.NewGuid)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsPropertyTypeGroupUniqueID")] + public Guid UniqueId { get; set; } + + [Column("contenttypeNodeId")] + [ForeignKey(typeof(ContentTypeDto), Column = "nodeId")] + public int ContentTypeNodeId { get; set; } + + [Column("type")] + [Constraint(Default = 0)] + public short Type { get; set; } + + [Column("text")] public string? Text { get; set; } + + [Column("alias")] public string Alias { get; set; } = null!; + + [Column("sortorder")] public int SortOrder { get; set; } + + [ResultColumn] + [Reference(ReferenceType.Many, ReferenceMemberName = "PropertyTypeGroupId")] + public List? PropertyTypeDtos { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeGroupReadOnlyDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeGroupReadOnlyDto.cs index f93b9b602a17..7efd9e3ecce1 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeGroupReadOnlyDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeGroupReadOnlyDto.cs @@ -1,26 +1,20 @@ -using System; -using NPoco; +using NPoco; +using Umbraco.Cms.Core; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.PropertyTypeGroup)] +[PrimaryKey("id", AutoIncrement = true)] +[ExplicitColumns] +internal class PropertyTypeGroupReadOnlyDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup)] - [PrimaryKey("id", AutoIncrement = true)] - [ExplicitColumns] - internal class PropertyTypeGroupReadOnlyDto - { - [Column("PropertyTypeGroupId")] - public int? Id { get; set; } + [Column("PropertyTypeGroupId")] public int? Id { get; set; } - [Column("PropertyGroupName")] - public string? Text { get; set; } + [Column("PropertyGroupName")] public string? Text { get; set; } - [Column("PropertyGroupSortOrder")] - public int SortOrder { get; set; } + [Column("PropertyGroupSortOrder")] public int SortOrder { get; set; } - [Column("contenttypeNodeId")] - public int ContentTypeNodeId { get; set; } + [Column("contenttypeNodeId")] public int ContentTypeNodeId { get; set; } - [Column("PropertyGroupUniqueID")] - public Guid UniqueId { get; set; } - } + [Column("PropertyGroupUniqueID")] public Guid UniqueId { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeReadOnlyDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeReadOnlyDto.cs index ae1358b5cd88..33173fd98213 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeReadOnlyDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeReadOnlyDto.cs @@ -1,70 +1,50 @@ -using System; -using NPoco; +using NPoco; +using Umbraco.Cms.Core; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.PropertyType)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class PropertyTypeReadOnlyDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class PropertyTypeReadOnlyDto - { - [Column("PropertyTypeId")] - public int? Id { get; set; } + [Column("PropertyTypeId")] public int? Id { get; set; } - [Column("dataTypeId")] - public int DataTypeId { get; set; } + [Column("dataTypeId")] public int DataTypeId { get; set; } - [Column("contentTypeId")] - public int ContentTypeId { get; set; } + [Column("contentTypeId")] public int ContentTypeId { get; set; } - [Column("PropertyTypesGroupId")] - public int? PropertyTypeGroupId { get; set; } + [Column("PropertyTypesGroupId")] public int? PropertyTypeGroupId { get; set; } - [Column("Alias")] - public string? Alias { get; set; } + [Column("Alias")] public string? Alias { get; set; } - [Column("Name")] - public string? Name { get; set; } + [Column("Name")] public string? Name { get; set; } - [Column("PropertyTypeSortOrder")] - public int SortOrder { get; set; } + [Column("PropertyTypeSortOrder")] public int SortOrder { get; set; } - [Column("mandatory")] - public bool Mandatory { get; set; } + [Column("mandatory")] public bool Mandatory { get; set; } - [Column("mandatoryMessage")] - public string? MandatoryMessage { get; set; } + [Column("mandatoryMessage")] public string? MandatoryMessage { get; set; } - [Column("validationRegExp")] - public string? ValidationRegExp { get; set; } + [Column("validationRegExp")] public string? ValidationRegExp { get; set; } - [Column("validationRegExpMessage")] - public string? ValidationRegExpMessage { get; set; } + [Column("validationRegExpMessage")] public string? ValidationRegExpMessage { get; set; } - [Column("Description")] - public string? Description { get; set; } + [Column("Description")] public string? Description { get; set; } - [Column("labelOnTop")] - public bool LabelOnTop { get; set; } + [Column("labelOnTop")] public bool LabelOnTop { get; set; } - /* cmsMemberType */ - [Column("memberCanEdit")] - public bool CanEdit { get; set; } + /* cmsMemberType */ + [Column("memberCanEdit")] public bool CanEdit { get; set; } - [Column("viewOnProfile")] - public bool ViewOnProfile { get; set; } + [Column("viewOnProfile")] public bool ViewOnProfile { get; set; } - [Column("isSensitive")] - public bool IsSensitive { get; set; } + [Column("isSensitive")] public bool IsSensitive { get; set; } - /* DataType */ - [Column("propertyEditorAlias")] - public string? PropertyEditorAlias { get; set; } + /* DataType */ + [Column("propertyEditorAlias")] public string? PropertyEditorAlias { get; set; } - [Column("dbType")] - public string? DbType { get; set; } + [Column("dbType")] public string? DbType { get; set; } - [Column("UniqueID")] - public Guid UniqueId { get; set; } - } + [Column("UniqueID")] public Guid UniqueId { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/RedirectUrlDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/RedirectUrlDto.cs index 435e07230702..7aa4adf4d633 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/RedirectUrlDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/RedirectUrlDto.cs @@ -1,54 +1,50 @@ -using System; -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.RedirectUrl)] +[PrimaryKey("id", AutoIncrement = false)] +[ExplicitColumns] +internal class RedirectUrlDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.RedirectUrl)] - [PrimaryKey("id", AutoIncrement = false)] - [ExplicitColumns] - class RedirectUrlDto - { - public RedirectUrlDto() - { - CreateDateUtc = DateTime.UtcNow; - } - - // notes - // - // we want a unique, non-clustered index on (url ASC, contentId ASC, culture ASC, createDate DESC) but the - // problem is that the index key must be 900 bytes max. should we run without an index? done - // some perfs comparisons, and running with an index on a hash is only slightly slower on - // inserts, and much faster on reads, so... we have an index on a hash. - - [Column("id")] - [PrimaryKeyColumn(Name = "PK_umbracoRedirectUrl", AutoIncrement = false)] - public Guid Id { get; set; } - - [ResultColumn] - public int ContentId { get; set; } - - [Column("contentKey")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [ForeignKey(typeof(NodeDto), Column = "uniqueID")] - public Guid ContentKey { get; set; } - - [Column("createDateUtc")] - [NullSetting(NullSetting = NullSettings.NotNull)] - public DateTime CreateDateUtc { get; set; } - - [Column("url")] - [NullSetting(NullSetting = NullSettings.NotNull)] - public string Url { get; set; } = null!; - - [Column("culture")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Culture { get; set; } - - [Column("urlHash")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRedirectUrl", ForColumns = "urlHash, contentKey, culture, createDateUtc")] - [Length(40)] - public string UrlHash { get; set; } = null!; - } + public RedirectUrlDto() => CreateDateUtc = DateTime.UtcNow; + + // notes + // + // we want a unique, non-clustered index on (url ASC, contentId ASC, culture ASC, createDate DESC) but the + // problem is that the index key must be 900 bytes max. should we run without an index? done + // some perfs comparisons, and running with an index on a hash is only slightly slower on + // inserts, and much faster on reads, so... we have an index on a hash. + + [Column("id")] + [PrimaryKeyColumn(Name = "PK_umbracoRedirectUrl", AutoIncrement = false)] + public Guid Id { get; set; } + + [ResultColumn] public int ContentId { get; set; } + + [Column("contentKey")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [ForeignKey(typeof(NodeDto), Column = "uniqueID")] + public Guid ContentKey { get; set; } + + [Column("createDateUtc")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public DateTime CreateDateUtc { get; set; } + + [Column("url")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string Url { get; set; } = null!; + + [Column("culture")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Culture { get; set; } + + [Column("urlHash")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRedirectUrl", + ForColumns = "urlHash, contentKey, culture, createDateUtc")] + [Length(40)] + public string UrlHash { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/RelationDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/RelationDto.cs index b197f126922c..caaa4486888c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/RelationDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/RelationDto.cs @@ -1,46 +1,42 @@ -using System; -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.Relation)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class RelationDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.Relation)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class RelationDto - { - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } - - [Column("parentId")] - [ForeignKey(typeof(NodeDto), Name = "FK_umbracoRelation_umbracoNode")] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelation_parentChildType", ForColumns = "parentId,childId,relType")] - public int ParentId { get; set; } - - [Column("childId")] - [ForeignKey(typeof(NodeDto), Name = "FK_umbracoRelation_umbracoNode1")] - public int ChildId { get; set; } - - [Column("relType")] - [ForeignKey(typeof(RelationTypeDto))] - public int RelationType { get; set; } - - [Column("datetime")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime Datetime { get; set; } - - [Column("comment")] - [Length(1000)] - public string? Comment { get; set; } - - [ResultColumn] - [Column("parentObjectType")] - public Guid ParentObjectType { get; set; } - - [ResultColumn] - [Column("childObjectType")] - public Guid ChildObjectType { get; set; } - } + [Column("id")] [PrimaryKeyColumn] public int Id { get; set; } + + [Column("parentId")] + [ForeignKey(typeof(NodeDto), Name = "FK_umbracoRelation_umbracoNode")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelation_parentChildType", + ForColumns = "parentId,childId,relType")] + public int ParentId { get; set; } + + [Column("childId")] + [ForeignKey(typeof(NodeDto), Name = "FK_umbracoRelation_umbracoNode1")] + public int ChildId { get; set; } + + [Column("relType")] + [ForeignKey(typeof(RelationTypeDto))] + public int RelationType { get; set; } + + [Column("datetime")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime Datetime { get; set; } + + [Column("comment")] [Length(1000)] public string? Comment { get; set; } + + [ResultColumn] + [Column("parentObjectType")] + public Guid ParentObjectType { get; set; } + + [ResultColumn] + [Column("childObjectType")] + public Guid ChildObjectType { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/RelationTypeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/RelationTypeDto.cs index d1cb5cc278f9..3f5a470fb221 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/RelationTypeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/RelationTypeDto.cs @@ -1,48 +1,46 @@ -using System; -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.RelationType)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class RelationTypeDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.RelationType)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class RelationTypeDto - { - public const int NodeIdSeed = 10; - - [Column("id")] - [PrimaryKeyColumn(IdentitySeed = NodeIdSeed)] - public int Id { get; set; } - - [Column("typeUniqueId")] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_UniqueId")] - public Guid UniqueId { get; set; } - - [Column("dual")] - public bool Dual { get; set; } - - [Column("parentObjectType")] - [NullSetting(NullSetting = NullSettings.Null)] - public Guid? ParentObjectType { get; set; } - - [Column("childObjectType")] - [NullSetting(NullSetting = NullSettings.Null)] - public Guid? ChildObjectType { get; set; } - - [Column("name")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_name")] - public string Name { get; set; } = null!; - - [Column("alias")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Length(100)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_alias")] - public string Alias { get; set; } = null!; - - [Constraint(Default = "0")] - [Column("isDependency")] - public bool IsDependency { get; set; } - } + public const int NodeIdSeed = 10; + + [Column("id")] + [PrimaryKeyColumn(IdentitySeed = NodeIdSeed)] + public int Id { get; set; } + + [Column("typeUniqueId")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_UniqueId")] + public Guid UniqueId { get; set; } + + [Column("dual")] public bool Dual { get; set; } + + [Column("parentObjectType")] + [NullSetting(NullSetting = NullSettings.Null)] + public Guid? ParentObjectType { get; set; } + + [Column("childObjectType")] + [NullSetting(NullSetting = NullSettings.Null)] + public Guid? ChildObjectType { get; set; } + + [Column("name")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_name")] + public string Name { get; set; } = null!; + + [Column("alias")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Length(100)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_alias")] + public string Alias { get; set; } = null!; + + [Constraint(Default = "0")] + [Column("isDependency")] + public bool IsDependency { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ServerRegistrationDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ServerRegistrationDto.cs index 89ef0039ab07..ad0bbe53d590 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ServerRegistrationDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ServerRegistrationDto.cs @@ -1,40 +1,35 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.Server)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class ServerRegistrationDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.Server)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class ServerRegistrationDto - { - [Column("id")] - [PrimaryKeyColumn(AutoIncrement = true)] - public int Id { get; set; } - - [Column("address")] - [Length(500)] - public string? ServerAddress { get; set; } - - [Column("computerName")] - [Length(255)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_computerName")] // server identity is unique - public string? ServerIdentity { get; set; } - - [Column("registeredDate")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime DateRegistered { get; set; } - - [Column("lastNotifiedDate")] - public DateTime DateAccessed { get; set; } - - [Column("isActive")] - [Index(IndexTypes.NonClustered)] - public bool IsActive { get; set; } - - [Column("isSchedulingPublisher")] - public bool IsSchedulingPublisher { get; set; } - } + [Column("id")] + [PrimaryKeyColumn(AutoIncrement = true)] + public int Id { get; set; } + + [Column("address")] [Length(500)] public string? ServerAddress { get; set; } + + [Column("computerName")] + [Length(255)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_computerName")] // server identity is unique + public string? ServerIdentity { get; set; } + + [Column("registeredDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime DateRegistered { get; set; } + + [Column("lastNotifiedDate")] public DateTime DateAccessed { get; set; } + + [Column("isActive")] + [Index(IndexTypes.NonClustered)] + public bool IsActive { get; set; } + + [Column("isSchedulingPublisher")] public bool IsSchedulingPublisher { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/TagDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/TagDto.cs index 8c032660dfb8..48d2e60e49d3 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/TagDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/TagDto.cs @@ -1,40 +1,35 @@ using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class TagDto { - [TableName(TableName)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class TagDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.Tag; - - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } - - [Column("group")] - [Length(100)] - public string Group { get; set; } = null!; - - [Column("languageId")] - [ForeignKey(typeof(LanguageDto))] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] - [NullSetting(NullSetting = NullSettings.Null)] - public int? LanguageId { get;set; } - - [Column("tag")] - [Length(200)] - [Index(IndexTypes.UniqueNonClustered, ForColumns = "group,tag,languageId", Name = "IX_cmsTags")] - public string Text { get; set; } = null!; - - //[Column("key")] - //[Length(301)] // de-normalized "{group}/{tag}" - //public string Key { get; set; } - - // queries result column - [ResultColumn("NodeCount")] - public int NodeCount { get; set; } - } + public const string TableName = Constants.DatabaseSchema.Tables.Tag; + + [Column("id")] [PrimaryKeyColumn] public int Id { get; set; } + + [Column("group")] [Length(100)] public string Group { get; set; } = null!; + + [Column("languageId")] + [ForeignKey(typeof(LanguageDto))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? LanguageId { get; set; } + + [Column("tag")] + [Length(200)] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "group,tag,languageId", Name = "IX_cmsTags")] + public string Text { get; set; } = null!; + + //[Column("key")] + //[Length(301)] // de-normalized "{group}/{tag}" + //public string Key { get; set; } + + // queries result column + [ResultColumn("NodeCount")] public int NodeCount { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/TagRelationshipDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/TagRelationshipDto.cs index 2cc287ac9225..27edbea36752 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/TagRelationshipDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/TagRelationshipDto.cs @@ -1,26 +1,27 @@ using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("nodeId", AutoIncrement = false)] +[ExplicitColumns] +internal class TagRelationshipDto { - [TableName(TableName)] - [PrimaryKey("nodeId", AutoIncrement = false)] - [ExplicitColumns] - internal class TagRelationshipDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.TagRelationship; + public const string TableName = Constants.DatabaseSchema.Tables.TagRelationship; - [Column("nodeId")] - [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_cmsTagRelationship", OnColumns = "nodeId, propertyTypeId, tagId")] - [ForeignKey(typeof(ContentDto), Name = "FK_cmsTagRelationship_cmsContent", Column = "nodeId")] - public int NodeId { get; set; } + [Column("nodeId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_cmsTagRelationship", + OnColumns = "nodeId, propertyTypeId, tagId")] + [ForeignKey(typeof(ContentDto), Name = "FK_cmsTagRelationship_cmsContent", Column = "nodeId")] + public int NodeId { get; set; } - [Column("tagId")] - [ForeignKey(typeof(TagDto))] - public int TagId { get; set; } + [Column("tagId")] + [ForeignKey(typeof(TagDto))] + public int TagId { get; set; } - [Column("propertyTypeId")] - [ForeignKey(typeof(PropertyTypeDto), Name = "FK_cmsTagRelationship_cmsPropertyType")] - public int PropertyTypeId { get; set; } - } + [Column("propertyTypeId")] + [ForeignKey(typeof(PropertyTypeDto), Name = "FK_cmsTagRelationship_cmsPropertyType")] + public int PropertyTypeId { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/TemplateDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/TemplateDto.cs index 9a80cdd8e2e5..039952ef2855 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/TemplateDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/TemplateDto.cs @@ -1,29 +1,27 @@ using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.Template)] +[PrimaryKey("pk")] +[ExplicitColumns] +internal class TemplateDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.Template)] - [PrimaryKey("pk")] - [ExplicitColumns] - internal class TemplateDto - { - [Column("pk")] - [PrimaryKeyColumn] - public int PrimaryKey { get; set; } + [Column("pk")] [PrimaryKeyColumn] public int PrimaryKey { get; set; } - [Column("nodeId")] - [Index(IndexTypes.UniqueNonClustered)] - [ForeignKey(typeof(NodeDto), Name = "FK_cmsTemplate_umbracoNode")] - public int NodeId { get; set; } + [Column("nodeId")] + [Index(IndexTypes.UniqueNonClustered)] + [ForeignKey(typeof(NodeDto), Name = "FK_cmsTemplate_umbracoNode")] + public int NodeId { get; set; } - [Column("alias")] - [Length(100)] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Alias { get; set; } + [Column("alias")] + [Length(100)] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Alias { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne, ColumnName = "NodeId")] - public NodeDto NodeDto { get; set; } = null!; - } + [ResultColumn] + [Reference(ReferenceType.OneToOne, ColumnName = "NodeId")] + public NodeDto NodeDto { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/TwoFactorLoginDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/TwoFactorLoginDto.cs index 09f6647bfeab..2486ddc6950d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/TwoFactorLoginDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/TwoFactorLoginDto.cs @@ -1,34 +1,31 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[ExplicitColumns] +[PrimaryKey("Id")] +internal class TwoFactorLoginDto { - [TableName(TableName)] - [ExplicitColumns] - [PrimaryKey("Id")] - internal class TwoFactorLoginDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.TwoFactorLogin; + public const string TableName = Constants.DatabaseSchema.Tables.TwoFactorLogin; - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + [Column("id")] [PrimaryKeyColumn] public int Id { get; set; } - [Column("userOrMemberKey")] - [Index(IndexTypes.NonClustered)] - public Guid UserOrMemberKey { get; set; } + [Column("userOrMemberKey")] + [Index(IndexTypes.NonClustered)] + public Guid UserOrMemberKey { get; set; } - [Column("providerName")] - [Length(400)] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.UniqueNonClustered, ForColumns = "providerName,userOrMemberKey", - Name = "IX_" + TableName + "_ProviderName")] - public string ProviderName { get; set; } = null!; + [Column("providerName")] + [Length(400)] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "providerName,userOrMemberKey", + Name = "IX_" + TableName + "_ProviderName")] + public string ProviderName { get; set; } = null!; - [Column("secret")] - [Length(400)] - [NullSetting(NullSetting = NullSettings.NotNull)] - public string Secret { get; set; } = null!; - } + [Column("secret")] + [Length(400)] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string Secret { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/User2NodeNotifyDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/User2NodeNotifyDto.cs index fd8806124ec1..016acc535e97 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/User2NodeNotifyDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/User2NodeNotifyDto.cs @@ -1,25 +1,25 @@ using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.User2NodeNotify)] +[PrimaryKey("userId", AutoIncrement = false)] +[ExplicitColumns] +internal class User2NodeNotifyDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.User2NodeNotify)] - [PrimaryKey("userId", AutoIncrement = false)] - [ExplicitColumns] - internal class User2NodeNotifyDto - { - [Column("userId")] - [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_umbracoUser2NodeNotify", OnColumns = "userId, nodeId, action")] - [ForeignKey(typeof(UserDto))] - public int UserId { get; set; } + [Column("userId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_umbracoUser2NodeNotify", OnColumns = "userId, nodeId, action")] + [ForeignKey(typeof(UserDto))] + public int UserId { get; set; } - [Column("nodeId")] - [ForeignKey(typeof(NodeDto))] - public int NodeId { get; set; } + [Column("nodeId")] + [ForeignKey(typeof(NodeDto))] + public int NodeId { get; set; } - [Column("action")] - [SpecialDbType(SpecialDbTypes.NCHAR)] - [Length(1)] - public string? Action { get; set; } - } + [Column("action")] + [SpecialDbType(SpecialDbTypes.NCHAR)] + [Length(1)] + public string? Action { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/User2UserGroupDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/User2UserGroupDto.cs index db3d5b4e74a5..3bc059ff214b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/User2UserGroupDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/User2UserGroupDto.cs @@ -1,19 +1,19 @@ using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.User2UserGroup)] +[ExplicitColumns] +public class User2UserGroupDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.User2UserGroup)] - [ExplicitColumns] - public class User2UserGroupDto - { - [Column("userId")] - [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_user2userGroup", OnColumns = "userId, userGroupId")] - [ForeignKey(typeof(UserDto))] - public int UserId { get; set; } + [Column("userId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_user2userGroup", OnColumns = "userId, userGroupId")] + [ForeignKey(typeof(UserDto))] + public int UserId { get; set; } - [Column("userGroupId")] - [ForeignKey(typeof(UserGroupDto))] - public int UserGroupId { get; set; } - } + [Column("userGroupId")] + [ForeignKey(typeof(UserGroupDto))] + public int UserGroupId { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs index 20768eed65d7..7275af2e757e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs @@ -1,127 +1,121 @@ -using System; -using System.Collections.Generic; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id", AutoIncrement = true)] +[ExplicitColumns] +public class UserDto { - [TableName(TableName)] - [PrimaryKey("id", AutoIncrement = true)] - [ExplicitColumns] - public class UserDto + public const string TableName = Constants.DatabaseSchema.Tables.User; + + public UserDto() { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.User; - - public UserDto() - { - UserGroupDtos = new List(); - UserStartNodeDtos = new HashSet(); - } - - // TODO: We need to add a GUID for users and track external logins with that instead of the INT - - [Column("id")] - [PrimaryKeyColumn(Name = "PK_user")] - public int Id { get; set; } - - [Column("userDisabled")] - [Constraint(Default = "0")] - public bool Disabled { get; set; } - - [Column("userNoConsole")] - [Constraint(Default = "0")] - public bool NoConsole { get; set; } - - [Column("userName")] - public string UserName { get; set; } = null!; - - [Column("userLogin")] - [Length(125)] - [Index(IndexTypes.NonClustered)] - public string? Login { get; set; } - - [Column("userPassword")] - [Length(500)] - public string? Password { get; set; } - - /// - /// This will represent a JSON structure of how the password has been created (i.e hash algorithm, iterations) - /// - [Column("passwordConfig")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(500)] - public string? PasswordConfig { get; set; } - - [Column("userEmail")] - public string Email { get; set; } = null!; - - [Column("userLanguage")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(10)] - public string? UserLanguage { get; set; } - - [Column("securityStampToken")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(255)] - public string? SecurityStampToken { get; set; } - - [Column("failedLoginAttempts")] - [NullSetting(NullSetting = NullSettings.Null)] - public int? FailedLoginAttempts { get; set; } - - [Column("lastLockoutDate")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? LastLockoutDate { get; set; } - - [Column("lastPasswordChangeDate")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? LastPasswordChangeDate { get; set; } - - [Column("lastLoginDate")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? LastLoginDate { get; set; } - - [Column("emailConfirmedDate")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? EmailConfirmedDate { get; set; } - - [Column("invitedDate")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? InvitedDate { get; set; } - - [Column("createDate")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime CreateDate { get; set; } = DateTime.Now; - - [Column("updateDate")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime UpdateDate { get; set; } = DateTime.Now; - - /// - /// Will hold the media file system relative path of the users custom avatar if they uploaded one - /// - [Column("avatar")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(500)] - public string? Avatar { get; set; } - - /// - /// A Json blob stored for recording tour data for a user - /// - [Column("tourData")] - [NullSetting(NullSetting = NullSettings.Null)] - [SpecialDbType(SpecialDbTypes.NTEXT)] - public string? TourData { get; set; } - - [ResultColumn] - [Reference(ReferenceType.Many, ReferenceMemberName = "UserId")] - public List UserGroupDtos { get; set; } - - [ResultColumn] - [Reference(ReferenceType.Many, ReferenceMemberName = "UserId")] - public HashSet UserStartNodeDtos { get; set; } + UserGroupDtos = new List(); + UserStartNodeDtos = new HashSet(); } + + // TODO: We need to add a GUID for users and track external logins with that instead of the INT + + [Column("id")] + [PrimaryKeyColumn(Name = "PK_user")] + public int Id { get; set; } + + [Column("userDisabled")] + [Constraint(Default = "0")] + public bool Disabled { get; set; } + + [Column("userNoConsole")] + [Constraint(Default = "0")] + public bool NoConsole { get; set; } + + [Column("userName")] public string UserName { get; set; } = null!; + + [Column("userLogin")] + [Length(125)] + [Index(IndexTypes.NonClustered)] + public string? Login { get; set; } + + [Column("userPassword")] [Length(500)] public string? Password { get; set; } + + /// + /// This will represent a JSON structure of how the password has been created (i.e hash algorithm, iterations) + /// + [Column("passwordConfig")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(500)] + public string? PasswordConfig { get; set; } + + [Column("userEmail")] public string Email { get; set; } = null!; + + [Column("userLanguage")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(10)] + public string? UserLanguage { get; set; } + + [Column("securityStampToken")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(255)] + public string? SecurityStampToken { get; set; } + + [Column("failedLoginAttempts")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? FailedLoginAttempts { get; set; } + + [Column("lastLockoutDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? LastLockoutDate { get; set; } + + [Column("lastPasswordChangeDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? LastPasswordChangeDate { get; set; } + + [Column("lastLoginDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? LastLoginDate { get; set; } + + [Column("emailConfirmedDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? EmailConfirmedDate { get; set; } + + [Column("invitedDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? InvitedDate { get; set; } + + [Column("createDate")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime CreateDate { get; set; } = DateTime.Now; + + [Column("updateDate")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime UpdateDate { get; set; } = DateTime.Now; + + /// + /// Will hold the media file system relative path of the users custom avatar if they uploaded one + /// + [Column("avatar")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(500)] + public string? Avatar { get; set; } + + /// + /// A Json blob stored for recording tour data for a user + /// + [Column("tourData")] + [NullSetting(NullSetting = NullSettings.Null)] + [SpecialDbType(SpecialDbTypes.NTEXT)] + public string? TourData { get; set; } + + [ResultColumn] + [Reference(ReferenceType.Many, ReferenceMemberName = "UserId")] + public List UserGroupDtos { get; set; } + + [ResultColumn] + [Reference(ReferenceType.Many, ReferenceMemberName = "UserId")] + public HashSet UserStartNodeDtos { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2AppDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2AppDto.cs index b5719c1c63c4..37a16066bd68 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2AppDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2AppDto.cs @@ -1,19 +1,17 @@ using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.UserGroup2App)] +[ExplicitColumns] +public class UserGroup2AppDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup2App)] - [ExplicitColumns] - public class UserGroup2AppDto - { - [Column("userGroupId")] - [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_userGroup2App", OnColumns = "userGroupId, app")] - [ForeignKey(typeof(UserGroupDto))] - public int UserGroupId { get; set; } + [Column("userGroupId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_userGroup2App", OnColumns = "userGroupId, app")] + [ForeignKey(typeof(UserGroupDto))] + public int UserGroupId { get; set; } - [Column("app")] - [Length(50)] - public string AppAlias { get; set; } = null!; - } + [Column("app")] [Length(50)] public string AppAlias { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2NodeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2NodeDto.cs index ad172c846c70..54b66b8e220a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2NodeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2NodeDto.cs @@ -2,22 +2,21 @@ using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[ExplicitColumns] +internal class UserGroup2NodeDto { - [TableName(TableName)] - [ExplicitColumns] - internal class UserGroup2NodeDto - { - public const string TableName = Constants.DatabaseSchema.Tables.UserGroup2Node; + public const string TableName = Constants.DatabaseSchema.Tables.UserGroup2Node; - [Column("userGroupId")] - [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_" + TableName, OnColumns = "userGroupId, nodeId")] - [ForeignKey(typeof(UserGroupDto))] - public int UserGroupId { get; set; } + [Column("userGroupId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_" + TableName, OnColumns = "userGroupId, nodeId")] + [ForeignKey(typeof(UserGroupDto))] + public int UserGroupId { get; set; } - [Column("nodeId")] - [ForeignKey(typeof(NodeDto))] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_nodeId")] - public int NodeId { get; set; } - } + [Column("nodeId")] + [ForeignKey(typeof(NodeDto))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_nodeId")] + public int NodeId { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2NodePermissionDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2NodePermissionDto.cs index 4461089f9675..7f88a5088a92 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2NodePermissionDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2NodePermissionDto.cs @@ -1,23 +1,23 @@ using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.UserGroup2NodePermission)] +[ExplicitColumns] +internal class UserGroup2NodePermissionDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup2NodePermission)] - [ExplicitColumns] - internal class UserGroup2NodePermissionDto - { - [Column("userGroupId")] - [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_umbracoUserGroup2NodePermission", OnColumns = "userGroupId, nodeId, permission")] - [ForeignKey(typeof(UserGroupDto))] - public int UserGroupId { get; set; } + [Column("userGroupId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_umbracoUserGroup2NodePermission", + OnColumns = "userGroupId, nodeId, permission")] + [ForeignKey(typeof(UserGroupDto))] + public int UserGroupId { get; set; } - [Column("nodeId")] - [ForeignKey(typeof(NodeDto))] - [Index(IndexTypes.NonClustered, Name = "IX_umbracoUser2NodePermission_nodeId")] - public int NodeId { get; set; } + [Column("nodeId")] + [ForeignKey(typeof(NodeDto))] + [Index(IndexTypes.NonClustered, Name = "IX_umbracoUser2NodePermission_nodeId")] + public int NodeId { get; set; } - [Column("permission")] - public string? Permission { get; set; } - } + [Column("permission")] public string? Permission { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroupDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroupDto.cs index afbda3cc9a0c..e40d3bb2104e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroupDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroupDto.cs @@ -1,72 +1,67 @@ -using System; -using System.Collections.Generic; -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.UserGroup)] +[PrimaryKey("id")] +[ExplicitColumns] +public class UserGroupDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup)] - [PrimaryKey("id")] - [ExplicitColumns] - public class UserGroupDto - { - public UserGroupDto() - { - UserGroup2AppDtos = new List(); - } + public UserGroupDto() => UserGroup2AppDtos = new List(); - [Column("id")] - [PrimaryKeyColumn(IdentitySeed = 6)] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn(IdentitySeed = 6)] + public int Id { get; set; } - [Column("userGroupAlias")] - [Length(200)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoUserGroup_userGroupAlias")] - public string? Alias { get; set; } + [Column("userGroupAlias")] + [Length(200)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoUserGroup_userGroupAlias")] + public string? Alias { get; set; } - [Column("userGroupName")] - [Length(200)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoUserGroup_userGroupName")] - public string? Name { get; set; } + [Column("userGroupName")] + [Length(200)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoUserGroup_userGroupName")] + public string? Name { get; set; } - [Column("userGroupDefaultPermissions")] - [Length(50)] - [NullSetting(NullSetting = NullSettings.Null)] - public string? DefaultPermissions { get; set; } + [Column("userGroupDefaultPermissions")] + [Length(50)] + [NullSetting(NullSetting = NullSettings.Null)] + public string? DefaultPermissions { get; set; } - [Column("createDate")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime CreateDate { get; set; } + [Column("createDate")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime CreateDate { get; set; } - [Column("updateDate")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime UpdateDate { get; set; } + [Column("updateDate")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime UpdateDate { get; set; } - [Column("icon")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Icon { get; set; } + [Column("icon")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Icon { get; set; } - [Column("startContentId")] - [NullSetting(NullSetting = NullSettings.Null)] - [ForeignKey(typeof(NodeDto), Name = "FK_startContentId_umbracoNode_id")] - public int? StartContentId { get; set; } + [Column("startContentId")] + [NullSetting(NullSetting = NullSettings.Null)] + [ForeignKey(typeof(NodeDto), Name = "FK_startContentId_umbracoNode_id")] + public int? StartContentId { get; set; } - [Column("startMediaId")] - [NullSetting(NullSetting = NullSettings.Null)] - [ForeignKey(typeof(NodeDto), Name = "FK_startMediaId_umbracoNode_id")] - public int? StartMediaId { get; set; } + [Column("startMediaId")] + [NullSetting(NullSetting = NullSettings.Null)] + [ForeignKey(typeof(NodeDto), Name = "FK_startMediaId_umbracoNode_id")] + public int? StartMediaId { get; set; } - [ResultColumn] - [Reference(ReferenceType.Many, ReferenceMemberName = "UserGroupId")] - public List UserGroup2AppDtos { get; set; } + [ResultColumn] + [Reference(ReferenceType.Many, ReferenceMemberName = "UserGroupId")] + public List UserGroup2AppDtos { get; set; } - /// - /// This is only relevant when this column is included in the results (i.e. GetUserGroupsWithUserCounts) - /// - [ResultColumn] - public int UserCount { get; set; } - } + /// + /// This is only relevant when this column is included in the results (i.e. GetUserGroupsWithUserCounts) + /// + [ResultColumn] + public int UserCount { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserLoginDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserLoginDto.cs index 4d18a39557d5..68af3f5a7317 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserLoginDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserLoginDto.cs @@ -1,57 +1,60 @@ -using System; -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("sessionId", AutoIncrement = false)] +[ExplicitColumns] +internal class UserLoginDto { - [TableName(TableName)] - [PrimaryKey("sessionId", AutoIncrement = false)] - [ExplicitColumns] - internal class UserLoginDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.UserLogin; - - [Column("sessionId")] - [PrimaryKeyColumn(AutoIncrement = false)] - public Guid SessionId { get; set; } - - [Column("userId")] - [ForeignKey(typeof(UserDto), Name = "FK_" + TableName + "_umbracoUser_id")] - public int? UserId { get; set; } - - /// - /// Tracks when the session is created - /// - [Column("loggedInUtc")] - [NullSetting(NullSetting = NullSettings.NotNull)] - public DateTime LoggedInUtc { get; set; } - - /// - /// Updated every time a user's session is validated - /// - /// - /// This allows us to guess if a session is timed out if a user doesn't actively - /// log out and also allows us to trim the data in the table. - /// The index is IMPORTANT as it prevents deadlocks during deletion of - /// old sessions (DELETE ... WHERE lastValidatedUtc < date). - /// - [Column("lastValidatedUtc")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.NonClustered, Name = "IX_umbracoUserLogin_lastValidatedUtc")] - public DateTime LastValidatedUtc { get; set; } - - /// - /// Tracks when the session is removed when the user's account is logged out - /// - [Column("loggedOutUtc")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? LoggedOutUtc { get; set; } - - /// - /// Logs the IP address of the session if available - /// - [Column("ipAddress")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? IpAddress { get; set; } - } + public const string TableName = Constants.DatabaseSchema.Tables.UserLogin; + + [Column("sessionId")] + [PrimaryKeyColumn(AutoIncrement = false)] + public Guid SessionId { get; set; } + + [Column("userId")] + [ForeignKey(typeof(UserDto), Name = "FK_" + TableName + "_umbracoUser_id")] + public int? UserId { get; set; } + + /// + /// Tracks when the session is created + /// + [Column("loggedInUtc")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public DateTime LoggedInUtc { get; set; } + + /// + /// Updated every time a user's session is validated + /// + /// + /// + /// This allows us to guess if a session is timed out if a user doesn't actively + /// log out and also allows us to trim the data in the table. + /// + /// + /// The index is IMPORTANT as it prevents deadlocks during deletion of + /// old sessions (DELETE ... WHERE lastValidatedUtc < date). + /// + /// + [Column("lastValidatedUtc")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.NonClustered, Name = "IX_umbracoUserLogin_lastValidatedUtc")] + public DateTime LastValidatedUtc { get; set; } + + /// + /// Tracks when the session is removed when the user's account is logged out + /// + [Column("loggedOutUtc")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? LoggedOutUtc { get; set; } + + /// + /// Logs the IP address of the session if available + /// + [Column("ipAddress")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? IpAddress { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserNotificationDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserNotificationDto.cs index c6116648c7d2..0d926e954574 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserNotificationDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserNotificationDto.cs @@ -1,20 +1,14 @@ -using System; using NPoco; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +internal class UserNotificationDto { - internal class UserNotificationDto - { - [Column("nodeId")] - public int NodeId { get; set; } + [Column("nodeId")] public int NodeId { get; set; } - [Column("userId")] - public int UserId { get; set; } + [Column("userId")] public int UserId { get; set; } - [Column("nodeObjectType")] - public Guid NodeObjectType { get; set; } + [Column("nodeObjectType")] public Guid NodeObjectType { get; set; } - [Column("action")] - public string Action { get; set; } = null!; - } + [Column("action")] public string Action { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserStartNodeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserStartNodeDto.cs index 44e637900709..455532a28917 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserStartNodeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserStartNodeDto.cs @@ -1,67 +1,78 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.UserStartNode)] +[PrimaryKey("id", AutoIncrement = true)] +[ExplicitColumns] +public class UserStartNodeDto : IEquatable { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.UserStartNode)] - [PrimaryKey("id", AutoIncrement = true)] - [ExplicitColumns] - public class UserStartNodeDto : IEquatable + public enum StartNodeTypeValue { - [Column("id")] - [PrimaryKeyColumn(Name = "PK_userStartNode")] - public int Id { get; set; } - - [Column("userId")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [ForeignKey(typeof(UserDto))] - public int UserId { get; set; } - - [Column("startNode")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [ForeignKey(typeof(NodeDto))] - public int StartNode { get; set; } - - [Column("startNodeType")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.UniqueNonClustered, ForColumns = "startNodeType, startNode, userId", Name = "IX_umbracoUserStartNode_startNodeType")] - public int StartNodeType { get; set; } - - public enum StartNodeTypeValue - { - Content = 1, - Media = 2 - } + Content = 1, + Media = 2 + } + + [Column("id")] + [PrimaryKeyColumn(Name = "PK_userStartNode")] + public int Id { get; set; } + + [Column("userId")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [ForeignKey(typeof(UserDto))] + public int UserId { get; set; } + + [Column("startNode")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [ForeignKey(typeof(NodeDto))] + public int StartNode { get; set; } + + [Column("startNodeType")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "startNodeType, startNode, userId", + Name = "IX_umbracoUserStartNode_startNodeType")] + public int StartNodeType { get; set; } - public bool Equals(UserStartNodeDto? other) + public bool Equals(UserStartNodeDto? other) + { + if (ReferenceEquals(null, other)) { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return Id == other.Id; + return false; } - public override bool Equals(object? obj) + if (ReferenceEquals(this, other)) { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != GetType()) return false; - return Equals((UserStartNodeDto) obj); + return true; } - public override int GetHashCode() + return Id == other.Id; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { - return Id; + return false; } - public static bool operator ==(UserStartNodeDto left, UserStartNodeDto right) + if (ReferenceEquals(this, obj)) { - return Equals(left, right); + return true; } - public static bool operator !=(UserStartNodeDto left, UserStartNodeDto right) + if (obj.GetType() != GetType()) { - return !Equals(left, right); + return false; } + + return Equals((UserStartNodeDto)obj); } + + public override int GetHashCode() => Id; + + public static bool operator ==(UserStartNodeDto left, UserStartNodeDto right) => Equals(left, right); + + public static bool operator !=(UserStartNodeDto left, UserStartNodeDto right) => !Equals(left, right); } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/AuditEntryFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/AuditEntryFactory.cs index 67c60ffb4d8e..efec73f9cbee 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/AuditEntryFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/AuditEntryFactory.cs @@ -1,52 +1,45 @@ -using System.Collections.Generic; -using System.Linq; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class AuditEntryFactory { - internal static class AuditEntryFactory - { - public static IEnumerable BuildEntities(IEnumerable dtos) - { - return dtos.Select(BuildEntity).ToList(); - } + public static IEnumerable BuildEntities(IEnumerable dtos) => + dtos.Select(BuildEntity).ToList(); - public static IAuditEntry BuildEntity(AuditEntryDto dto) + public static IAuditEntry BuildEntity(AuditEntryDto dto) + { + var entity = new AuditEntry { - var entity = new AuditEntry - { - Id = dto.Id, - PerformingUserId = dto.PerformingUserId, - PerformingDetails = dto.PerformingDetails, - PerformingIp = dto.PerformingIp, - EventDateUtc = dto.EventDateUtc, - AffectedUserId = dto.AffectedUserId, - AffectedDetails = dto.AffectedDetails, - EventType = dto.EventType, - EventDetails = dto.EventDetails - }; + Id = dto.Id, + PerformingUserId = dto.PerformingUserId, + PerformingDetails = dto.PerformingDetails, + PerformingIp = dto.PerformingIp, + EventDateUtc = dto.EventDateUtc, + AffectedUserId = dto.AffectedUserId, + AffectedDetails = dto.AffectedDetails, + EventType = dto.EventType, + EventDetails = dto.EventDetails + }; - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - entity.ResetDirtyProperties(false); - return entity; - } + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + entity.ResetDirtyProperties(false); + return entity; + } - public static AuditEntryDto BuildDto(IAuditEntry entity) + public static AuditEntryDto BuildDto(IAuditEntry entity) => + new AuditEntryDto { - return new AuditEntryDto - { - Id = entity.Id, - PerformingUserId = entity.PerformingUserId, - PerformingDetails = entity.PerformingDetails, - PerformingIp = entity.PerformingIp, - EventDateUtc = entity.EventDateUtc, - AffectedUserId = entity.AffectedUserId, - AffectedDetails = entity.AffectedDetails, - EventType = entity.EventType, - EventDetails = entity.EventDetails - }; - } - } + Id = entity.Id, + PerformingUserId = entity.PerformingUserId, + PerformingDetails = entity.PerformingDetails, + PerformingIp = entity.PerformingIp, + EventDateUtc = entity.EventDateUtc, + AffectedUserId = entity.AffectedUserId, + AffectedDetails = entity.AffectedDetails, + EventType = entity.EventType, + EventDetails = entity.EventDetails + }; } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/CacheInstructionFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/CacheInstructionFactory.cs index 1a38348acf48..271453006b78 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/CacheInstructionFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/CacheInstructionFactory.cs @@ -1,25 +1,23 @@ -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class CacheInstructionFactory { - internal static class CacheInstructionFactory - { - public static IEnumerable BuildEntities(IEnumerable dtos) => dtos.Select(BuildEntity).ToList(); + public static IEnumerable BuildEntities(IEnumerable dtos) => + dtos.Select(BuildEntity).ToList(); - public static CacheInstruction BuildEntity(CacheInstructionDto dto) => - new CacheInstruction(dto.Id, dto.UtcStamp, dto.Instructions, dto.OriginIdentity, dto.InstructionCount); + public static CacheInstruction BuildEntity(CacheInstructionDto dto) => + new(dto.Id, dto.UtcStamp, dto.Instructions, dto.OriginIdentity, dto.InstructionCount); - public static CacheInstructionDto BuildDto(CacheInstruction entity) => - new CacheInstructionDto - { - Id = entity.Id, - UtcStamp = entity.UtcStamp, - Instructions = entity.Instructions, - OriginIdentity = entity.OriginIdentity, - InstructionCount = entity.InstructionCount, - }; - } + public static CacheInstructionDto BuildDto(CacheInstruction entity) => + new() + { + Id = entity.Id, + UtcStamp = entity.UtcStamp, + Instructions = entity.Instructions, + OriginIdentity = entity.OriginIdentity, + InstructionCount = entity.InstructionCount + }; } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/ConsentFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/ConsentFactory.cs index 33f348a64411..7a7091802fc7 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/ConsentFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/ConsentFactory.cs @@ -1,65 +1,64 @@ -using System.Collections.Generic; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class ConsentFactory { - internal static class ConsentFactory + public static IEnumerable BuildEntities(IEnumerable dtos) { - public static IEnumerable BuildEntities(IEnumerable dtos) + var ix = new Dictionary(); + var output = new List(); + + foreach (ConsentDto dto in dtos) { - var ix = new Dictionary(); - var output = new List(); + var k = dto.Source + "::" + dto.Context + "::" + dto.Action; - foreach (var dto in dtos) + var consent = new Consent { - var k = dto.Source + "::" + dto.Context + "::" + dto.Action; - - var consent = new Consent - { - Id = dto.Id, - Current = dto.Current, - CreateDate = dto.CreateDate, - Source = dto.Source, - Context = dto.Context, - Action = dto.Action, - State = (ConsentState) dto.State, // assume value is valid - Comment = dto.Comment - }; + Id = dto.Id, + Current = dto.Current, + CreateDate = dto.CreateDate, + Source = dto.Source, + Context = dto.Context, + Action = dto.Action, + State = (ConsentState)dto.State, // assume value is valid + Comment = dto.Comment + }; - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - consent.ResetDirtyProperties(false); + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + consent.ResetDirtyProperties(false); - if (ix.TryGetValue(k, out var current)) - { - if (current.HistoryInternal == null) - current.HistoryInternal = new List(); - current.HistoryInternal.Add(consent); - } - else + if (ix.TryGetValue(k, out Consent? current)) + { + if (current.HistoryInternal == null) { - ix[k] = consent; - output.Add(consent); + current.HistoryInternal = new List(); } - } - return output; - } - - public static ConsentDto BuildDto(IConsent entity) - { - return new ConsentDto + current.HistoryInternal.Add(consent); + } + else { - Id = entity.Id, - Current = entity.Current, - CreateDate = entity.CreateDate, - Source = entity.Source, - Context = entity.Context, - Action = entity.Action, - State = (int) entity.State, - Comment = entity.Comment - }; + ix[k] = consent; + output.Add(consent); + } } + + return output; } + + public static ConsentDto BuildDto(IConsent entity) => + new ConsentDto + { + Id = entity.Id, + Current = entity.Current, + CreateDate = entity.CreateDate, + Source = entity.Source, + Context = entity.Context, + Action = entity.Action, + State = (int)entity.State, + Comment = entity.Comment + }; } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/ContentBaseFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/ContentBaseFactory.cs index 5048474ee71d..7804d068f196 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/ContentBaseFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/ContentBaseFactory.cs @@ -1,187 +1,186 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal class ContentBaseFactory { - internal class ContentBaseFactory + /// + /// Builds an IContent item from a dto and content type. + /// + public static Content BuildEntity(DocumentDto dto, IContentType? contentType) { - /// - /// Builds an IContent item from a dto and content type. - /// - public static Content BuildEntity(DocumentDto dto, IContentType? contentType) - { - var contentDto = dto.ContentDto; - var nodeDto = contentDto.NodeDto; - var documentVersionDto = dto.DocumentVersionDto; - var contentVersionDto = documentVersionDto.ContentVersionDto; - var publishedVersionDto = dto.PublishedVersionDto; - - var content = new Content(nodeDto.Text, nodeDto.ParentId, contentType); - - try - { - content.DisableChangeTracking(); + ContentDto contentDto = dto.ContentDto; + NodeDto nodeDto = contentDto.NodeDto; + DocumentVersionDto documentVersionDto = dto.DocumentVersionDto; + ContentVersionDto contentVersionDto = documentVersionDto.ContentVersionDto; + DocumentVersionDto? publishedVersionDto = dto.PublishedVersionDto; - content.Id = dto.NodeId; - content.Key = nodeDto.UniqueId; - content.VersionId = contentVersionDto.Id; + var content = new Content(nodeDto.Text, nodeDto.ParentId, contentType); - content.Name = contentVersionDto.Text; + try + { + content.DisableChangeTracking(); - content.Path = nodeDto.Path; - content.Level = nodeDto.Level; - content.ParentId = nodeDto.ParentId; - content.SortOrder = nodeDto.SortOrder; - content.Trashed = nodeDto.Trashed; + content.Id = dto.NodeId; + content.Key = nodeDto.UniqueId; + content.VersionId = contentVersionDto.Id; - content.CreatorId = nodeDto.UserId ?? Cms.Core.Constants.Security.UnknownUserId; - content.WriterId = contentVersionDto.UserId ?? Cms.Core.Constants.Security.UnknownUserId; - content.CreateDate = nodeDto.CreateDate; - content.UpdateDate = contentVersionDto.VersionDate; + content.Name = contentVersionDto.Text; - content.Published = dto.Published; - content.Edited = dto.Edited; + content.Path = nodeDto.Path; + content.Level = nodeDto.Level; + content.ParentId = nodeDto.ParentId; + content.SortOrder = nodeDto.SortOrder; + content.Trashed = nodeDto.Trashed; - // TODO: shall we get published infos or not? - //if (dto.Published) - if (publishedVersionDto != null) - { - content.PublishedVersionId = publishedVersionDto.Id; - content.PublishDate = publishedVersionDto.ContentVersionDto.VersionDate; - content.PublishName = publishedVersionDto.ContentVersionDto.Text; - content.PublisherId = publishedVersionDto.ContentVersionDto.UserId; - } + content.CreatorId = nodeDto.UserId ?? Constants.Security.UnknownUserId; + content.WriterId = contentVersionDto.UserId ?? Constants.Security.UnknownUserId; + content.CreateDate = nodeDto.CreateDate; + content.UpdateDate = contentVersionDto.VersionDate; - // templates = ignored, managed by the repository + content.Published = dto.Published; + content.Edited = dto.Edited; - // reset dirty initial properties (U4-1946) - content.ResetDirtyProperties(false); - return content; - } - finally + // TODO: shall we get published infos or not? + //if (dto.Published) + if (publishedVersionDto != null) { - content.EnableChangeTracking(); + content.PublishedVersionId = publishedVersionDto.Id; + content.PublishDate = publishedVersionDto.ContentVersionDto.VersionDate; + content.PublishName = publishedVersionDto.ContentVersionDto.Text; + content.PublisherId = publishedVersionDto.ContentVersionDto.UserId; } - } - /// - /// Builds an IMedia item from a dto and content type. - /// - public static Core.Models.Media BuildEntity(ContentDto dto, IMediaType? contentType) + // templates = ignored, managed by the repository + + // reset dirty initial properties (U4-1946) + content.ResetDirtyProperties(false); + return content; + } + finally { - var nodeDto = dto.NodeDto; - var contentVersionDto = dto.ContentVersionDto; + content.EnableChangeTracking(); + } + } - var content = new Core.Models.Media(nodeDto.Text, nodeDto.ParentId, contentType); + /// + /// Builds an IMedia item from a dto and content type. + /// + public static Core.Models.Media BuildEntity(ContentDto dto, IMediaType? contentType) + { + NodeDto nodeDto = dto.NodeDto; + ContentVersionDto contentVersionDto = dto.ContentVersionDto; - try - { - content.DisableChangeTracking(); + var content = new Core.Models.Media(nodeDto.Text, nodeDto.ParentId, contentType); - content.Id = dto.NodeId; - content.Key = nodeDto.UniqueId; - content.VersionId = contentVersionDto.Id; + try + { + content.DisableChangeTracking(); - // TODO: missing names? + content.Id = dto.NodeId; + content.Key = nodeDto.UniqueId; + content.VersionId = contentVersionDto.Id; - content.Path = nodeDto.Path; - content.Level = nodeDto.Level; - content.ParentId = nodeDto.ParentId; - content.SortOrder = nodeDto.SortOrder; - content.Trashed = nodeDto.Trashed; + // TODO: missing names? - content.CreatorId = nodeDto.UserId ?? Cms.Core.Constants.Security.UnknownUserId; - content.WriterId = contentVersionDto.UserId ?? Cms.Core.Constants.Security.UnknownUserId; - content.CreateDate = nodeDto.CreateDate; - content.UpdateDate = contentVersionDto.VersionDate; + content.Path = nodeDto.Path; + content.Level = nodeDto.Level; + content.ParentId = nodeDto.ParentId; + content.SortOrder = nodeDto.SortOrder; + content.Trashed = nodeDto.Trashed; - // reset dirty initial properties (U4-1946) - content.ResetDirtyProperties(false); - return content; - } - finally - { - content.EnableChangeTracking(); - } - } + content.CreatorId = nodeDto.UserId ?? Constants.Security.UnknownUserId; + content.WriterId = contentVersionDto.UserId ?? Constants.Security.UnknownUserId; + content.CreateDate = nodeDto.CreateDate; + content.UpdateDate = contentVersionDto.VersionDate; - /// - /// Builds an IMedia item from a dto and content type. - /// - public static Member BuildEntity(MemberDto dto, IMemberType? contentType) + // reset dirty initial properties (U4-1946) + content.ResetDirtyProperties(false); + return content; + } + finally { - var nodeDto = dto.ContentDto.NodeDto; - var contentVersionDto = dto.ContentVersionDto; - - var content = new Member(nodeDto.Text, dto.Email, dto.LoginName, dto.Password, contentType); - - try - { - content.DisableChangeTracking(); - - content.Id = dto.NodeId; - content.SecurityStamp = dto.SecurityStampToken; - content.EmailConfirmedDate = dto.EmailConfirmedDate; - content.PasswordConfiguration = dto.PasswordConfig; - content.Key = nodeDto.UniqueId; - content.VersionId = contentVersionDto.Id; - - // TODO: missing names? - - content.Path = nodeDto.Path; - content.Level = nodeDto.Level; - content.ParentId = nodeDto.ParentId; - content.SortOrder = nodeDto.SortOrder; - content.Trashed = nodeDto.Trashed; - - content.CreatorId = nodeDto.UserId ?? Cms.Core.Constants.Security.UnknownUserId; - content.WriterId = contentVersionDto.UserId ?? Cms.Core.Constants.Security.UnknownUserId; - content.CreateDate = nodeDto.CreateDate; - content.UpdateDate = contentVersionDto.VersionDate; - content.FailedPasswordAttempts = dto.FailedPasswordAttempts ?? default; - content.IsLockedOut = dto.IsLockedOut; - content.IsApproved = dto.IsApproved; - content.LastLoginDate = dto.LastLoginDate; - content.LastLockoutDate = dto.LastLockoutDate; - content.LastPasswordChangeDate = dto.LastPasswordChangeDate; - - // reset dirty initial properties (U4-1946) - content.ResetDirtyProperties(false); - return content; - } - finally - { - content.EnableChangeTracking(); - } + content.EnableChangeTracking(); } + } - /// - /// Builds a dto from an IContent item. - /// - public static DocumentDto BuildDto(IContent entity, Guid objectType) - { - var contentDto = BuildContentDto(entity, objectType); + /// + /// Builds an IMedia item from a dto and content type. + /// + public static Member BuildEntity(MemberDto dto, IMemberType? contentType) + { + NodeDto nodeDto = dto.ContentDto.NodeDto; + ContentVersionDto contentVersionDto = dto.ContentVersionDto; - var dto = new DocumentDto - { - NodeId = entity.Id, - Published = entity.Published, - ContentDto = contentDto, - DocumentVersionDto = BuildDocumentVersionDto(entity, contentDto) - }; + var content = new Member(nodeDto.Text, dto.Email, dto.LoginName, dto.Password, contentType); - return dto; + try + { + content.DisableChangeTracking(); + + content.Id = dto.NodeId; + content.SecurityStamp = dto.SecurityStampToken; + content.EmailConfirmedDate = dto.EmailConfirmedDate; + content.PasswordConfiguration = dto.PasswordConfig; + content.Key = nodeDto.UniqueId; + content.VersionId = contentVersionDto.Id; + + // TODO: missing names? + + content.Path = nodeDto.Path; + content.Level = nodeDto.Level; + content.ParentId = nodeDto.ParentId; + content.SortOrder = nodeDto.SortOrder; + content.Trashed = nodeDto.Trashed; + + content.CreatorId = nodeDto.UserId ?? Constants.Security.UnknownUserId; + content.WriterId = contentVersionDto.UserId ?? Constants.Security.UnknownUserId; + content.CreateDate = nodeDto.CreateDate; + content.UpdateDate = contentVersionDto.VersionDate; + content.FailedPasswordAttempts = dto.FailedPasswordAttempts ?? default; + content.IsLockedOut = dto.IsLockedOut; + content.IsApproved = dto.IsApproved; + content.LastLoginDate = dto.LastLoginDate; + content.LastLockoutDate = dto.LastLockoutDate; + content.LastPasswordChangeDate = dto.LastPasswordChangeDate; + + // reset dirty initial properties (U4-1946) + content.ResetDirtyProperties(false); + return content; } + finally + { + content.EnableChangeTracking(); + } + } - public static IEnumerable<(ContentSchedule Model, ContentScheduleDto Dto)> BuildScheduleDto(IContent entity, ContentScheduleCollection contentSchedule, ILanguageRepository languageRepository) + /// + /// Builds a dto from an IContent item. + /// + public static DocumentDto BuildDto(IContent entity, Guid objectType) + { + ContentDto contentDto = BuildContentDto(entity, objectType); + + var dto = new DocumentDto { - return contentSchedule.FullSchedule.Select(x => - (x, new ContentScheduleDto + NodeId = entity.Id, + Published = entity.Published, + ContentDto = contentDto, + DocumentVersionDto = BuildDocumentVersionDto(entity, contentDto) + }; + + return dto; + } + + public static IEnumerable<(ContentSchedule Model, ContentScheduleDto Dto)> BuildScheduleDto(IContent entity, + ContentScheduleCollection contentSchedule, ILanguageRepository languageRepository) => + contentSchedule.FullSchedule.Select(x => + (x, + new ContentScheduleDto { Action = x.Action.ToString(), Date = x.Date, @@ -189,143 +188,136 @@ public static DocumentDto BuildDto(IContent entity, Guid objectType) LanguageId = languageRepository.GetIdByIsoCode(x.Culture, false), Id = x.Id })); - } - /// - /// Builds a dto from an IMedia item. - /// - public static MediaDto BuildDto(MediaUrlGeneratorCollection mediaUrlGenerators, IMedia entity) + /// + /// Builds a dto from an IMedia item. + /// + public static MediaDto BuildDto(MediaUrlGeneratorCollection mediaUrlGenerators, IMedia entity) + { + ContentDto contentDto = BuildContentDto(entity, Constants.ObjectTypes.Media); + + var dto = new MediaDto { - var contentDto = BuildContentDto(entity, Cms.Core.Constants.ObjectTypes.Media); + NodeId = entity.Id, + ContentDto = contentDto, + MediaVersionDto = BuildMediaVersionDto(mediaUrlGenerators, entity, contentDto) + }; - var dto = new MediaDto - { - NodeId = entity.Id, - ContentDto = contentDto, - MediaVersionDto = BuildMediaVersionDto(mediaUrlGenerators, entity, contentDto) - }; + return dto; + } - return dto; - } + /// + /// Builds a dto from an IMember item. + /// + public static MemberDto BuildDto(IMember entity) + { + ContentDto contentDto = BuildContentDto(entity, Constants.ObjectTypes.Member); - /// - /// Builds a dto from an IMember item. - /// - public static MemberDto BuildDto(IMember entity) + var dto = new MemberDto { - var contentDto = BuildContentDto(entity, Cms.Core.Constants.ObjectTypes.Member); - - var dto = new MemberDto - { - Email = entity.Email, - LoginName = entity.Username, - NodeId = entity.Id, - Password = entity.RawPasswordValue, - SecurityStampToken = entity.SecurityStamp, - EmailConfirmedDate = entity.EmailConfirmedDate, - ContentDto = contentDto, - ContentVersionDto = BuildContentVersionDto(entity, contentDto), - PasswordConfig = entity.PasswordConfiguration, - FailedPasswordAttempts = entity.FailedPasswordAttempts, - IsApproved = entity.IsApproved, - IsLockedOut = entity.IsLockedOut, - LastLockoutDate = entity.LastLockoutDate, - LastLoginDate = entity.LastLoginDate, - LastPasswordChangeDate = entity.LastPasswordChangeDate, - }; - return dto; - } + Email = entity.Email, + LoginName = entity.Username, + NodeId = entity.Id, + Password = entity.RawPasswordValue, + SecurityStampToken = entity.SecurityStamp, + EmailConfirmedDate = entity.EmailConfirmedDate, + ContentDto = contentDto, + ContentVersionDto = BuildContentVersionDto(entity, contentDto), + PasswordConfig = entity.PasswordConfiguration, + FailedPasswordAttempts = entity.FailedPasswordAttempts, + IsApproved = entity.IsApproved, + IsLockedOut = entity.IsLockedOut, + LastLockoutDate = entity.LastLockoutDate, + LastLoginDate = entity.LastLoginDate, + LastPasswordChangeDate = entity.LastPasswordChangeDate + }; + return dto; + } - private static ContentDto BuildContentDto(IContentBase entity, Guid objectType) + private static ContentDto BuildContentDto(IContentBase entity, Guid objectType) + { + var dto = new ContentDto { - var dto = new ContentDto - { - NodeId = entity.Id, - ContentTypeId = entity.ContentTypeId, + NodeId = entity.Id, ContentTypeId = entity.ContentTypeId, NodeDto = BuildNodeDto(entity, objectType) + }; - NodeDto = BuildNodeDto(entity, objectType) - }; + return dto; + } - return dto; - } + private static NodeDto BuildNodeDto(IContentBase entity, Guid objectType) + { + var dto = new NodeDto + { + NodeId = entity.Id, + UniqueId = entity.Key, + ParentId = entity.ParentId, + Level = Convert.ToInt16(entity.Level), + Path = entity.Path, + SortOrder = entity.SortOrder, + Trashed = entity.Trashed, + UserId = entity.CreatorId, + Text = entity.Name, + NodeObjectType = objectType, + CreateDate = entity.CreateDate + }; + + return dto; + } - private static NodeDto BuildNodeDto(IContentBase entity, Guid objectType) + // always build the current / VersionPk dto + // we're never going to build / save old versions (which are immutable) + private static ContentVersionDto BuildContentVersionDto(IContentBase entity, ContentDto contentDto) + { + var dto = new ContentVersionDto { - var dto = new NodeDto - { - NodeId = entity.Id, - UniqueId = entity.Key, - ParentId = entity.ParentId, - Level = Convert.ToInt16(entity.Level), - Path = entity.Path, - SortOrder = entity.SortOrder, - Trashed = entity.Trashed, - UserId = entity.CreatorId, - Text = entity.Name, - NodeObjectType = objectType, - CreateDate = entity.CreateDate - }; - - return dto; - } + Id = entity.VersionId, + NodeId = entity.Id, + VersionDate = entity.UpdateDate, + UserId = entity.WriterId, + Current = true, // always building the current one + Text = entity.Name, + ContentDto = contentDto + }; + + return dto; + } - // always build the current / VersionPk dto - // we're never going to build / save old versions (which are immutable) - private static ContentVersionDto BuildContentVersionDto(IContentBase entity, ContentDto contentDto) + // always build the current / VersionPk dto + // we're never going to build / save old versions (which are immutable) + private static DocumentVersionDto BuildDocumentVersionDto(IContent entity, ContentDto contentDto) + { + var dto = new DocumentVersionDto { - var dto = new ContentVersionDto - { - Id = entity.VersionId, - NodeId = entity.Id, - VersionDate = entity.UpdateDate, - UserId = entity.WriterId, - Current = true, // always building the current one - Text = entity.Name, + Id = entity.VersionId, + TemplateId = entity.TemplateId, + Published = false, // always building the current, unpublished one - ContentDto = contentDto - }; + ContentVersionDto = BuildContentVersionDto(entity, contentDto) + }; - return dto; - } + return dto; + } - // always build the current / VersionPk dto - // we're never going to build / save old versions (which are immutable) - private static DocumentVersionDto BuildDocumentVersionDto(IContent entity, ContentDto contentDto) - { - var dto = new DocumentVersionDto - { - Id = entity.VersionId, - TemplateId = entity.TemplateId, - Published = false, // always building the current, unpublished one + private static MediaVersionDto BuildMediaVersionDto(MediaUrlGeneratorCollection mediaUrlGenerators, IMedia entity, + ContentDto contentDto) + { + // try to get a path from the string being stored for media + // TODO: only considering umbracoFile - ContentVersionDto = BuildContentVersionDto(entity, contentDto) - }; + string? path = null; - return dto; + if (entity.Properties.TryGetValue(Constants.Conventions.Media.File, out IProperty? property) + && mediaUrlGenerators.TryGetMediaPath(property.PropertyType.PropertyEditorAlias, property.GetValue(), + out var mediaPath)) + { + path = mediaPath; } - private static MediaVersionDto BuildMediaVersionDto(MediaUrlGeneratorCollection mediaUrlGenerators, IMedia entity, ContentDto contentDto) + var dto = new MediaVersionDto { - // try to get a path from the string being stored for media - // TODO: only considering umbracoFile - - string? path = null; - - if (entity.Properties.TryGetValue(Cms.Core.Constants.Conventions.Media.File, out var property) - && mediaUrlGenerators.TryGetMediaPath(property.PropertyType.PropertyEditorAlias, property.GetValue(), out var mediaPath)) - { - path = mediaPath; - } + Id = entity.VersionId, Path = path, ContentVersionDto = BuildContentVersionDto(entity, contentDto) + }; - var dto = new MediaVersionDto - { - Id = entity.VersionId, - Path = path, - - ContentVersionDto = BuildContentVersionDto(entity, contentDto) - }; - - return dto; - } + return dto; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/ContentTypeFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/ContentTypeFactory.cs index a3a1deb62d52..5e3c1def4e60 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/ContentTypeFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/ContentTypeFactory.cs @@ -1,180 +1,189 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; +using System.Globalization; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +// factory for +// IContentType (document types) +// IMediaType (media types) +// IMemberType (member types) +// +internal static class ContentTypeFactory { - // factory for - // IContentType (document types) - // IMediaType (media types) - // IMemberType (member types) - // - internal static class ContentTypeFactory + #region IContentType + + public static IContentType BuildContentTypeEntity(IShortStringHelper shortStringHelper, ContentTypeDto dto) { - #region IContentType + var contentType = new ContentType(shortStringHelper, dto.NodeDto.ParentId); + + try + { + contentType.DisableChangeTracking(); + + BuildCommonEntity(contentType, dto); - public static IContentType BuildContentTypeEntity(IShortStringHelper shortStringHelper, ContentTypeDto dto) + // reset dirty initial properties (U4-1946) + contentType.ResetDirtyProperties(false); + return contentType; + } + finally { - var contentType = new ContentType(shortStringHelper, dto.NodeDto.ParentId); - - try - { - contentType.DisableChangeTracking(); - - BuildCommonEntity(contentType, dto); - - // reset dirty initial properties (U4-1946) - contentType.ResetDirtyProperties(false); - return contentType; - } - finally - { - contentType.EnableChangeTracking(); - } + contentType.EnableChangeTracking(); } + } - #endregion + #endregion - #region IMediaType + #region IMediaType - public static IMediaType BuildMediaTypeEntity(IShortStringHelper shortStringHelper, ContentTypeDto dto) + public static IMediaType BuildMediaTypeEntity(IShortStringHelper shortStringHelper, ContentTypeDto dto) + { + var contentType = new MediaType(shortStringHelper, dto.NodeDto.ParentId); + try { - var contentType = new MediaType(shortStringHelper, dto.NodeDto.ParentId); - try - { - contentType.DisableChangeTracking(); - - BuildCommonEntity(contentType, dto); + contentType.DisableChangeTracking(); - // reset dirty initial properties (U4-1946) - contentType.ResetDirtyProperties(false); - } - finally - { - contentType.EnableChangeTracking(); - } + BuildCommonEntity(contentType, dto); - return contentType; + // reset dirty initial properties (U4-1946) + contentType.ResetDirtyProperties(false); + } + finally + { + contentType.EnableChangeTracking(); } - #endregion + return contentType; + } - #region IMemberType + #endregion - public static IMemberType BuildMemberTypeEntity(IShortStringHelper shortStringHelper, ContentTypeDto dto) - { - var contentType = new MemberType(shortStringHelper, dto.NodeDto.ParentId); - try - { - contentType.DisableChangeTracking(); - BuildCommonEntity(contentType, dto, false); - contentType.ResetDirtyProperties(false); - } - finally - { - contentType.EnableChangeTracking(); - } + #region IMemberType - return contentType; + public static IMemberType BuildMemberTypeEntity(IShortStringHelper shortStringHelper, ContentTypeDto dto) + { + var contentType = new MemberType(shortStringHelper, dto.NodeDto.ParentId); + try + { + contentType.DisableChangeTracking(); + BuildCommonEntity(contentType, dto, false); + contentType.ResetDirtyProperties(false); + } + finally + { + contentType.EnableChangeTracking(); } - public static IEnumerable BuildMemberPropertyTypeDtos(IMemberType entity) + return contentType; + } + + public static IEnumerable BuildMemberPropertyTypeDtos(IMemberType entity) + { + var memberType = entity as MemberType; + if (memberType == null || memberType.PropertyTypes.Any() == false) { - var memberType = entity as MemberType; - if (memberType == null || memberType.PropertyTypes.Any() == false) - return Enumerable.Empty(); - - var dtos = memberType.PropertyTypes.Select(x => new MemberPropertyTypeDto - { - NodeId = entity.Id, - PropertyTypeId = x.Id, - CanEdit = memberType.MemberCanEditProperty(x.Alias), - ViewOnProfile = memberType.MemberCanViewProperty(x.Alias), - IsSensitive = memberType.IsSensitiveProperty(x.Alias) - }).ToList(); - return dtos; + return Enumerable.Empty(); } - #endregion + var dtos = memberType.PropertyTypes.Select(x => new MemberPropertyTypeDto + { + NodeId = entity.Id, + PropertyTypeId = x.Id, + CanEdit = memberType.MemberCanEditProperty(x.Alias), + ViewOnProfile = memberType.MemberCanViewProperty(x.Alias), + IsSensitive = memberType.IsSensitiveProperty(x.Alias) + }).ToList(); + return dtos; + } + + #endregion - #region Common + #region Common - private static void BuildCommonEntity(ContentTypeBase entity, ContentTypeDto dto, bool setVariations = true) + private static void BuildCommonEntity(ContentTypeBase entity, ContentTypeDto dto, bool setVariations = true) + { + entity.Id = dto.NodeDto.NodeId; + entity.Key = dto.NodeDto.UniqueId; + entity.Alias = dto.Alias ?? string.Empty; + entity.Name = dto.NodeDto.Text; + entity.Icon = dto.Icon; + entity.Thumbnail = dto.Thumbnail; + entity.SortOrder = dto.NodeDto.SortOrder; + entity.Description = dto.Description; + entity.CreateDate = dto.NodeDto.CreateDate; + entity.UpdateDate = dto.NodeDto.CreateDate; + entity.Path = dto.NodeDto.Path; + entity.Level = dto.NodeDto.Level; + entity.CreatorId = dto.NodeDto.UserId ?? Constants.Security.UnknownUserId; + entity.AllowedAsRoot = dto.AllowAtRoot; + entity.IsContainer = dto.IsContainer; + entity.IsElement = dto.IsElement; + entity.Trashed = dto.NodeDto.Trashed; + + if (setVariations) { - entity.Id = dto.NodeDto.NodeId; - entity.Key = dto.NodeDto.UniqueId; - entity.Alias = dto.Alias ?? string.Empty; - entity.Name = dto.NodeDto.Text; - entity.Icon = dto.Icon; - entity.Thumbnail = dto.Thumbnail; - entity.SortOrder = dto.NodeDto.SortOrder; - entity.Description = dto.Description; - entity.CreateDate = dto.NodeDto.CreateDate; - entity.UpdateDate = dto.NodeDto.CreateDate; - entity.Path = dto.NodeDto.Path; - entity.Level = dto.NodeDto.Level; - entity.CreatorId = dto.NodeDto.UserId ?? Cms.Core.Constants.Security.UnknownUserId; - entity.AllowedAsRoot = dto.AllowAtRoot; - entity.IsContainer = dto.IsContainer; - entity.IsElement = dto.IsElement; - entity.Trashed = dto.NodeDto.Trashed; - - if (setVariations) - entity.Variations = (ContentVariation) dto.Variations; + entity.Variations = (ContentVariation)dto.Variations; } + } - public static ContentTypeDto BuildContentTypeDto(IContentTypeBase entity) + public static ContentTypeDto BuildContentTypeDto(IContentTypeBase entity) + { + Guid nodeObjectType; + if (entity is IContentType) { - Guid nodeObjectType; - if (entity is IContentType) - nodeObjectType = Cms.Core.Constants.ObjectTypes.DocumentType; - else if (entity is IMediaType) - nodeObjectType = Cms.Core.Constants.ObjectTypes.MediaType; - else if (entity is IMemberType) - nodeObjectType = Cms.Core.Constants.ObjectTypes.MemberType; - else - throw new Exception("Invalid entity."); - - var contentTypeDto = new ContentTypeDto - { - Alias = entity.Alias, - Description = entity.Description, - Icon = entity.Icon, - Thumbnail = entity.Thumbnail, - NodeId = entity.Id, - AllowAtRoot = entity.AllowedAsRoot, - IsContainer = entity.IsContainer, - IsElement = entity.IsElement, - Variations = (byte) entity.Variations, - NodeDto = BuildNodeDto(entity, nodeObjectType) - }; - return contentTypeDto; + nodeObjectType = Constants.ObjectTypes.DocumentType; } - - private static NodeDto BuildNodeDto(IUmbracoEntity entity, Guid nodeObjectType) + else if (entity is IMediaType) { - var nodeDto = new NodeDto - { - CreateDate = entity.CreateDate, - NodeId = entity.Id, - Level = short.Parse(entity.Level.ToString(CultureInfo.InvariantCulture)), - NodeObjectType = nodeObjectType, - ParentId = entity.ParentId, - Path = entity.Path, - SortOrder = entity.SortOrder, - Text = entity.Name, - Trashed = false, - UniqueId = entity.Key, - UserId = entity.CreatorId - }; - return nodeDto; + nodeObjectType = Constants.ObjectTypes.MediaType; } + else if (entity is IMemberType) + { + nodeObjectType = Constants.ObjectTypes.MemberType; + } + else + { + throw new Exception("Invalid entity."); + } + + var contentTypeDto = new ContentTypeDto + { + Alias = entity.Alias, + Description = entity.Description, + Icon = entity.Icon, + Thumbnail = entity.Thumbnail, + NodeId = entity.Id, + AllowAtRoot = entity.AllowedAsRoot, + IsContainer = entity.IsContainer, + IsElement = entity.IsElement, + Variations = (byte)entity.Variations, + NodeDto = BuildNodeDto(entity, nodeObjectType) + }; + return contentTypeDto; + } - #endregion + private static NodeDto BuildNodeDto(IUmbracoEntity entity, Guid nodeObjectType) + { + var nodeDto = new NodeDto + { + CreateDate = entity.CreateDate, + NodeId = entity.Id, + Level = short.Parse(entity.Level.ToString(CultureInfo.InvariantCulture)), + NodeObjectType = nodeObjectType, + ParentId = entity.ParentId, + Path = entity.Path, + SortOrder = entity.SortOrder, + Text = entity.Name, + Trashed = false, + UniqueId = entity.Key, + UserId = entity.CreatorId + }; + return nodeDto; } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/DataTypeFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/DataTypeFactory.cs index df655d3ade93..20018752b898 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/DataTypeFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/DataTypeFactory.cs @@ -1,91 +1,91 @@ -using System; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class DataTypeFactory { - internal static class DataTypeFactory + public static IDataType BuildEntity(DataTypeDto dto, PropertyEditorCollection editors, ILogger logger, + IConfigurationEditorJsonSerializer serializer) { - public static IDataType BuildEntity(DataTypeDto dto, PropertyEditorCollection editors, ILogger logger, IConfigurationEditorJsonSerializer serializer) + // Check we have an editor for the data type. + if (!editors.TryGet(dto.EditorAlias, out IDataEditor? editor)) { - // Check we have an editor for the data type. - if (!editors.TryGet(dto.EditorAlias, out var editor)) - { - logger.LogWarning("Could not find an editor with alias {EditorAlias}, treating as Label. " + - "The site may fail to boot and/or load data types and run.", dto.EditorAlias); + logger.LogWarning("Could not find an editor with alias {EditorAlias}, treating as Label. " + + "The site may fail to boot and/or load data types and run.", dto.EditorAlias); - // Create as special type, which downstream can be handled by converting to a LabelPropertyEditor to make clear - // the situation to the user. - editor = new MissingPropertyEditor(); - } + // Create as special type, which downstream can be handled by converting to a LabelPropertyEditor to make clear + // the situation to the user. + editor = new MissingPropertyEditor(); + } - var dataType = new DataType(editor, serializer); + var dataType = new DataType(editor, serializer); - try - { - dataType.DisableChangeTracking(); + try + { + dataType.DisableChangeTracking(); - dataType.CreateDate = dto.NodeDto.CreateDate; - dataType.DatabaseType = dto.DbType.EnumParse(true); - dataType.Id = dto.NodeId; - dataType.Key = dto.NodeDto.UniqueId; - dataType.Level = dto.NodeDto.Level; - dataType.UpdateDate = dto.NodeDto.CreateDate; - dataType.Name = dto.NodeDto.Text; - dataType.ParentId = dto.NodeDto.ParentId; - dataType.Path = dto.NodeDto.Path; - dataType.SortOrder = dto.NodeDto.SortOrder; - dataType.Trashed = dto.NodeDto.Trashed; - dataType.CreatorId = dto.NodeDto.UserId ?? Cms.Core.Constants.Security.UnknownUserId; + dataType.CreateDate = dto.NodeDto.CreateDate; + dataType.DatabaseType = dto.DbType.EnumParse(true); + dataType.Id = dto.NodeId; + dataType.Key = dto.NodeDto.UniqueId; + dataType.Level = dto.NodeDto.Level; + dataType.UpdateDate = dto.NodeDto.CreateDate; + dataType.Name = dto.NodeDto.Text; + dataType.ParentId = dto.NodeDto.ParentId; + dataType.Path = dto.NodeDto.Path; + dataType.SortOrder = dto.NodeDto.SortOrder; + dataType.Trashed = dto.NodeDto.Trashed; + dataType.CreatorId = dto.NodeDto.UserId ?? Constants.Security.UnknownUserId; - dataType.SetLazyConfiguration(dto.Configuration); + dataType.SetLazyConfiguration(dto.Configuration); - // reset dirty initial properties (U4-1946) - dataType.ResetDirtyProperties(false); - return dataType; - } - finally - { - dataType.EnableChangeTracking(); - } + // reset dirty initial properties (U4-1946) + dataType.ResetDirtyProperties(false); + return dataType; } + finally + { + dataType.EnableChangeTracking(); + } + } - public static DataTypeDto BuildDto(IDataType entity, IConfigurationEditorJsonSerializer serializer) + public static DataTypeDto BuildDto(IDataType entity, IConfigurationEditorJsonSerializer serializer) + { + var dataTypeDto = new DataTypeDto { - var dataTypeDto = new DataTypeDto - { - EditorAlias = entity.EditorAlias, - NodeId = entity.Id, - DbType = entity.DatabaseType.ToString(), - Configuration = ConfigurationEditor.ToDatabase(entity.Configuration, serializer), - NodeDto = BuildNodeDto(entity) - }; + EditorAlias = entity.EditorAlias, + NodeId = entity.Id, + DbType = entity.DatabaseType.ToString(), + Configuration = ConfigurationEditor.ToDatabase(entity.Configuration, serializer), + NodeDto = BuildNodeDto(entity) + }; - return dataTypeDto; - } + return dataTypeDto; + } - private static NodeDto BuildNodeDto(IDataType entity) + private static NodeDto BuildNodeDto(IDataType entity) + { + var nodeDto = new NodeDto { - var nodeDto = new NodeDto - { - CreateDate = entity.CreateDate, - NodeId = entity.Id, - Level = Convert.ToInt16(entity.Level), - NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, - ParentId = entity.ParentId, - Path = entity.Path, - SortOrder = entity.SortOrder, - Text = entity.Name, - Trashed = entity.Trashed, - UniqueId = entity.Key, - UserId = entity.CreatorId - }; + CreateDate = entity.CreateDate, + NodeId = entity.Id, + Level = Convert.ToInt16(entity.Level), + NodeObjectType = Constants.ObjectTypes.DataType, + ParentId = entity.ParentId, + Path = entity.Path, + SortOrder = entity.SortOrder, + Text = entity.Name, + Trashed = entity.Trashed, + UniqueId = entity.Key, + UserId = entity.CreatorId + }; - return nodeDto; - } + return nodeDto; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/DictionaryItemFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/DictionaryItemFactory.cs index 31dc7ef2ec3f..418a6a6a122f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/DictionaryItemFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/DictionaryItemFactory.cs @@ -1,70 +1,66 @@ -using System.Collections.Generic; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class DictionaryItemFactory { - internal static class DictionaryItemFactory + private static List BuildLanguageTextDtos(IDictionaryItem entity) { - #region Implementation of IEntityFactory - - public static IDictionaryItem BuildEntity(DictionaryDto dto) + var list = new List(); + if (entity.Translations is not null) { - var item = new DictionaryItem(dto.Parent, dto.Key); - - try + foreach (IDictionaryTranslation translation in entity.Translations) { - item.DisableChangeTracking(); + var text = new LanguageTextDto + { + LanguageId = translation.LanguageId, UniqueId = translation.Key, Value = translation.Value! + }; - item.Id = dto.PrimaryKey; - item.Key = dto.UniqueId; + if (translation.HasIdentity) + { + text.PrimaryKey = translation.Id; + } - // reset dirty initial properties (U4-1946) - item.ResetDirtyProperties(false); - return item; - } - finally - { - item.EnableChangeTracking(); + list.Add(text); } } - public static DictionaryDto BuildDto(IDictionaryItem entity) - { - return new DictionaryDto - { - UniqueId = entity.Key, - Key = entity.ItemKey, - Parent = entity.ParentId, - PrimaryKey = entity.Id, - LanguageTextDtos = BuildLanguageTextDtos(entity) - }; - } + return list; + } - #endregion + #region Implementation of IEntityFactory - private static List BuildLanguageTextDtos(IDictionaryItem entity) - { - var list = new List(); - if (entity.Translations is not null) - { - foreach (var translation in entity.Translations) - { - var text = new LanguageTextDto - { - LanguageId = translation.LanguageId, - UniqueId = translation.Key, - Value = translation.Value!, - }; + public static IDictionaryItem BuildEntity(DictionaryDto dto) + { + var item = new DictionaryItem(dto.Parent, dto.Key); - if (translation.HasIdentity) - text.PrimaryKey = translation.Id; + try + { + item.DisableChangeTracking(); - list.Add(text); - } - } + item.Id = dto.PrimaryKey; + item.Key = dto.UniqueId; - return list; + // reset dirty initial properties (U4-1946) + item.ResetDirtyProperties(false); + return item; + } + finally + { + item.EnableChangeTracking(); } } + + public static DictionaryDto BuildDto(IDictionaryItem entity) => + new DictionaryDto + { + UniqueId = entity.Key, + Key = entity.ItemKey, + Parent = entity.ParentId, + PrimaryKey = entity.Id, + LanguageTextDtos = BuildLanguageTextDtos(entity) + }; + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/DictionaryTranslationFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/DictionaryTranslationFactory.cs index a53222ad5eb4..acfaad8666f6 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/DictionaryTranslationFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/DictionaryTranslationFactory.cs @@ -1,48 +1,43 @@ -using System; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class DictionaryTranslationFactory { - internal static class DictionaryTranslationFactory + #region Implementation of IEntityFactory + + public static IDictionaryTranslation BuildEntity(LanguageTextDto dto, Guid uniqueId) { - #region Implementation of IEntityFactory + var item = new DictionaryTranslation(dto.LanguageId, dto.Value, uniqueId); - public static IDictionaryTranslation BuildEntity(LanguageTextDto dto, Guid uniqueId) + try { - var item = new DictionaryTranslation(dto.LanguageId, dto.Value, uniqueId); - - try - { - item.DisableChangeTracking(); - - item.Id = dto.PrimaryKey; - - // reset dirty initial properties (U4-1946) - item.ResetDirtyProperties(false); - return item; - } - finally - { - item.EnableChangeTracking(); - } - } + item.DisableChangeTracking(); - public static LanguageTextDto BuildDto(IDictionaryTranslation entity, Guid uniqueId) + item.Id = dto.PrimaryKey; + + // reset dirty initial properties (U4-1946) + item.ResetDirtyProperties(false); + return item; + } + finally { - var text = new LanguageTextDto - { - LanguageId = entity.LanguageId, - UniqueId = uniqueId, - Value = entity.Value - }; + item.EnableChangeTracking(); + } + } - if (entity.HasIdentity) - text.PrimaryKey = entity.Id; + public static LanguageTextDto BuildDto(IDictionaryTranslation entity, Guid uniqueId) + { + var text = new LanguageTextDto {LanguageId = entity.LanguageId, UniqueId = uniqueId, Value = entity.Value}; - return text; + if (entity.HasIdentity) + { + text.PrimaryKey = entity.Id; } - #endregion + return text; } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/ExternalLoginFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/ExternalLoginFactory.cs index 1c74dcb8bd9b..5849d677ba44 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/ExternalLoginFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/ExternalLoginFactory.cs @@ -1,81 +1,79 @@ -using System; -using System.Globalization; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class ExternalLoginFactory { - internal static class ExternalLoginFactory + public static IIdentityUserToken BuildEntity(ExternalLoginTokenDto dto) { - public static IIdentityUserToken BuildEntity(ExternalLoginTokenDto dto) - { - var entity = new IdentityUserToken(dto.Id, dto.ExternalLoginDto.LoginProvider, dto.Name, dto.Value, dto.ExternalLoginDto.UserOrMemberKey.ToString(), dto.CreateDate); - - // reset dirty initial properties (U4-1946) - entity.ResetDirtyProperties(false); - return entity; - } + var entity = new IdentityUserToken(dto.Id, dto.ExternalLoginDto.LoginProvider, dto.Name, dto.Value, + dto.ExternalLoginDto.UserOrMemberKey.ToString(), dto.CreateDate); - public static IIdentityUserLogin BuildEntity(ExternalLoginDto dto) - { + // reset dirty initial properties (U4-1946) + entity.ResetDirtyProperties(false); + return entity; + } - //If there exists a UserId - this means the database is still not migrated. E.g on the upgrade state. - //At this point we have to manually set the key, to ensure external logins can be used to upgrade - var key = dto.UserId.HasValue ? dto.UserId.Value.ToGuid().ToString() : dto.UserOrMemberKey.ToString(); + public static IIdentityUserLogin BuildEntity(ExternalLoginDto dto) + { + //If there exists a UserId - this means the database is still not migrated. E.g on the upgrade state. + //At this point we have to manually set the key, to ensure external logins can be used to upgrade + var key = dto.UserId.HasValue ? dto.UserId.Value.ToGuid().ToString() : dto.UserOrMemberKey.ToString(); - var entity = new IdentityUserLogin(dto.Id, dto.LoginProvider, dto.ProviderKey, key, dto.CreateDate) + var entity = + new IdentityUserLogin(dto.Id, dto.LoginProvider, dto.ProviderKey, key, dto.CreateDate) { UserData = dto.UserData }; - // reset dirty initial properties (U4-1946) - entity.ResetDirtyProperties(false); - return entity; - } + // reset dirty initial properties (U4-1946) + entity.ResetDirtyProperties(false); + return entity; + } - public static ExternalLoginDto BuildDto(IIdentityUserLogin entity) + public static ExternalLoginDto BuildDto(IIdentityUserLogin entity) + { + var dto = new ExternalLoginDto { - var dto = new ExternalLoginDto - { - Id = entity.Id, - CreateDate = entity.CreateDate, - LoginProvider = entity.LoginProvider, - ProviderKey = entity.ProviderKey, - UserOrMemberKey = entity.Key, - UserData = entity.UserData - }; + Id = entity.Id, + CreateDate = entity.CreateDate, + LoginProvider = entity.LoginProvider, + ProviderKey = entity.ProviderKey, + UserOrMemberKey = entity.Key, + UserData = entity.UserData + }; - return dto; - } + return dto; + } - public static ExternalLoginDto BuildDto(Guid userOrMemberKey, IExternalLogin entity, int? id = null) + public static ExternalLoginDto BuildDto(Guid userOrMemberKey, IExternalLogin entity, int? id = null) + { + var dto = new ExternalLoginDto { - var dto = new ExternalLoginDto - { - Id = id ?? default, - UserOrMemberKey = userOrMemberKey, - LoginProvider = entity.LoginProvider, - ProviderKey = entity.ProviderKey, - UserData = entity.UserData, - CreateDate = DateTime.Now - }; + Id = id ?? default, + UserOrMemberKey = userOrMemberKey, + LoginProvider = entity.LoginProvider, + ProviderKey = entity.ProviderKey, + UserData = entity.UserData, + CreateDate = DateTime.Now + }; - return dto; - } + return dto; + } - public static ExternalLoginTokenDto BuildDto(int externalLoginId, IExternalLoginToken token, int? id = null) + public static ExternalLoginTokenDto BuildDto(int externalLoginId, IExternalLoginToken token, int? id = null) + { + var dto = new ExternalLoginTokenDto { - var dto = new ExternalLoginTokenDto - { - Id = id ?? default, - ExternalLoginId = externalLoginId, - Name = token.Name, - Value = token.Value, - CreateDate = DateTime.Now - }; + Id = id ?? default, + ExternalLoginId = externalLoginId, + Name = token.Name, + Value = token.Value, + CreateDate = DateTime.Now + }; - return dto; - } + return dto; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/LanguageFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/LanguageFactory.cs index 2c7c6c081e92..7d729e0a946c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/LanguageFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/LanguageFactory.cs @@ -1,51 +1,50 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class LanguageFactory { - internal static class LanguageFactory + public static ILanguage BuildEntity(LanguageDto dto) { - public static ILanguage BuildEntity(LanguageDto dto) + ArgumentNullException.ThrowIfNull(dto); + if (dto.IsoCode == null || dto.CultureName == null) { - ArgumentNullException.ThrowIfNull(dto); - if (dto.IsoCode == null || dto.CultureName == null) - { - throw new InvalidOperationException("Language ISO code and/or culture name can't be null."); - } - - var lang = new Language(dto.IsoCode, dto.CultureName) - { - Id = dto.Id, - IsDefault = dto.IsDefault, - IsMandatory = dto.IsMandatory, - FallbackLanguageId = dto.FallbackLanguageId - }; - - // Reset dirty initial properties - lang.ResetDirtyProperties(false); - - return lang; + throw new InvalidOperationException("Language ISO code and/or culture name can't be null."); } - public static LanguageDto BuildDto(ILanguage entity) + var lang = new Language(dto.IsoCode, dto.CultureName) + { + Id = dto.Id, + IsDefault = dto.IsDefault, + IsMandatory = dto.IsMandatory, + FallbackLanguageId = dto.FallbackLanguageId + }; + + // Reset dirty initial properties + lang.ResetDirtyProperties(false); + + return lang; + } + + public static LanguageDto BuildDto(ILanguage entity) + { + ArgumentNullException.ThrowIfNull(entity); + + var dto = new LanguageDto { - ArgumentNullException.ThrowIfNull(entity); - - var dto = new LanguageDto - { - IsoCode = entity.IsoCode, - CultureName = entity.CultureName, - IsDefault = entity.IsDefault, - IsMandatory = entity.IsMandatory, - FallbackLanguageId = entity.FallbackLanguageId - }; - - if (entity.HasIdentity) - { - dto.Id = (short)entity.Id; - } - - return dto; + IsoCode = entity.IsoCode, + CultureName = entity.CultureName, + IsDefault = entity.IsDefault, + IsMandatory = entity.IsMandatory, + FallbackLanguageId = entity.FallbackLanguageId + }; + + if (entity.HasIdentity) + { + dto.Id = (short)entity.Id; } + + return dto; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/MacroFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/MacroFactory.cs index 7f73abacaa59..4fdc92962fe5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/MacroFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/MacroFactory.cs @@ -1,79 +1,80 @@ -using System.Collections.Generic; -using System.Globalization; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class MacroFactory { - internal static class MacroFactory + public static IMacro BuildEntity(IShortStringHelper shortStringHelper, MacroDto dto) { - public static IMacro BuildEntity(IShortStringHelper shortStringHelper, MacroDto dto) - { - var model = new Macro(shortStringHelper, dto.Id, dto.UniqueId, dto.UseInEditor, dto.RefreshRate, dto.Alias, dto.Name, dto.CacheByPage, dto.CachePersonalized, dto.DontRender, dto.MacroSource); + var model = new Macro(shortStringHelper, dto.Id, dto.UniqueId, dto.UseInEditor, dto.RefreshRate, dto.Alias, + dto.Name, dto.CacheByPage, dto.CachePersonalized, dto.DontRender, dto.MacroSource); - try - { - model.DisableChangeTracking(); - - foreach (var p in dto.MacroPropertyDtos.EmptyNull()) - { - model.Properties.Add(new MacroProperty(p.Id, p.UniqueId, p.Alias, p.Name, p.SortOrder, p.EditorAlias)); - } + try + { + model.DisableChangeTracking(); - // reset dirty initial properties (U4-1946) - model.ResetDirtyProperties(false); - return model; - } - finally + foreach (MacroPropertyDto p in dto.MacroPropertyDtos.EmptyNull()) { - model.EnableChangeTracking(); + model.Properties.Add(new MacroProperty(p.Id, p.UniqueId, p.Alias, p.Name, p.SortOrder, p.EditorAlias)); } - } - public static MacroDto BuildDto(IMacro entity) + // reset dirty initial properties (U4-1946) + model.ResetDirtyProperties(false); + return model; + } + finally { - var dto = new MacroDto - { - UniqueId = entity.Key, - Alias = entity.Alias, - CacheByPage = entity.CacheByPage, - CachePersonalized = entity.CacheByMember, - DontRender = entity.DontRender, - Name = entity.Name, - MacroSource = entity.MacroSource, - RefreshRate = entity.CacheDuration, - UseInEditor = entity.UseInEditor, - MacroPropertyDtos = BuildPropertyDtos(entity), - MacroType = 7 //PartialView - }; + model.EnableChangeTracking(); + } + } - if (entity.HasIdentity) - dto.Id = entity.Id; + public static MacroDto BuildDto(IMacro entity) + { + var dto = new MacroDto + { + UniqueId = entity.Key, + Alias = entity.Alias, + CacheByPage = entity.CacheByPage, + CachePersonalized = entity.CacheByMember, + DontRender = entity.DontRender, + Name = entity.Name, + MacroSource = entity.MacroSource, + RefreshRate = entity.CacheDuration, + UseInEditor = entity.UseInEditor, + MacroPropertyDtos = BuildPropertyDtos(entity), + MacroType = 7 //PartialView + }; - return dto; + if (entity.HasIdentity) + { + dto.Id = entity.Id; } - private static List BuildPropertyDtos(IMacro entity) + return dto; + } + + private static List BuildPropertyDtos(IMacro entity) + { + var list = new List(); + foreach (IMacroProperty p in entity.Properties) { - var list = new List(); - foreach (var p in entity.Properties) + var text = new MacroPropertyDto { - var text = new MacroPropertyDto - { - UniqueId = p.Key, - Alias = p.Alias, - Name = p.Name, - Macro = entity.Id, - SortOrder = (byte)p.SortOrder, - EditorAlias = p.EditorAlias, - Id = p.Id - }; + UniqueId = p.Key, + Alias = p.Alias, + Name = p.Name, + Macro = entity.Id, + SortOrder = (byte)p.SortOrder, + EditorAlias = p.EditorAlias, + Id = p.Id + }; - list.Add(text); - } - return list; + list.Add(text); } + + return list; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/MemberGroupFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/MemberGroupFactory.cs index d3ddf40ce33a..fd158204a9a4 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/MemberGroupFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/MemberGroupFactory.cs @@ -1,71 +1,65 @@ -using System; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class MemberGroupFactory { - internal static class MemberGroupFactory - { + private static readonly Guid _nodeObjectTypeId; - private static readonly Guid _nodeObjectTypeId; + static MemberGroupFactory() => _nodeObjectTypeId = Constants.ObjectTypes.MemberGroup; - static MemberGroupFactory() - { - _nodeObjectTypeId = Cms.Core.Constants.ObjectTypes.MemberGroup; - } + #region Implementation of IEntityFactory - #region Implementation of IEntityFactory + public static IMemberGroup BuildEntity(NodeDto dto) + { + var group = new MemberGroup(); - public static IMemberGroup BuildEntity(NodeDto dto) + try { - var group = new MemberGroup(); - - try - { - group.DisableChangeTracking(); + group.DisableChangeTracking(); - group.CreateDate = dto.CreateDate; - group.Id = dto.NodeId; - group.Key = dto.UniqueId; - group.Name = dto.Text; + group.CreateDate = dto.CreateDate; + group.Id = dto.NodeId; + group.Key = dto.UniqueId; + group.Name = dto.Text; - // reset dirty initial properties (U4-1946) - group.ResetDirtyProperties(false); - return group; - } - finally - { - group.EnableChangeTracking(); - } + // reset dirty initial properties (U4-1946) + group.ResetDirtyProperties(false); + return group; } - - public static NodeDto BuildDto(IMemberGroup entity) + finally { - var dto = new NodeDto - { - CreateDate = entity.CreateDate, - NodeId = entity.Id, - Level = 0, - NodeObjectType = _nodeObjectTypeId, - ParentId = -1, - Path = "", - SortOrder = 0, - Text = entity.Name, - Trashed = false, - UniqueId = entity.Key, - UserId = entity.CreatorId - }; + group.EnableChangeTracking(); + } + } - if (entity.HasIdentity) - { - dto.NodeId = entity.Id; - dto.Path = "-1," + entity.Id; - } + public static NodeDto BuildDto(IMemberGroup entity) + { + var dto = new NodeDto + { + CreateDate = entity.CreateDate, + NodeId = entity.Id, + Level = 0, + NodeObjectType = _nodeObjectTypeId, + ParentId = -1, + Path = "", + SortOrder = 0, + Text = entity.Name, + Trashed = false, + UniqueId = entity.Key, + UserId = entity.CreatorId + }; - return dto; + if (entity.HasIdentity) + { + dto.NodeId = entity.Id; + dto.Path = "-1," + entity.Id; } - #endregion - + return dto; } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/PropertyFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/PropertyFactory.cs index 1b8708c64013..23841f345f80 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/PropertyFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/PropertyFactory.cs @@ -1,193 +1,223 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; +using System.Globalization; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class PropertyFactory { - internal static class PropertyFactory + public static IEnumerable BuildEntities(IPropertyType[]? propertyTypes, + IReadOnlyCollection dtos, int publishedVersionId, ILanguageRepository languageRepository) { - public static IEnumerable BuildEntities(IPropertyType[]? propertyTypes, IReadOnlyCollection dtos, int publishedVersionId, ILanguageRepository languageRepository) + var properties = new List(); + var xdtos = dtos.GroupBy(x => x.PropertyTypeId).ToDictionary(x => x.Key, x => (IEnumerable)x); + + if (propertyTypes is null) { - var properties = new List(); - var xdtos = dtos.GroupBy(x => x.PropertyTypeId).ToDictionary(x => x.Key, x => (IEnumerable)x); + return properties; + } - if (propertyTypes is null) - { - return properties; - } - foreach (var propertyType in propertyTypes) - { - var values = new List(); - int propertyId = default; + foreach (IPropertyType propertyType in propertyTypes) + { + var values = new List(); + int propertyId = default; - // see notes in BuildDtos - we always have edit+published dtos - if (xdtos.TryGetValue(propertyType.Id, out var propDtos)) + // see notes in BuildDtos - we always have edit+published dtos + if (xdtos.TryGetValue(propertyType.Id, out IEnumerable? propDtos)) + { + foreach (PropertyDataDto propDto in propDtos) { - foreach (var propDto in propDtos) - { - propertyId = propDto.Id; - values.Add(new Property.InitialPropertyValue(languageRepository.GetIsoCodeById(propDto.LanguageId), propDto.Segment, propDto.VersionId == publishedVersionId, propDto.Value)); - } + propertyId = propDto.Id; + values.Add(new Property.InitialPropertyValue(languageRepository.GetIsoCodeById(propDto.LanguageId), + propDto.Segment, propDto.VersionId == publishedVersionId, propDto.Value)); } - - var property = Property.CreateWithValues(propertyId, propertyType, values.ToArray()); - properties.Add(property); } - return properties; + var property = Property.CreateWithValues(propertyId, propertyType, values.ToArray()); + properties.Add(property); } - private static PropertyDataDto BuildDto(int versionId, IProperty property, int? languageId, string? segment, object? value) - { - var dto = new PropertyDataDto { VersionId = versionId, PropertyTypeId = property.PropertyTypeId }; + return properties; + } - if (languageId.HasValue) - dto.LanguageId = languageId; + private static PropertyDataDto BuildDto(int versionId, IProperty property, int? languageId, string? segment, + object? value) + { + var dto = new PropertyDataDto {VersionId = versionId, PropertyTypeId = property.PropertyTypeId}; - if (segment != null) - dto.Segment = segment; + if (languageId.HasValue) + { + dto.LanguageId = languageId; + } - if (property.ValueStorageType == ValueStorageType.Integer) - { - if (value is bool || property.PropertyType.PropertyEditorAlias == Cms.Core.Constants.PropertyEditors.Aliases.Boolean) - { - dto.IntegerValue = value != null && string.IsNullOrEmpty(value.ToString()) ? 0 : Convert.ToInt32(value); - } - else if (value != null && string.IsNullOrWhiteSpace(value.ToString()) == false && int.TryParse(value.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) - { - dto.IntegerValue = val; - } - } - else if (property.ValueStorageType == ValueStorageType.Decimal && value != null) + if (segment != null) + { + dto.Segment = segment; + } + + if (property.ValueStorageType == ValueStorageType.Integer) + { + if (value is bool || property.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.Boolean) { - if (decimal.TryParse(value.ToString(), out var val)) - { - dto.DecimalValue = val; // property value should be normalized already - } + dto.IntegerValue = value != null && string.IsNullOrEmpty(value.ToString()) ? 0 : Convert.ToInt32(value); } - else if (property.ValueStorageType == ValueStorageType.Date && value != null && string.IsNullOrWhiteSpace(value.ToString()) == false) + else if (value != null && string.IsNullOrWhiteSpace(value.ToString()) == false && + int.TryParse(value.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) { - if (DateTime.TryParse(value.ToString(), out var date)) - { - dto.DateValue = date; - } + dto.IntegerValue = val; } - else if (property.ValueStorageType == ValueStorageType.Ntext && value != null) + } + else if (property.ValueStorageType == ValueStorageType.Decimal && value != null) + { + if (decimal.TryParse(value.ToString(), out var val)) { - dto.TextValue = value.ToString(); + dto.DecimalValue = val; // property value should be normalized already } - else if (property.ValueStorageType == ValueStorageType.Nvarchar && value != null) + } + else if (property.ValueStorageType == ValueStorageType.Date && value != null && + string.IsNullOrWhiteSpace(value.ToString()) == false) + { + if (DateTime.TryParse(value.ToString(), out DateTime date)) { - dto.VarcharValue = value.ToString(); + dto.DateValue = date; } - - return dto; } - - /// - /// Creates a collection of from a collection of - /// - /// - /// The of the entity containing the collection of - /// - /// - /// - /// The properties to map - /// - /// out parameter indicating that one or more properties have been edited - /// - /// Out parameter containing a collection of edited cultures when the contentVariation varies by culture. - /// The value of this will be used to populate the edited cultures in the umbracoDocumentCultureVariation table. - /// - /// - public static IEnumerable BuildDtos(ContentVariation contentVariation, int currentVersionId, int publishedVersionId, IEnumerable properties, - ILanguageRepository languageRepository, out bool edited, - out HashSet? editedCultures) + else if (property.ValueStorageType == ValueStorageType.Ntext && value != null) + { + dto.TextValue = value.ToString(); + } + else if (property.ValueStorageType == ValueStorageType.Nvarchar && value != null) { - var propertyDataDtos = new List(); - edited = false; - editedCultures = null; // don't allocate unless necessary - string? defaultCulture = null; //don't allocate unless necessary + dto.VarcharValue = value.ToString(); + } - var entityVariesByCulture = contentVariation.VariesByCulture(); + return dto; + } + + /// + /// Creates a collection of from a collection of + /// + /// + /// The of the entity containing the collection of + /// + /// + /// + /// The properties to map + /// + /// out parameter indicating that one or more properties have been edited + /// + /// Out parameter containing a collection of edited cultures when the contentVariation varies by culture. + /// The value of this will be used to populate the edited cultures in the umbracoDocumentCultureVariation table. + /// + /// + public static IEnumerable BuildDtos(ContentVariation contentVariation, int currentVersionId, + int publishedVersionId, IEnumerable properties, + ILanguageRepository languageRepository, out bool edited, + out HashSet? editedCultures) + { + var propertyDataDtos = new List(); + edited = false; + editedCultures = null; // don't allocate unless necessary + string? defaultCulture = null; //don't allocate unless necessary + + var entityVariesByCulture = contentVariation.VariesByCulture(); - // create dtos for each property values, but only for values that do actually exist - // ie have a non-null value, everything else is just ignored and won't have a db row + // create dtos for each property values, but only for values that do actually exist + // ie have a non-null value, everything else is just ignored and won't have a db row - foreach (var property in properties) + foreach (IProperty property in properties) + { + if (property.PropertyType.SupportsPublishing) { - if (property.PropertyType.SupportsPublishing) + //create the resulting hashset if it's not created and the entity varies by culture + if (entityVariesByCulture && editedCultures == null) { - //create the resulting hashset if it's not created and the entity varies by culture - if (entityVariesByCulture && editedCultures == null) - editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase); + editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase); + } - // publishing = deal with edit and published values - foreach (var propertyValue in property.Values) - { - var isInvariantValue = propertyValue.Culture == null && propertyValue.Segment == null; - var isCultureValue = propertyValue.Culture != null; - var isSegmentValue = propertyValue.Segment != null; + // publishing = deal with edit and published values + foreach (IPropertyValue? propertyValue in property.Values) + { + var isInvariantValue = propertyValue.Culture == null && propertyValue.Segment == null; + var isCultureValue = propertyValue.Culture != null; + var isSegmentValue = propertyValue.Segment != null; - // deal with published value - if ((propertyValue.PublishedValue != null || isSegmentValue) && publishedVersionId > 0) - propertyDataDtos.Add(BuildDto(publishedVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue?.Segment, propertyValue?.PublishedValue)); + // deal with published value + if ((propertyValue.PublishedValue != null || isSegmentValue) && publishedVersionId > 0) + { + propertyDataDtos.Add(BuildDto(publishedVersionId, property, + languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue?.Segment, + propertyValue?.PublishedValue)); + } - // deal with edit value - if (propertyValue?.EditedValue != null || isSegmentValue) - propertyDataDtos.Add(BuildDto(currentVersionId, property, languageRepository.GetIdByIsoCode(propertyValue?.Culture), propertyValue?.Segment, propertyValue?.EditedValue)); + // deal with edit value + if (propertyValue?.EditedValue != null || isSegmentValue) + { + propertyDataDtos.Add(BuildDto(currentVersionId, property, + languageRepository.GetIdByIsoCode(propertyValue?.Culture), propertyValue?.Segment, + propertyValue?.EditedValue)); + } - // property.Values will contain ALL of it's values, both variant and invariant which will be populated if the - // administrator has previously changed the property type to be variant vs invariant. - // We need to check for this scenario here because otherwise the editedCultures and edited flags - // will end up incorrectly set in the umbracoDocumentCultureVariation table so here we need to - // only process edited cultures based on the current value type and how the property varies. - // The above logic will still persist the currently saved property value for each culture in case the admin - // decides to swap the property's variance again, in which case the edited flag will be recalculated. + // property.Values will contain ALL of it's values, both variant and invariant which will be populated if the + // administrator has previously changed the property type to be variant vs invariant. + // We need to check for this scenario here because otherwise the editedCultures and edited flags + // will end up incorrectly set in the umbracoDocumentCultureVariation table so here we need to + // only process edited cultures based on the current value type and how the property varies. + // The above logic will still persist the currently saved property value for each culture in case the admin + // decides to swap the property's variance again, in which case the edited flag will be recalculated. - if (property.PropertyType.VariesByCulture() && isInvariantValue || !property.PropertyType.VariesByCulture() && isCultureValue) - continue; + if ((property.PropertyType.VariesByCulture() && isInvariantValue) || + (!property.PropertyType.VariesByCulture() && isCultureValue)) + { + continue; + } - // use explicit equals here, else object comparison fails at comparing eg strings - var sameValues = propertyValue?.PublishedValue == null ? propertyValue?.EditedValue == null : propertyValue.PublishedValue.Equals(propertyValue.EditedValue); + // use explicit equals here, else object comparison fails at comparing eg strings + var sameValues = propertyValue?.PublishedValue == null + ? propertyValue?.EditedValue == null + : propertyValue.PublishedValue.Equals(propertyValue.EditedValue); - edited |= !sameValues; + edited |= !sameValues; - if (entityVariesByCulture && !sameValues) + if (entityVariesByCulture && !sameValues) + { + if (isCultureValue && propertyValue?.Culture is not null) + { + editedCultures?.Add(propertyValue.Culture); // report culture as edited + } + else if (isInvariantValue) { - if (isCultureValue && propertyValue?.Culture is not null) + // flag culture as edited if it contains an edited invariant property + if (defaultCulture == null) { - editedCultures?.Add(propertyValue.Culture); // report culture as edited + defaultCulture = languageRepository.GetDefaultIsoCode(); } - else if (isInvariantValue) - { - // flag culture as edited if it contains an edited invariant property - if (defaultCulture == null) - defaultCulture = languageRepository.GetDefaultIsoCode(); - editedCultures?.Add(defaultCulture); - } + editedCultures?.Add(defaultCulture); } } } - else + } + else + { + foreach (IPropertyValue propertyValue in property.Values) { - foreach (var propertyValue in property.Values) + // not publishing = only deal with edit values + if (propertyValue.EditedValue != null) { - // not publishing = only deal with edit values - if (propertyValue.EditedValue != null) - propertyDataDtos.Add(BuildDto(currentVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.EditedValue)); + propertyDataDtos.Add(BuildDto(currentVersionId, property, + languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, + propertyValue.EditedValue)); } - edited = true; } - } - return propertyDataDtos; + edited = true; + } } + + return propertyDataDtos; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/PropertyGroupFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/PropertyGroupFactory.cs index 333c0176c8a3..c9ad75f1d78d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/PropertyGroupFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/PropertyGroupFactory.cs @@ -1,160 +1,161 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class PropertyGroupFactory { - internal static class PropertyGroupFactory + internal static PropertyTypeGroupDto BuildGroupDto(PropertyGroup propertyGroup, int contentTypeId) { - - #region Implementation of IEntityFactory,IEnumerable> - - public static IEnumerable BuildEntity(IEnumerable groupDtos, - bool isPublishing, - int contentTypeId, - DateTime createDate, - DateTime updateDate, - Func propertyTypeCtor) + var dto = new PropertyTypeGroupDto { - // groupDtos contains all the groups, those that are defined on the current - // content type, and those that are inherited from composition content types - var propertyGroups = new PropertyGroupCollection(); - foreach (var groupDto in groupDtos) - { - var group = new PropertyGroup(isPublishing); - - try - { - group.DisableChangeTracking(); - - // if the group is defined on the current content type, - // assign its identifier, else it will be zero - if (groupDto.ContentTypeNodeId == contentTypeId) - group.Id = groupDto.Id; - - group.Key = groupDto.UniqueId; - group.Type = (PropertyGroupType)groupDto.Type; - group.Name = groupDto.Text; - group.Alias = groupDto.Alias; - group.SortOrder = groupDto.SortOrder; + UniqueId = propertyGroup.Key, + Type = (short)propertyGroup.Type, + ContentTypeNodeId = contentTypeId, + Text = propertyGroup.Name, + Alias = propertyGroup.Alias, + SortOrder = propertyGroup.SortOrder + }; + + if (propertyGroup.HasIdentity) + { + dto.Id = propertyGroup.Id; + } - group.PropertyTypes = new PropertyTypeCollection(isPublishing); + dto.PropertyTypeDtos = propertyGroup.PropertyTypes + ?.Select(propertyType => BuildPropertyTypeDto(propertyGroup.Id, propertyType, contentTypeId)).ToList(); - //Because we are likely to have a group with no PropertyTypes we need to ensure that these are excluded - var typeDtos = groupDto.PropertyTypeDtos?.Where(x => x.Id > 0) ?? Enumerable.Empty(); - foreach (var typeDto in typeDtos) - { - var tempGroupDto = groupDto; - var propertyType = propertyTypeCtor(typeDto.DataTypeDto.EditorAlias, - typeDto.DataTypeDto.DbType.EnumParse(true), - typeDto.Alias); - - try - { - propertyType.DisableChangeTracking(); - - propertyType.Alias = typeDto.Alias ?? string.Empty; - propertyType.DataTypeId = typeDto.DataTypeId; - propertyType.DataTypeKey = typeDto.DataTypeDto.NodeDto.UniqueId; - propertyType.Description = typeDto.Description; - propertyType.Id = typeDto.Id; - propertyType.Key = typeDto.UniqueId; - propertyType.Name = typeDto.Name ?? string.Empty; - propertyType.Mandatory = typeDto.Mandatory; - propertyType.MandatoryMessage = typeDto.MandatoryMessage; - propertyType.SortOrder = typeDto.SortOrder; - propertyType.ValidationRegExp = typeDto.ValidationRegExp; - propertyType.ValidationRegExpMessage = typeDto.ValidationRegExpMessage; - propertyType.PropertyGroupId = new Lazy(() => tempGroupDto.Id); - propertyType.CreateDate = createDate; - propertyType.UpdateDate = updateDate; - propertyType.Variations = (ContentVariation)typeDto.Variations; - - // reset dirty initial properties (U4-1946) - propertyType.ResetDirtyProperties(false); - group.PropertyTypes.Add(propertyType); - } - finally - { - propertyType.EnableChangeTracking(); - } - } - - // reset dirty initial properties (U4-1946) - group.ResetDirtyProperties(false); - propertyGroups.Add(group); - } - finally - { - group.EnableChangeTracking(); - } - } + return dto; + } - return propertyGroups; + internal static PropertyTypeDto BuildPropertyTypeDto(int groupId, IPropertyType propertyType, int contentTypeId) + { + var propertyTypeDto = new PropertyTypeDto + { + Alias = propertyType.Alias, + ContentTypeId = contentTypeId, + DataTypeId = propertyType.DataTypeId, + Description = propertyType.Description, + Mandatory = propertyType.Mandatory, + MandatoryMessage = propertyType.MandatoryMessage, + Name = propertyType.Name, + SortOrder = propertyType.SortOrder, + ValidationRegExp = propertyType.ValidationRegExp, + ValidationRegExpMessage = propertyType.ValidationRegExpMessage, + UniqueId = propertyType.Key, + Variations = (byte)propertyType.Variations, + LabelOnTop = propertyType.LabelOnTop + }; + + if (groupId != default) + { + propertyTypeDto.PropertyTypeGroupId = groupId; + } + else + { + propertyTypeDto.PropertyTypeGroupId = null; } - public static IEnumerable BuildDto(IEnumerable entity) + if (propertyType.HasIdentity) { - return entity.Select(BuildGroupDto).ToList(); + propertyTypeDto.Id = propertyType.Id; } - #endregion + return propertyTypeDto; + } + + #region Implementation of IEntityFactory,IEnumerable> - internal static PropertyTypeGroupDto BuildGroupDto(PropertyGroup propertyGroup, int contentTypeId) + public static IEnumerable BuildEntity(IEnumerable groupDtos, + bool isPublishing, + int contentTypeId, + DateTime createDate, + DateTime updateDate, + Func propertyTypeCtor) + { + // groupDtos contains all the groups, those that are defined on the current + // content type, and those that are inherited from composition content types + var propertyGroups = new PropertyGroupCollection(); + foreach (PropertyTypeGroupDto groupDto in groupDtos) { - var dto = new PropertyTypeGroupDto + var group = new PropertyGroup(isPublishing); + + try { - UniqueId = propertyGroup.Key, - Type = (short)propertyGroup.Type, - ContentTypeNodeId = contentTypeId, - Text = propertyGroup.Name, - Alias = propertyGroup.Alias, - SortOrder = propertyGroup.SortOrder - }; + group.DisableChangeTracking(); + + // if the group is defined on the current content type, + // assign its identifier, else it will be zero + if (groupDto.ContentTypeNodeId == contentTypeId) + { + group.Id = groupDto.Id; + } - if (propertyGroup.HasIdentity) - dto.Id = propertyGroup.Id; + group.Key = groupDto.UniqueId; + group.Type = (PropertyGroupType)groupDto.Type; + group.Name = groupDto.Text; + group.Alias = groupDto.Alias; + group.SortOrder = groupDto.SortOrder; - dto.PropertyTypeDtos = propertyGroup.PropertyTypes?.Select(propertyType => BuildPropertyTypeDto(propertyGroup.Id, propertyType, contentTypeId)).ToList(); + group.PropertyTypes = new PropertyTypeCollection(isPublishing); - return dto; - } + //Because we are likely to have a group with no PropertyTypes we need to ensure that these are excluded + IEnumerable typeDtos = groupDto.PropertyTypeDtos?.Where(x => x.Id > 0) ?? + Enumerable.Empty(); + foreach (PropertyTypeDto typeDto in typeDtos) + { + PropertyTypeGroupDto tempGroupDto = groupDto; + PropertyType propertyType = propertyTypeCtor(typeDto.DataTypeDto.EditorAlias, + typeDto.DataTypeDto.DbType.EnumParse(true), + typeDto.Alias); - internal static PropertyTypeDto BuildPropertyTypeDto(int groupId, IPropertyType propertyType, int contentTypeId) - { - var propertyTypeDto = new PropertyTypeDto - { - Alias = propertyType.Alias, - ContentTypeId = contentTypeId, - DataTypeId = propertyType.DataTypeId, - Description = propertyType.Description, - Mandatory = propertyType.Mandatory, - MandatoryMessage = propertyType.MandatoryMessage, - Name = propertyType.Name, - SortOrder = propertyType.SortOrder, - ValidationRegExp = propertyType.ValidationRegExp, - ValidationRegExpMessage = propertyType.ValidationRegExpMessage, - UniqueId = propertyType.Key, - Variations = (byte)propertyType.Variations, - LabelOnTop = propertyType.LabelOnTop - }; - - if (groupId != default) - { - propertyTypeDto.PropertyTypeGroupId = groupId; + try + { + propertyType.DisableChangeTracking(); + + propertyType.Alias = typeDto.Alias ?? string.Empty; + propertyType.DataTypeId = typeDto.DataTypeId; + propertyType.DataTypeKey = typeDto.DataTypeDto.NodeDto.UniqueId; + propertyType.Description = typeDto.Description; + propertyType.Id = typeDto.Id; + propertyType.Key = typeDto.UniqueId; + propertyType.Name = typeDto.Name ?? string.Empty; + propertyType.Mandatory = typeDto.Mandatory; + propertyType.MandatoryMessage = typeDto.MandatoryMessage; + propertyType.SortOrder = typeDto.SortOrder; + propertyType.ValidationRegExp = typeDto.ValidationRegExp; + propertyType.ValidationRegExpMessage = typeDto.ValidationRegExpMessage; + propertyType.PropertyGroupId = new Lazy(() => tempGroupDto.Id); + propertyType.CreateDate = createDate; + propertyType.UpdateDate = updateDate; + propertyType.Variations = (ContentVariation)typeDto.Variations; + + // reset dirty initial properties (U4-1946) + propertyType.ResetDirtyProperties(false); + group.PropertyTypes.Add(propertyType); + } + finally + { + propertyType.EnableChangeTracking(); + } + } + + // reset dirty initial properties (U4-1946) + group.ResetDirtyProperties(false); + propertyGroups.Add(group); } - else + finally { - propertyTypeDto.PropertyTypeGroupId = null; + group.EnableChangeTracking(); } - - if (propertyType.HasIdentity) - propertyTypeDto.Id = propertyType.Id; - - return propertyTypeDto; } + + return propertyGroups; } + + public static IEnumerable BuildDto(IEnumerable entity) => + entity.Select(BuildGroupDto).ToList(); + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/PublicAccessEntryFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/PublicAccessEntryFactory.cs index 0ed16d80da5d..0f0a2802122f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/PublicAccessEntryFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/PublicAccessEntryFactory.cs @@ -1,53 +1,44 @@ -using System.Linq; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class PublicAccessEntryFactory { - internal static class PublicAccessEntryFactory + public static PublicAccessEntry BuildEntity(AccessDto dto) { - public static PublicAccessEntry BuildEntity(AccessDto dto) - { - var entity = new PublicAccessEntry(dto.Id, dto.NodeId, dto.LoginNodeId, dto.NoAccessNodeId, - dto.Rules.Select(x => new PublicAccessRule(x.Id, x.AccessId) - { - RuleValue = x.RuleValue, - RuleType = x.RuleType, - CreateDate = x.CreateDate, - UpdateDate = x.UpdateDate - })) + var entity = new PublicAccessEntry(dto.Id, dto.NodeId, dto.LoginNodeId, dto.NoAccessNodeId, + dto.Rules.Select(x => new PublicAccessRule(x.Id, x.AccessId) { - CreateDate = dto.CreateDate, - UpdateDate = dto.UpdateDate - }; + RuleValue = x.RuleValue, RuleType = x.RuleType, CreateDate = x.CreateDate, UpdateDate = x.UpdateDate + })) {CreateDate = dto.CreateDate, UpdateDate = dto.UpdateDate}; - // reset dirty initial properties (U4-1946) - entity.ResetDirtyProperties(false); - return entity; - } + // reset dirty initial properties (U4-1946) + entity.ResetDirtyProperties(false); + return entity; + } - public static AccessDto BuildDto(PublicAccessEntry entity) + public static AccessDto BuildDto(PublicAccessEntry entity) + { + var dto = new AccessDto { - var dto = new AccessDto + Id = entity.Key, + NoAccessNodeId = entity.NoAccessNodeId, + LoginNodeId = entity.LoginNodeId, + NodeId = entity.ProtectedNodeId, + CreateDate = entity.CreateDate, + UpdateDate = entity.UpdateDate, + Rules = entity.Rules.Select(x => new AccessRuleDto { - Id = entity.Key, - NoAccessNodeId = entity.NoAccessNodeId, - LoginNodeId = entity.LoginNodeId, - NodeId = entity.ProtectedNodeId, - CreateDate = entity.CreateDate, - UpdateDate = entity.UpdateDate, - Rules = entity.Rules.Select(x => new AccessRuleDto - { - AccessId = x.AccessEntryId, - Id = x.Key, - RuleValue = x.RuleValue, - RuleType = x.RuleType, - CreateDate = x.CreateDate, - UpdateDate = x.UpdateDate - }).ToList() - }; + AccessId = x.AccessEntryId, + Id = x.Key, + RuleValue = x.RuleValue, + RuleType = x.RuleType, + CreateDate = x.CreateDate, + UpdateDate = x.UpdateDate + }).ToList() + }; - return dto; - } + return dto; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/RelationFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/RelationFactory.cs index 63d329216040..8cebdc7ea18b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/RelationFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/RelationFactory.cs @@ -1,66 +1,68 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class RelationFactory { - internal static class RelationFactory + public static IRelation BuildEntity(RelationDto dto, IRelationType relationType) { - public static IRelation BuildEntity(RelationDto dto, IRelationType relationType) - { - var entity = new Relation(dto.ParentId, dto.ChildId, dto.ParentObjectType, dto.ChildObjectType, relationType); + var entity = new Relation(dto.ParentId, dto.ChildId, dto.ParentObjectType, dto.ChildObjectType, relationType); - try - { - entity.DisableChangeTracking(); + try + { + entity.DisableChangeTracking(); - entity.Comment = dto.Comment; - entity.CreateDate = dto.Datetime; - entity.Id = dto.Id; - entity.UpdateDate = dto.Datetime; + entity.Comment = dto.Comment; + entity.CreateDate = dto.Datetime; + entity.Id = dto.Id; + entity.UpdateDate = dto.Datetime; - // reset dirty initial properties (U4-1946) - entity.ResetDirtyProperties(false); - return entity; - } - finally - { - entity.EnableChangeTracking(); - } + // reset dirty initial properties (U4-1946) + entity.ResetDirtyProperties(false); + return entity; } - - public static RelationDto BuildDto(IRelation entity) + finally { - var dto = new RelationDto - { - ChildId = entity.ChildId, - Comment = string.IsNullOrEmpty(entity.Comment) ? string.Empty : entity.Comment, - Datetime = entity.CreateDate, - ParentId = entity.ParentId, - RelationType = entity.RelationType.Id - }; + entity.EnableChangeTracking(); + } + } - if (entity.HasIdentity) - dto.Id = entity.Id; + public static RelationDto BuildDto(IRelation entity) + { + var dto = new RelationDto + { + ChildId = entity.ChildId, + Comment = string.IsNullOrEmpty(entity.Comment) ? string.Empty : entity.Comment, + Datetime = entity.CreateDate, + ParentId = entity.ParentId, + RelationType = entity.RelationType.Id + }; - return dto; + if (entity.HasIdentity) + { + dto.Id = entity.Id; } - public static RelationDto BuildDto(ReadOnlyRelation entity) - { - var dto = new RelationDto - { - ChildId = entity.ChildId, - Comment = string.IsNullOrEmpty(entity.Comment) ? string.Empty : entity.Comment, - Datetime = entity.CreateDate, - ParentId = entity.ParentId, - RelationType = entity.RelationTypeId - }; + return dto; + } - if (entity.HasIdentity) - dto.Id = entity.Id; + public static RelationDto BuildDto(ReadOnlyRelation entity) + { + var dto = new RelationDto + { + ChildId = entity.ChildId, + Comment = string.IsNullOrEmpty(entity.Comment) ? string.Empty : entity.Comment, + Datetime = entity.CreateDate, + ParentId = entity.ParentId, + RelationType = entity.RelationTypeId + }; - return dto; + if (entity.HasIdentity) + { + dto.Id = entity.Id; } + return dto; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/RelationTypeFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/RelationTypeFactory.cs index 57b1831c9d0c..082b6a03c063 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/RelationTypeFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/RelationTypeFactory.cs @@ -1,60 +1,59 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class RelationTypeFactory { - internal static class RelationTypeFactory + #region Implementation of IEntityFactory + + public static IRelationType BuildEntity(RelationTypeDto dto) { - #region Implementation of IEntityFactory + var entity = new RelationType(dto.Name, dto.Alias, dto.Dual, dto.ParentObjectType, dto.ChildObjectType, + dto.IsDependency); - public static IRelationType BuildEntity(RelationTypeDto dto) + try { - var entity = new RelationType(dto.Name, dto.Alias, dto.Dual, dto.ParentObjectType, dto.ChildObjectType, dto.IsDependency); - - try - { - entity.DisableChangeTracking(); - - entity.Id = dto.Id; - entity.Key = dto.UniqueId; - - // reset dirty initial properties (U4-1946) - entity.ResetDirtyProperties(false); - return entity; - } - finally - { - entity.EnableChangeTracking(); - } - } + entity.DisableChangeTracking(); + + entity.Id = dto.Id; + entity.Key = dto.UniqueId; - public static RelationTypeDto BuildDto(IRelationType entity) + // reset dirty initial properties (U4-1946) + entity.ResetDirtyProperties(false); + return entity; + } + finally { - var isDependency = false; - if (entity is IRelationTypeWithIsDependency relationTypeWithIsDependency) - { - isDependency = relationTypeWithIsDependency.IsDependency; - } - var dto = new RelationTypeDto - { - Alias = entity.Alias, - ChildObjectType = entity.ChildObjectType, - Dual = entity.IsBidirectional, - IsDependency = isDependency, - Name = entity.Name ?? string.Empty, - ParentObjectType = entity.ParentObjectType, - UniqueId = entity.Key - }; - if (entity.HasIdentity) - { - dto.Id = entity.Id; - } - - return dto; + entity.EnableChangeTracking(); } + } + public static RelationTypeDto BuildDto(IRelationType entity) + { + var isDependency = false; + if (entity is IRelationTypeWithIsDependency relationTypeWithIsDependency) + { + isDependency = relationTypeWithIsDependency.IsDependency; + } + var dto = new RelationTypeDto + { + Alias = entity.Alias, + ChildObjectType = entity.ChildObjectType, + Dual = entity.IsBidirectional, + IsDependency = isDependency, + Name = entity.Name ?? string.Empty, + ParentObjectType = entity.ParentObjectType, + UniqueId = entity.Key + }; + if (entity.HasIdentity) + { + dto.Id = entity.Id; + } - #endregion + return dto; } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/ServerRegistrationFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/ServerRegistrationFactory.cs index f662faf5616e..7f28b7970b58 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/ServerRegistrationFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/ServerRegistrationFactory.cs @@ -1,34 +1,36 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class ServerRegistrationFactory { - internal static class ServerRegistrationFactory + public static ServerRegistration BuildEntity(ServerRegistrationDto dto) { - public static ServerRegistration BuildEntity(ServerRegistrationDto dto) - { - var model = new ServerRegistration(dto.Id, dto.ServerAddress, dto.ServerIdentity, dto.DateRegistered, dto.DateAccessed, dto.IsActive, dto.IsSchedulingPublisher); - // reset dirty initial properties (U4-1946) - model.ResetDirtyProperties(false); - return model; - } + var model = new ServerRegistration(dto.Id, dto.ServerAddress, dto.ServerIdentity, dto.DateRegistered, + dto.DateAccessed, dto.IsActive, dto.IsSchedulingPublisher); + // reset dirty initial properties (U4-1946) + model.ResetDirtyProperties(false); + return model; + } - public static ServerRegistrationDto BuildDto(IServerRegistration entity) + public static ServerRegistrationDto BuildDto(IServerRegistration entity) + { + var dto = new ServerRegistrationDto { - var dto = new ServerRegistrationDto - { - ServerAddress = entity.ServerAddress, - DateRegistered = entity.CreateDate, - IsActive = entity.IsActive, - IsSchedulingPublisher = ((ServerRegistration) entity).IsSchedulingPublisher, - DateAccessed = entity.UpdateDate, - ServerIdentity = entity.ServerIdentity - }; - - if (entity.HasIdentity) - dto.Id = entity.Id; + ServerAddress = entity.ServerAddress, + DateRegistered = entity.CreateDate, + IsActive = entity.IsActive, + IsSchedulingPublisher = ((ServerRegistration)entity).IsSchedulingPublisher, + DateAccessed = entity.UpdateDate, + ServerIdentity = entity.ServerIdentity + }; - return dto; + if (entity.HasIdentity) + { + dto.Id = entity.Id; } + + return dto; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/TagFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/TagFactory.cs index e666e536587e..3e16c646a089 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/TagFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/TagFactory.cs @@ -1,28 +1,22 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class TagFactory { - internal static class TagFactory + public static ITag BuildEntity(TagDto dto) { - public static ITag BuildEntity(TagDto dto) - { - var entity = new Tag(dto.Id, dto.Group, dto.Text, dto.LanguageId) { NodeCount = dto.NodeCount }; - // reset dirty initial properties (U4-1946) - entity.ResetDirtyProperties(false); - return entity; - } + var entity = new Tag(dto.Id, dto.Group, dto.Text, dto.LanguageId) {NodeCount = dto.NodeCount}; + // reset dirty initial properties (U4-1946) + entity.ResetDirtyProperties(false); + return entity; + } - public static TagDto BuildDto(ITag entity) + public static TagDto BuildDto(ITag entity) => + new TagDto { - return new TagDto - { - Id = entity.Id, - Group = entity.Group, - Text = entity.Text, - LanguageId = entity.LanguageId - //Key = entity.Group + "/" + entity.Text // de-normalize - }; - } - } + Id = entity.Id, Group = entity.Group, Text = entity.Text, LanguageId = entity.LanguageId + //Key = entity.Group + "/" + entity.Text // de-normalize + }; } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/TemplateFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/TemplateFactory.cs index eb84d46f6810..3028d1a50982 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/TemplateFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/TemplateFactory.cs @@ -1,87 +1,81 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using File = Umbraco.Cms.Core.Models.File; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class TemplateFactory { - internal static class TemplateFactory + private static NodeDto BuildNodeDto(Template entity, Guid? nodeObjectTypeId) { - - #region Implementation of IEntityFactory - - public static Template BuildEntity(IShortStringHelper shortStringHelper, TemplateDto dto, IEnumerable childDefinitions, Func getFileContent) + var nodeDto = new NodeDto { - var template = new Template(shortStringHelper, dto.NodeDto.Text, dto.Alias, getFileContent); - - try - { - template.DisableChangeTracking(); - - template.CreateDate = dto.NodeDto.CreateDate; - template.Id = dto.NodeId; - template.Key = dto.NodeDto.UniqueId; - template.Path = dto.NodeDto.Path; + CreateDate = entity.CreateDate, + NodeId = entity.Id, + Level = 1, + NodeObjectType = nodeObjectTypeId, + ParentId = entity.MasterTemplateId?.Value ?? 0, + Path = entity.Path, + Text = entity.Name, + Trashed = false, + UniqueId = entity.Key + }; - template.IsMasterTemplate = childDefinitions.Any(x => x.ParentId == dto.NodeId); + return nodeDto; + } - if (dto.NodeDto.ParentId > 0) - template.MasterTemplateId = new Lazy(() => dto.NodeDto.ParentId); + #region Implementation of IEntityFactory - // reset dirty initial properties (U4-1946) - template.ResetDirtyProperties(false); - return template; - } - finally - { - template.EnableChangeTracking(); - } - } + public static Template BuildEntity(IShortStringHelper shortStringHelper, TemplateDto dto, + IEnumerable childDefinitions, Func getFileContent) + { + var template = new Template(shortStringHelper, dto.NodeDto.Text, dto.Alias, getFileContent); - public static TemplateDto BuildDto(Template entity, Guid? nodeObjectTypeId,int primaryKey) + try { - var dto = new TemplateDto - { - Alias = entity.Alias, - NodeDto = BuildNodeDto(entity, nodeObjectTypeId) - }; + template.DisableChangeTracking(); - if (entity.MasterTemplateId != null && entity.MasterTemplateId.Value > 0) - { - dto.NodeDto.ParentId = entity.MasterTemplateId.Value; - } + template.CreateDate = dto.NodeDto.CreateDate; + template.Id = dto.NodeId; + template.Key = dto.NodeDto.UniqueId; + template.Path = dto.NodeDto.Path; + + template.IsMasterTemplate = childDefinitions.Any(x => x.ParentId == dto.NodeId); - if (entity.HasIdentity) + if (dto.NodeDto.ParentId > 0) { - dto.NodeId = entity.Id; - dto.PrimaryKey = primaryKey; + template.MasterTemplateId = new Lazy(() => dto.NodeDto.ParentId); } - return dto; + // reset dirty initial properties (U4-1946) + template.ResetDirtyProperties(false); + return template; } + finally + { + template.EnableChangeTracking(); + } + } - #endregion + public static TemplateDto BuildDto(Template entity, Guid? nodeObjectTypeId, int primaryKey) + { + var dto = new TemplateDto {Alias = entity.Alias, NodeDto = BuildNodeDto(entity, nodeObjectTypeId)}; - private static NodeDto BuildNodeDto(Template entity,Guid? nodeObjectTypeId) + if (entity.MasterTemplateId != null && entity.MasterTemplateId.Value > 0) { - var nodeDto = new NodeDto - { - CreateDate = entity.CreateDate, - NodeId = entity.Id, - Level = 1, - NodeObjectType = nodeObjectTypeId, - ParentId = entity.MasterTemplateId?.Value ?? 0, - Path = entity.Path, - Text = entity.Name, - Trashed = false, - UniqueId = entity.Key - }; + dto.NodeDto.ParentId = entity.MasterTemplateId.Value; + } - return nodeDto; + if (entity.HasIdentity) + { + dto.NodeId = entity.Id; + dto.PrimaryKey = primaryKey; } + + return dto; } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs index 8ee2bcfec0bc..f058fbd0b253 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs @@ -1,119 +1,120 @@ -using System; -using System.Linq; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class UserFactory { - internal static class UserFactory + public static IUser BuildEntity(GlobalSettings globalSettings, UserDto dto) { - public static IUser BuildEntity(GlobalSettings globalSettings, UserDto dto) - { - var guidId = dto.Id.ToGuid(); + var guidId = dto.Id.ToGuid(); - var user = new User(globalSettings, dto.Id, dto.UserName, dto.Email, dto.Login, dto.Password, dto.PasswordConfig, - dto.UserGroupDtos.Select(x => ToReadOnlyGroup(x)).ToArray(), - dto.UserStartNodeDtos.Where(x => x.StartNodeType == (int)UserStartNodeDto.StartNodeTypeValue.Content).Select(x => x.StartNode).ToArray(), - dto.UserStartNodeDtos.Where(x => x.StartNodeType == (int)UserStartNodeDto.StartNodeTypeValue.Media).Select(x => x.StartNode).ToArray()); + var user = new User(globalSettings, dto.Id, dto.UserName, dto.Email, dto.Login, dto.Password, + dto.PasswordConfig, + dto.UserGroupDtos.Select(x => ToReadOnlyGroup(x)).ToArray(), + dto.UserStartNodeDtos.Where(x => x.StartNodeType == (int)UserStartNodeDto.StartNodeTypeValue.Content) + .Select(x => x.StartNode).ToArray(), + dto.UserStartNodeDtos.Where(x => x.StartNodeType == (int)UserStartNodeDto.StartNodeTypeValue.Media) + .Select(x => x.StartNode).ToArray()); - try - { - user.DisableChangeTracking(); + try + { + user.DisableChangeTracking(); - user.Key = guidId; - user.IsLockedOut = dto.NoConsole; - user.IsApproved = dto.Disabled == false; - user.Language = dto.UserLanguage; - user.SecurityStamp = dto.SecurityStampToken; - user.FailedPasswordAttempts = dto.FailedLoginAttempts ?? 0; - user.LastLockoutDate = dto.LastLockoutDate; - user.LastLoginDate = dto.LastLoginDate; - user.LastPasswordChangeDate = dto.LastPasswordChangeDate; - user.CreateDate = dto.CreateDate; - user.UpdateDate = dto.UpdateDate; - user.Avatar = dto.Avatar; - user.EmailConfirmedDate = dto.EmailConfirmedDate; - user.InvitedDate = dto.InvitedDate; - user.TourData = dto.TourData; + user.Key = guidId; + user.IsLockedOut = dto.NoConsole; + user.IsApproved = dto.Disabled == false; + user.Language = dto.UserLanguage; + user.SecurityStamp = dto.SecurityStampToken; + user.FailedPasswordAttempts = dto.FailedLoginAttempts ?? 0; + user.LastLockoutDate = dto.LastLockoutDate; + user.LastLoginDate = dto.LastLoginDate; + user.LastPasswordChangeDate = dto.LastPasswordChangeDate; + user.CreateDate = dto.CreateDate; + user.UpdateDate = dto.UpdateDate; + user.Avatar = dto.Avatar; + user.EmailConfirmedDate = dto.EmailConfirmedDate; + user.InvitedDate = dto.InvitedDate; + user.TourData = dto.TourData; - // reset dirty initial properties (U4-1946) - user.ResetDirtyProperties(false); + // reset dirty initial properties (U4-1946) + user.ResetDirtyProperties(false); - return user; - } - finally - { - user.EnableChangeTracking(); - } + return user; + } + finally + { + user.EnableChangeTracking(); } + } - public static UserDto BuildDto(IUser entity) + public static UserDto BuildDto(IUser entity) + { + var dto = new UserDto { - var dto = new UserDto - { - Disabled = entity.IsApproved == false, - Email = entity.Email, - Login = entity.Username, - NoConsole = entity.IsLockedOut, - Password = entity.RawPasswordValue, - PasswordConfig = entity.PasswordConfiguration, - UserLanguage = entity.Language, - UserName = entity.Name!, - SecurityStampToken = entity.SecurityStamp, - FailedLoginAttempts = entity.FailedPasswordAttempts, - LastLockoutDate = entity.LastLockoutDate == DateTime.MinValue ? (DateTime?)null : entity.LastLockoutDate, - LastLoginDate = entity.LastLoginDate == DateTime.MinValue ? (DateTime?)null : entity.LastLoginDate, - LastPasswordChangeDate = entity.LastPasswordChangeDate == DateTime.MinValue ? (DateTime?)null : entity.LastPasswordChangeDate, - CreateDate = entity.CreateDate, - UpdateDate = entity.UpdateDate, - Avatar = entity.Avatar, - EmailConfirmedDate = entity.EmailConfirmedDate, - InvitedDate = entity.InvitedDate, - TourData = entity.TourData - }; + Disabled = entity.IsApproved == false, + Email = entity.Email, + Login = entity.Username, + NoConsole = entity.IsLockedOut, + Password = entity.RawPasswordValue, + PasswordConfig = entity.PasswordConfiguration, + UserLanguage = entity.Language, + UserName = entity.Name!, + SecurityStampToken = entity.SecurityStamp, + FailedLoginAttempts = entity.FailedPasswordAttempts, + LastLockoutDate = entity.LastLockoutDate == DateTime.MinValue ? null : entity.LastLockoutDate, + LastLoginDate = entity.LastLoginDate == DateTime.MinValue ? null : entity.LastLoginDate, + LastPasswordChangeDate = + entity.LastPasswordChangeDate == DateTime.MinValue ? null : entity.LastPasswordChangeDate, + CreateDate = entity.CreateDate, + UpdateDate = entity.UpdateDate, + Avatar = entity.Avatar, + EmailConfirmedDate = entity.EmailConfirmedDate, + InvitedDate = entity.InvitedDate, + TourData = entity.TourData + }; - if (entity.StartContentIds is not null) + if (entity.StartContentIds is not null) + { + foreach (var startNodeId in entity.StartContentIds) { - foreach (var startNodeId in entity.StartContentIds) + dto.UserStartNodeDtos.Add(new UserStartNodeDto { - dto.UserStartNodeDtos.Add(new UserStartNodeDto - { - StartNode = startNodeId, - StartNodeType = (int)UserStartNodeDto.StartNodeTypeValue.Content, - UserId = entity.Id - }); - } + StartNode = startNodeId, + StartNodeType = (int)UserStartNodeDto.StartNodeTypeValue.Content, + UserId = entity.Id + }); } + } - if (entity.StartMediaIds is not null) + if (entity.StartMediaIds is not null) + { + foreach (var startNodeId in entity.StartMediaIds) { - foreach (var startNodeId in entity.StartMediaIds) + dto.UserStartNodeDtos.Add(new UserStartNodeDto { - dto.UserStartNodeDtos.Add(new UserStartNodeDto - { - StartNode = startNodeId, - StartNodeType = (int)UserStartNodeDto.StartNodeTypeValue.Media, - UserId = entity.Id - }); - } - } - - if (entity.HasIdentity) - { - dto.Id = entity.Id.SafeCast(); + StartNode = startNodeId, + StartNodeType = (int)UserStartNodeDto.StartNodeTypeValue.Media, + UserId = entity.Id + }); } - - return dto; } - private static IReadOnlyUserGroup ToReadOnlyGroup(UserGroupDto group) + if (entity.HasIdentity) { - return new ReadOnlyUserGroup(group.Id, group.Name, group.Icon, - group.StartContentId, group.StartMediaId, group.Alias, - group.UserGroup2AppDtos.Select(x => x.AppAlias).WhereNotNull().ToArray(), - group.DefaultPermissions == null ? Enumerable.Empty() : group.DefaultPermissions.ToCharArray().Select(x => x.ToString())); + dto.Id = entity.Id.SafeCast(); } + + return dto; } + + private static IReadOnlyUserGroup ToReadOnlyGroup(UserGroupDto group) => + new ReadOnlyUserGroup(group.Id, group.Name, group.Icon, + group.StartContentId, group.StartMediaId, group.Alias, + group.UserGroup2AppDtos.Select(x => x.AppAlias).WhereNotNull().ToArray(), + group.DefaultPermissions == null + ? Enumerable.Empty() + : group.DefaultPermissions.ToCharArray().Select(x => x.ToString())); } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/UserGroupFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/UserGroupFactory.cs index 9672e0e3a908..b9e919e14a58 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/UserGroupFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/UserGroupFactory.cs @@ -1,82 +1,77 @@ -using System.Collections.Generic; -using System.Globalization; -using System.Linq; +using System.Globalization; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class UserGroupFactory { - internal static class UserGroupFactory + public static IUserGroup BuildEntity(IShortStringHelper shortStringHelper, UserGroupDto dto) { - public static IUserGroup BuildEntity(IShortStringHelper shortStringHelper, UserGroupDto dto) - { - var userGroup = new UserGroup(shortStringHelper, dto.UserCount, dto.Alias, dto.Name, - dto.DefaultPermissions.IsNullOrWhiteSpace() - ? Enumerable.Empty() - : dto.DefaultPermissions!.ToCharArray().Select(x => x.ToString(CultureInfo.InvariantCulture)).ToList(), - dto.Icon); + var userGroup = new UserGroup(shortStringHelper, dto.UserCount, dto.Alias, dto.Name, + dto.DefaultPermissions.IsNullOrWhiteSpace() + ? Enumerable.Empty() + : dto.DefaultPermissions!.ToCharArray().Select(x => x.ToString(CultureInfo.InvariantCulture)).ToList(), + dto.Icon); - try + try + { + userGroup.DisableChangeTracking(); + userGroup.Id = dto.Id; + userGroup.CreateDate = dto.CreateDate; + userGroup.UpdateDate = dto.UpdateDate; + userGroup.StartContentId = dto.StartContentId; + userGroup.StartMediaId = dto.StartMediaId; + if (dto.UserGroup2AppDtos != null) { - userGroup.DisableChangeTracking(); - userGroup.Id = dto.Id; - userGroup.CreateDate = dto.CreateDate; - userGroup.UpdateDate = dto.UpdateDate; - userGroup.StartContentId = dto.StartContentId; - userGroup.StartMediaId = dto.StartMediaId; - if (dto.UserGroup2AppDtos != null) + foreach (UserGroup2AppDto app in dto.UserGroup2AppDtos) { - foreach (var app in dto.UserGroup2AppDtos) - { - userGroup.AddAllowedSection(app.AppAlias); - } + userGroup.AddAllowedSection(app.AppAlias); } - - userGroup.ResetDirtyProperties(false); - return userGroup; - } - finally - { - userGroup.EnableChangeTracking(); } + + userGroup.ResetDirtyProperties(false); + return userGroup; + } + finally + { + userGroup.EnableChangeTracking(); } + } - public static UserGroupDto BuildDto(IUserGroup entity) + public static UserGroupDto BuildDto(IUserGroup entity) + { + var dto = new UserGroupDto { - var dto = new UserGroupDto - { - Alias = entity.Alias, - DefaultPermissions = entity.Permissions == null ? "" : string.Join("", entity.Permissions), - Name = entity.Name, - UserGroup2AppDtos = new List(), - CreateDate = entity.CreateDate, - UpdateDate = entity.UpdateDate, - Icon = entity.Icon, - StartMediaId = entity.StartMediaId, - StartContentId = entity.StartContentId - }; + Alias = entity.Alias, + DefaultPermissions = entity.Permissions == null ? "" : string.Join("", entity.Permissions), + Name = entity.Name, + UserGroup2AppDtos = new List(), + CreateDate = entity.CreateDate, + UpdateDate = entity.UpdateDate, + Icon = entity.Icon, + StartMediaId = entity.StartMediaId, + StartContentId = entity.StartContentId + }; - foreach (var app in entity.AllowedSections) + foreach (var app in entity.AllowedSections) + { + var appDto = new UserGroup2AppDto {AppAlias = app}; + if (entity.HasIdentity) { - var appDto = new UserGroup2AppDto - { - AppAlias = app - }; - if (entity.HasIdentity) - { - appDto.UserGroupId = entity.Id; - } - - dto.UserGroup2AppDtos.Add(appDto); + appDto.UserGroupId = entity.Id; } - if (entity.HasIdentity) - dto.Id = short.Parse(entity.Id.ToString()); + dto.UserGroup2AppDtos.Add(appDto); + } - return dto; + if (entity.HasIdentity) + { + dto.Id = short.Parse(entity.Id.ToString()); } + return dto; } } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/ITransientErrorDetectionStrategy.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/ITransientErrorDetectionStrategy.cs index 59d4f1c0b7af..345815cc62db 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/ITransientErrorDetectionStrategy.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/ITransientErrorDetectionStrategy.cs @@ -1,17 +1,15 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling; -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling +/// +/// Defines an interface which must be implemented by custom components responsible for detecting specific transient +/// conditions. +/// +public interface ITransientErrorDetectionStrategy { /// - /// Defines an interface which must be implemented by custom components responsible for detecting specific transient conditions. + /// Determines whether the specified exception represents a transient failure that can be compensated by a retry. /// - public interface ITransientErrorDetectionStrategy - { - /// - /// Determines whether the specified exception represents a transient failure that can be compensated by a retry. - /// - /// The exception object to be verified. - /// True if the specified exception is considered as transient, otherwise false. - bool IsTransient(Exception ex); - } + /// The exception object to be verified. + /// True if the specified exception is considered as transient, otherwise false. + bool IsTransient(Exception ex); } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryDbConnection.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryDbConnection.cs index 57b14bf0a9ae..9238603f99c1 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryDbConnection.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryDbConnection.cs @@ -1,239 +1,195 @@ -using System; -using System.Data; +using System.Data; using System.Data.Common; using System.Diagnostics.CodeAnalysis; -using Transaction = System.Transactions.Transaction; +using System.Transactions; +using IsolationLevel = System.Data.IsolationLevel; -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling; + +public class RetryDbConnection : DbConnection { - public class RetryDbConnection : DbConnection + private readonly RetryPolicy? _cmdRetryPolicy; + private readonly RetryPolicy _conRetryPolicy; + + public RetryDbConnection(DbConnection connection, RetryPolicy? conRetryPolicy, RetryPolicy? cmdRetryPolicy) { - private DbConnection _inner; - private readonly RetryPolicy _conRetryPolicy; - private readonly RetryPolicy? _cmdRetryPolicy; + Inner = connection; + Inner.StateChange += StateChangeHandler; - public RetryDbConnection(DbConnection connection, RetryPolicy? conRetryPolicy, RetryPolicy? cmdRetryPolicy) - { - _inner = connection; - _inner.StateChange += StateChangeHandler; + _conRetryPolicy = conRetryPolicy ?? RetryPolicy.NoRetry; + _cmdRetryPolicy = cmdRetryPolicy; + } - _conRetryPolicy = conRetryPolicy ?? RetryPolicy.NoRetry; - _cmdRetryPolicy = cmdRetryPolicy; - } + public DbConnection Inner { get; } - public DbConnection Inner { get { return _inner; } } + [AllowNull] + public override string ConnectionString + { + get => Inner.ConnectionString; + set => Inner.ConnectionString = value; + } - [AllowNull] - public override string ConnectionString { get { return _inner.ConnectionString; } set { _inner.ConnectionString = value; } } + protected override bool CanRaiseEvents => true; - protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) - { - return _inner.BeginTransaction(isolationLevel); - } + public override int ConnectionTimeout => Inner.ConnectionTimeout; - protected override bool CanRaiseEvents - { - get { return true; } - } + public override string DataSource => Inner.DataSource; - public override void ChangeDatabase(string databaseName) - { - _inner.ChangeDatabase(databaseName); - } + public override string Database => Inner.Database; - public override void Close() - { - _inner.Close(); - } + public override string ServerVersion => Inner.ServerVersion; - public override int ConnectionTimeout - { - get { return _inner.ConnectionTimeout; } - } + public override ConnectionState State => Inner.State; - protected override DbCommand CreateDbCommand() - { - return new FaultHandlingDbCommand(this, _inner.CreateCommand(), _cmdRetryPolicy); - } + protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) => + Inner.BeginTransaction(isolationLevel); - public override string DataSource - { - get { return _inner.DataSource; } - } + public override void ChangeDatabase(string databaseName) => Inner.ChangeDatabase(databaseName); - public override string Database - { - get { return _inner.Database; } - } + public override void Close() => Inner.Close(); - protected override void Dispose(bool disposing) - { - if (disposing && _inner != null) - { - _inner.StateChange -= StateChangeHandler; - _inner.Dispose(); - } - base.Dispose(disposing); - } + protected override DbCommand CreateDbCommand() => + new FaultHandlingDbCommand(this, Inner.CreateCommand(), _cmdRetryPolicy); - public override void EnlistTransaction(Transaction? transaction) + protected override void Dispose(bool disposing) + { + if (disposing && Inner != null) { - _inner.EnlistTransaction(transaction); + Inner.StateChange -= StateChangeHandler; + Inner.Dispose(); } - public override DataTable GetSchema() - { - return _inner.GetSchema(); - } + base.Dispose(disposing); + } - public override DataTable GetSchema(string collectionName) - { - return _inner.GetSchema(collectionName); - } + public override void EnlistTransaction(Transaction? transaction) => Inner.EnlistTransaction(transaction); - public override DataTable GetSchema(string collectionName, string?[] restrictionValues) - { - return _inner.GetSchema(collectionName, restrictionValues); - } + public override DataTable GetSchema() => Inner.GetSchema(); - public override void Open() - { - _conRetryPolicy.ExecuteAction(_inner.Open); - } + public override DataTable GetSchema(string collectionName) => Inner.GetSchema(collectionName); - public override string ServerVersion - { - get { return _inner.ServerVersion; } - } + public override DataTable GetSchema(string collectionName, string?[] restrictionValues) => + Inner.GetSchema(collectionName, restrictionValues); - public override ConnectionState State - { - get { return _inner.State; } - } + public override void Open() => _conRetryPolicy.ExecuteAction(Inner.Open); - private void StateChangeHandler(object sender, StateChangeEventArgs stateChangeEventArguments) - { - OnStateChange(stateChangeEventArguments); - } + private void StateChangeHandler(object sender, StateChangeEventArgs stateChangeEventArguments) => + OnStateChange(stateChangeEventArguments); - public void Ensure() + public void Ensure() + { + // verify whether or not the connection is valid and is open. This code may be retried therefore + // it is important to ensure that a connection is re-established should it have previously failed + if (State != ConnectionState.Open) { - // verify whether or not the connection is valid and is open. This code may be retried therefore - // it is important to ensure that a connection is re-established should it have previously failed - if (State != ConnectionState.Open) - Open(); + Open(); } } +} - class FaultHandlingDbCommand : DbCommand - { - private RetryDbConnection _connection; - private DbCommand _inner; - private readonly RetryPolicy _cmdRetryPolicy; +internal class FaultHandlingDbCommand : DbCommand +{ + private readonly RetryPolicy _cmdRetryPolicy; + private RetryDbConnection _connection; - public FaultHandlingDbCommand(RetryDbConnection connection, DbCommand command, RetryPolicy? cmdRetryPolicy) - { - _connection = connection; - _inner = command; - _cmdRetryPolicy = cmdRetryPolicy ?? RetryPolicy.NoRetry; - } + public FaultHandlingDbCommand(RetryDbConnection connection, DbCommand command, RetryPolicy? cmdRetryPolicy) + { + _connection = connection; + Inner = command; + _cmdRetryPolicy = cmdRetryPolicy ?? RetryPolicy.NoRetry; + } - public DbCommand Inner => _inner; + public DbCommand Inner { get; private set; } - protected override void Dispose(bool disposing) - { - if (disposing) - _inner.Dispose(); - _inner = null!; - base.Dispose(disposing); - } + [AllowNull] + public override string CommandText + { + get => Inner.CommandText; + set => Inner.CommandText = value; + } - public override void Cancel() - { - _inner.Cancel(); - } + public override int CommandTimeout + { + get => Inner.CommandTimeout; + set => Inner.CommandTimeout = value; + } - [AllowNull] - public override string CommandText - { - get => _inner.CommandText; - set => _inner.CommandText = value; - } + public override CommandType CommandType + { + get => Inner.CommandType; + set => Inner.CommandType = value; + } - public override int CommandTimeout + [AllowNull] + protected override DbConnection DbConnection + { + get => _connection; + set { - get => _inner.CommandTimeout; - set => _inner.CommandTimeout = value; - } + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } - public override CommandType CommandType - { - get => _inner.CommandType; - set => _inner.CommandType = value; - } + if (!(value is RetryDbConnection connection)) + { + throw new ArgumentException("Value is not a FaultHandlingDbConnection instance."); + } - [AllowNull] - protected override DbConnection DbConnection - { - get => _connection; - set + if (_connection != null && _connection != connection) { - if (value == null) throw new ArgumentNullException(nameof(value)); - if (!(value is RetryDbConnection connection)) throw new ArgumentException("Value is not a FaultHandlingDbConnection instance."); - if (_connection != null && _connection != connection) throw new Exception("Value is another FaultHandlingDbConnection instance."); - _connection = connection; - _inner.Connection = connection.Inner; + throw new Exception("Value is another FaultHandlingDbConnection instance."); } - } - protected override DbParameter CreateDbParameter() - { - return _inner.CreateParameter(); + _connection = connection; + Inner.Connection = connection.Inner; } + } - protected override DbParameterCollection DbParameterCollection => _inner.Parameters; + protected override DbParameterCollection DbParameterCollection => Inner.Parameters; - protected override DbTransaction? DbTransaction - { - get => _inner.Transaction; - set => _inner.Transaction = value; - } + protected override DbTransaction? DbTransaction + { + get => Inner.Transaction; + set => Inner.Transaction = value; + } - public override bool DesignTimeVisible { get; set; } + public override bool DesignTimeVisible { get; set; } - protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) - { - return Execute(() => _inner.ExecuteReader(behavior)); - } + public override UpdateRowSource UpdatedRowSource + { + get => Inner.UpdatedRowSource; + set => Inner.UpdatedRowSource = value; + } - public override int ExecuteNonQuery() + protected override void Dispose(bool disposing) + { + if (disposing) { - return Execute(() => _inner.ExecuteNonQuery()); + Inner.Dispose(); } - public override object? ExecuteScalar() - { - return Execute(() => _inner.ExecuteScalar()); - } + Inner = null!; + base.Dispose(disposing); + } - private T Execute(Func f) - { - return _cmdRetryPolicy.ExecuteAction(() => - { - _connection.Ensure(); - return f(); - })!; - } + public override void Cancel() => Inner.Cancel(); - public override void Prepare() - { - _inner.Prepare(); - } + protected override DbParameter CreateDbParameter() => Inner.CreateParameter(); - public override UpdateRowSource UpdatedRowSource + protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) => + Execute(() => Inner.ExecuteReader(behavior)); + + public override int ExecuteNonQuery() => Execute(() => Inner.ExecuteNonQuery()); + + public override object? ExecuteScalar() => Execute(() => Inner.ExecuteScalar()); + + private T Execute(Func f) => + _cmdRetryPolicy.ExecuteAction(() => { - get => _inner.UpdatedRowSource; - set => _inner.UpdatedRowSource = value; - } - } + _connection.Ensure(); + return f(); + })!; + + public override void Prepare() => Inner.Prepare(); } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryLimitExceededException.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryLimitExceededException.cs index 78c8ab9c2549..e5965868913e 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryLimitExceededException.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryLimitExceededException.cs @@ -1,54 +1,64 @@ -using System; -using System.Runtime.Serialization; +using System.Runtime.Serialization; -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling; + +/// +/// The special type of exception that provides managed exit from a retry loop. The user code can use this exception to +/// notify the retry policy that no further retry attempts are required. +/// +/// +[Serializable] +public sealed class RetryLimitExceededException : Exception { /// - /// The special type of exception that provides managed exit from a retry loop. The user code can use this exception to notify the retry policy that no further retry attempts are required. + /// Initializes a new instance of the class with a default error message. /// - /// - [Serializable] - public sealed class RetryLimitExceededException : Exception + public RetryLimitExceededException() { - /// - /// Initializes a new instance of the class with a default error message. - /// - public RetryLimitExceededException() - : base() - { } + } - /// - /// Initializes a new instance of the class with a specified error message. - /// - /// The message that describes the error. - public RetryLimitExceededException(string message) - : base(message) - { } + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public RetryLimitExceededException(string message) + : base(message) + { + } - /// - /// Initializes a new instance of the class with a reference to the inner exception that is the cause of this exception. - /// - /// The exception that is the cause of the current exception. - public RetryLimitExceededException(Exception innerException) - : base(null, innerException) - { } + /// + /// Initializes a new instance of the class with a reference to the inner + /// exception that is the cause of this exception. + /// + /// The exception that is the cause of the current exception. + public RetryLimitExceededException(Exception innerException) + : base(null, innerException) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - /// The exception that is the cause of the current exception. - public RetryLimitExceededException(string message, Exception innerException) - : base(message, innerException) - { } + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + /// The exception that is the cause of the current exception. + public RetryLimitExceededException(string message, Exception innerException) + : base(message, innerException) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - private RetryLimitExceededException(SerializationInfo info, StreamingContext context) - : base(info, context) - { } + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + private RetryLimitExceededException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryPolicy.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryPolicy.cs index 82e4f20c502c..5f0b958a5485 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryPolicy.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryPolicy.cs @@ -1,239 +1,268 @@ -using System; -using System.Threading; -using Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies; +using Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies; -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling; + +/// +/// Provides the base implementation of the retry mechanism for unreliable actions and transient conditions. +/// +public class RetryPolicy { /// - /// Provides the base implementation of the retry mechanism for unreliable actions and transient conditions. + /// Returns a default policy that does no retries, it just invokes action exactly once. /// - public class RetryPolicy - { - /// - /// Returns a default policy that does no retries, it just invokes action exactly once. - /// - public static readonly RetryPolicy NoRetry = new RetryPolicy(new TransientErrorIgnoreStrategy(), 0); - - /// - /// Returns a default policy that implements a fixed retry interval configured with the default retry strategy. - /// The default retry policy treats all caught exceptions as transient errors. - /// - public static readonly RetryPolicy DefaultFixed = new RetryPolicy(new TransientErrorCatchAllStrategy(), new FixedInterval()); - - /// - /// Returns a default policy that implements a progressive retry interval configured with the default retry strategy. - /// The default retry policy treats all caught exceptions as transient errors. - /// - public static readonly RetryPolicy DefaultProgressive = new RetryPolicy(new TransientErrorCatchAllStrategy(), new Incremental()); + public static readonly RetryPolicy NoRetry = new(new TransientErrorIgnoreStrategy(), 0); - /// - /// Returns a default policy that implements a random exponential retry interval configured with the default retry strategy. - /// The default retry policy treats all caught exceptions as transient errors. - /// - public static readonly RetryPolicy DefaultExponential = new RetryPolicy(new TransientErrorCatchAllStrategy(), new ExponentialBackoff()); + /// + /// Returns a default policy that implements a fixed retry interval configured with the default + /// retry strategy. + /// The default retry policy treats all caught exceptions as transient errors. + /// + public static readonly RetryPolicy DefaultFixed = new(new TransientErrorCatchAllStrategy(), new FixedInterval()); - /// - /// Initializes a new instance of the RetryPolicy class with the specified number of retry attempts and parameters defining the progressive delay between retries. - /// - /// The that is responsible for detecting transient conditions. - /// The retry strategy to use for this retry policy. - public RetryPolicy(ITransientErrorDetectionStrategy errorDetectionStrategy, RetryStrategy retryStrategy) - { - //Guard.ArgumentNotNull(errorDetectionStrategy, "errorDetectionStrategy"); - //Guard.ArgumentNotNull(retryStrategy, "retryPolicy"); + /// + /// Returns a default policy that implements a progressive retry interval configured with the default + /// retry strategy. + /// The default retry policy treats all caught exceptions as transient errors. + /// + public static readonly RetryPolicy + DefaultProgressive = new(new TransientErrorCatchAllStrategy(), new Incremental()); - this.ErrorDetectionStrategy = errorDetectionStrategy; + /// + /// Returns a default policy that implements a random exponential retry interval configured with the default + /// retry strategy. + /// The default retry policy treats all caught exceptions as transient errors. + /// + public static readonly RetryPolicy DefaultExponential = + new(new TransientErrorCatchAllStrategy(), new ExponentialBackoff()); - if (errorDetectionStrategy == null) - { - throw new InvalidOperationException("The error detection strategy type must implement the ITransientErrorDetectionStrategy interface."); - } + /// + /// Initializes a new instance of the RetryPolicy class with the specified number of retry attempts and parameters + /// defining the progressive delay between retries. + /// + /// + /// The that is responsible for + /// detecting transient conditions. + /// + /// The retry strategy to use for this retry policy. + public RetryPolicy(ITransientErrorDetectionStrategy errorDetectionStrategy, RetryStrategy retryStrategy) + { + //Guard.ArgumentNotNull(errorDetectionStrategy, "errorDetectionStrategy"); + //Guard.ArgumentNotNull(retryStrategy, "retryPolicy"); - this.RetryStrategy = retryStrategy; - } + ErrorDetectionStrategy = errorDetectionStrategy; - /// - /// Initializes a new instance of the RetryPolicy class with the specified number of retry attempts and default fixed time interval between retries. - /// - /// The that is responsible for detecting transient conditions. - /// The number of retry attempts. - public RetryPolicy(ITransientErrorDetectionStrategy errorDetectionStrategy, int retryCount) - : this(errorDetectionStrategy, new FixedInterval(retryCount)) + if (errorDetectionStrategy == null) { + throw new InvalidOperationException( + "The error detection strategy type must implement the ITransientErrorDetectionStrategy interface."); } - /// - /// Initializes a new instance of the RetryPolicy class with the specified number of retry attempts and fixed time interval between retries. - /// - /// The that is responsible for detecting transient conditions. - /// The number of retry attempts. - /// The interval between retries. - public RetryPolicy(ITransientErrorDetectionStrategy errorDetectionStrategy, int retryCount, TimeSpan retryInterval) - : this(errorDetectionStrategy, new FixedInterval(retryCount, retryInterval)) - { - } + RetryStrategy = retryStrategy; + } - /// - /// Initializes a new instance of the RetryPolicy class with the specified number of retry attempts and back-off parameters for calculating the exponential delay between retries. - /// - /// The that is responsible for detecting transient conditions. - /// The number of retry attempts. - /// The minimum back-off time. - /// The maximum back-off time. - /// The time value that will be used for calculating a random delta in the exponential delay between retries. - public RetryPolicy(ITransientErrorDetectionStrategy errorDetectionStrategy, int retryCount, TimeSpan minBackoff, TimeSpan maxBackoff, TimeSpan deltaBackoff) - : this(errorDetectionStrategy, new ExponentialBackoff(retryCount, minBackoff, maxBackoff, deltaBackoff)) - { - } + /// + /// Initializes a new instance of the RetryPolicy class with the specified number of retry attempts and default fixed + /// time interval between retries. + /// + /// + /// The that is responsible for + /// detecting transient conditions. + /// + /// The number of retry attempts. + public RetryPolicy(ITransientErrorDetectionStrategy errorDetectionStrategy, int retryCount) + : this(errorDetectionStrategy, new FixedInterval(retryCount)) + { + } - /// - /// Initializes a new instance of the RetryPolicy class with the specified number of retry attempts and parameters defining the progressive delay between retries. - /// - /// The that is responsible for detecting transient conditions. - /// The number of retry attempts. - /// The initial interval that will apply for the first retry. - /// The incremental time value that will be used for calculating the progressive delay between retries. - public RetryPolicy(ITransientErrorDetectionStrategy errorDetectionStrategy, int retryCount, TimeSpan initialInterval, TimeSpan increment) - : this(errorDetectionStrategy, new Incremental(retryCount, initialInterval, increment)) - { - } + /// + /// Initializes a new instance of the RetryPolicy class with the specified number of retry attempts and fixed time + /// interval between retries. + /// + /// + /// The that is responsible for + /// detecting transient conditions. + /// + /// The number of retry attempts. + /// The interval between retries. + public RetryPolicy(ITransientErrorDetectionStrategy errorDetectionStrategy, int retryCount, TimeSpan retryInterval) + : this(errorDetectionStrategy, new FixedInterval(retryCount, retryInterval)) + { + } - /// - /// An instance of a callback delegate that will be invoked whenever a retry condition is encountered. - /// - public event EventHandler? Retrying; + /// + /// Initializes a new instance of the RetryPolicy class with the specified number of retry attempts and back-off + /// parameters for calculating the exponential delay between retries. + /// + /// + /// The that is responsible for + /// detecting transient conditions. + /// + /// The number of retry attempts. + /// The minimum back-off time. + /// The maximum back-off time. + /// + /// The time value that will be used for calculating a random delta in the exponential delay + /// between retries. + /// + public RetryPolicy(ITransientErrorDetectionStrategy errorDetectionStrategy, int retryCount, TimeSpan minBackoff, + TimeSpan maxBackoff, TimeSpan deltaBackoff) + : this(errorDetectionStrategy, new ExponentialBackoff(retryCount, minBackoff, maxBackoff, deltaBackoff)) + { + } - /// - /// Gets the retry strategy. - /// - public RetryStrategy RetryStrategy { get; private set; } + /// + /// Initializes a new instance of the RetryPolicy class with the specified number of retry attempts and parameters + /// defining the progressive delay between retries. + /// + /// + /// The that is responsible for + /// detecting transient conditions. + /// + /// The number of retry attempts. + /// The initial interval that will apply for the first retry. + /// + /// The incremental time value that will be used for calculating the progressive delay between + /// retries. + /// + public RetryPolicy(ITransientErrorDetectionStrategy errorDetectionStrategy, int retryCount, + TimeSpan initialInterval, TimeSpan increment) + : this(errorDetectionStrategy, new Incremental(retryCount, initialInterval, increment)) + { + } - /// - /// Gets the instance of the error detection strategy. - /// - public ITransientErrorDetectionStrategy ErrorDetectionStrategy { get; private set; } + /// + /// Gets the retry strategy. + /// + public RetryStrategy RetryStrategy { get; } - /// - /// Repetitively executes the specified action while it satisfies the current retry policy. - /// - /// A delegate representing the executable action which doesn't return any results. - public virtual void ExecuteAction(Action action) - { - //Guard.ArgumentNotNull(action, "action"); + /// + /// Gets the instance of the error detection strategy. + /// + public ITransientErrorDetectionStrategy ErrorDetectionStrategy { get; } - this.ExecuteAction(() => { action(); return default(object); }); - } + /// + /// An instance of a callback delegate that will be invoked whenever a retry condition is encountered. + /// + public event EventHandler? Retrying; - /// - /// Repetitively executes the specified action while it satisfies the current retry policy. - /// - /// The type of result expected from the executable action. - /// A delegate representing the executable action which returns the result of type R. - /// The result from the action. - public virtual TResult? ExecuteAction(Func func) + /// + /// Repetitively executes the specified action while it satisfies the current retry policy. + /// + /// A delegate representing the executable action which doesn't return any results. + public virtual void ExecuteAction(Action action) => + //Guard.ArgumentNotNull(action, "action"); + ExecuteAction(() => { - //Guard.ArgumentNotNull(func, "func"); + action(); + return default(object); + }); + + /// + /// Repetitively executes the specified action while it satisfies the current retry policy. + /// + /// The type of result expected from the executable action. + /// A delegate representing the executable action which returns the result of type R. + /// The result from the action. + public virtual TResult? ExecuteAction(Func func) + { + //Guard.ArgumentNotNull(func, "func"); - int retryCount = 0; - TimeSpan delay = TimeSpan.Zero; - Exception? lastError; + var retryCount = 0; + TimeSpan delay = TimeSpan.Zero; + Exception? lastError; - var shouldRetry = this.RetryStrategy.GetShouldRetry(); + ShouldRetry shouldRetry = RetryStrategy.GetShouldRetry(); - for (; ; ) - { - lastError = null; + for (;;) + { + lastError = null; - try - { - return func(); - } - catch (RetryLimitExceededException limitExceededEx) + try + { + return func(); + } + catch (RetryLimitExceededException limitExceededEx) + { + // The user code can throw a RetryLimitExceededException to force the exit from the retry loop. + // The RetryLimitExceeded exception can have an inner exception attached to it. This is the exception + // which we will have to throw up the stack so that callers can handle it. + if (limitExceededEx.InnerException != null) { - // The user code can throw a RetryLimitExceededException to force the exit from the retry loop. - // The RetryLimitExceeded exception can have an inner exception attached to it. This is the exception - // which we will have to throw up the stack so that callers can handle it. - if (limitExceededEx.InnerException != null) - { - throw limitExceededEx.InnerException; - } - else - { - return default(TResult); - } + throw limitExceededEx.InnerException; } - catch (Exception ex) - { - lastError = ex; - if (!(this.ErrorDetectionStrategy.IsTransient(lastError) && shouldRetry(retryCount++, lastError, out delay))) - { - throw; - } - } + return default; + } + catch (Exception ex) + { + lastError = ex; - // Perform an extra check in the delay interval. Should prevent from accidentally ending up with the value of -1 that will block a thread indefinitely. - // In addition, any other negative numbers will cause an ArgumentOutOfRangeException fault that will be thrown by Thread.Sleep. - if (delay.TotalMilliseconds < 0) + if (!(ErrorDetectionStrategy.IsTransient(lastError) && shouldRetry(retryCount++, lastError, out delay))) { - delay = TimeSpan.Zero; + throw; } + } - this.OnRetrying(retryCount, lastError, delay); + // Perform an extra check in the delay interval. Should prevent from accidentally ending up with the value of -1 that will block a thread indefinitely. + // In addition, any other negative numbers will cause an ArgumentOutOfRangeException fault that will be thrown by Thread.Sleep. + if (delay.TotalMilliseconds < 0) + { + delay = TimeSpan.Zero; + } - if (retryCount > 1 || !this.RetryStrategy.FastFirstRetry) - { - Thread.Sleep(delay); - } + OnRetrying(retryCount, lastError, delay); + + if (retryCount > 1 || !RetryStrategy.FastFirstRetry) + { + Thread.Sleep(delay); } } + } - /// - /// Notifies the subscribers whenever a retry condition is encountered. - /// - /// The current retry attempt count. - /// The exception which caused the retry conditions to occur. - /// The delay indicating how long the current thread will be suspended for before the next iteration will be invoked. - protected virtual void OnRetrying(int retryCount, Exception lastError, TimeSpan delay) + /// + /// Notifies the subscribers whenever a retry condition is encountered. + /// + /// The current retry attempt count. + /// The exception which caused the retry conditions to occur. + /// + /// The delay indicating how long the current thread will be suspended for before the next iteration + /// will be invoked. + /// + protected virtual void OnRetrying(int retryCount, Exception lastError, TimeSpan delay) + { + if (Retrying != null) { - if (this.Retrying != null) - { - this.Retrying(this, new RetryingEventArgs(retryCount, delay, lastError)); - } + Retrying(this, new RetryingEventArgs(retryCount, delay, lastError)); } + } - #region Private classes + #region Private classes + + /// + /// Implements a strategy that ignores any transient errors. + /// + private sealed class TransientErrorIgnoreStrategy : ITransientErrorDetectionStrategy + { /// - /// Implements a strategy that ignores any transient errors. + /// Always return false. /// - private sealed class TransientErrorIgnoreStrategy : ITransientErrorDetectionStrategy - { - /// - /// Always return false. - /// - /// The exception. - /// Returns false. - public bool IsTransient(Exception ex) - { - return false; - } - } + /// The exception. + /// Returns false. + public bool IsTransient(Exception ex) => false; + } + /// + /// Implements a strategy that treats all exceptions as transient errors. + /// + private sealed class TransientErrorCatchAllStrategy : ITransientErrorDetectionStrategy + { /// - /// Implements a strategy that treats all exceptions as transient errors. + /// Always return true. /// - private sealed class TransientErrorCatchAllStrategy : ITransientErrorDetectionStrategy - { - /// - /// Always return true. - /// - /// The exception. - /// Returns true. - public bool IsTransient(Exception ex) - { - return true; - } - } - #endregion + /// The exception. + /// Returns true. + public bool IsTransient(Exception ex) => true; } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryPolicyFactory.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryPolicyFactory.cs index 41f2337644be..63f7e818df7a 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryPolicyFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryPolicyFactory.cs @@ -1,59 +1,53 @@ using Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies; -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling; +// TODO: These should move to Persistence.SqlServer + +/// +/// Provides a factory class for instantiating application-specific retry policies. +/// +public static class RetryPolicyFactory { - // TODO: These should move to Persistence.SqlServer + public static RetryPolicy GetDefaultSqlConnectionRetryPolicyByConnectionString(string? connectionString) => + //Is this really the best way to determine if the database is an Azure database? + connectionString?.Contains("database.windows.net") ?? false + ? GetDefaultSqlAzureConnectionRetryPolicy() + : GetDefaultSqlConnectionRetryPolicy(); + + public static RetryPolicy GetDefaultSqlConnectionRetryPolicy() + { + RetryStrategy retryStrategy = RetryStrategy.DefaultExponential; + var retryPolicy = new RetryPolicy(new NetworkConnectivityErrorDetectionStrategy(), retryStrategy); + + return retryPolicy; + } + + public static RetryPolicy GetDefaultSqlAzureConnectionRetryPolicy() + { + RetryStrategy retryStrategy = RetryStrategy.DefaultExponential; + var retryPolicy = new RetryPolicy(new SqlAzureTransientErrorDetectionStrategy(), retryStrategy); + return retryPolicy; + } + + public static RetryPolicy GetDefaultSqlCommandRetryPolicyByConnectionString(string? connectionString) => + //Is this really the best way to determine if the database is an Azure database? + connectionString?.Contains("database.windows.net") ?? false + ? GetDefaultSqlAzureCommandRetryPolicy() + : GetDefaultSqlCommandRetryPolicy(); - /// - /// Provides a factory class for instantiating application-specific retry policies. - /// - public static class RetryPolicyFactory + public static RetryPolicy GetDefaultSqlCommandRetryPolicy() { - public static RetryPolicy GetDefaultSqlConnectionRetryPolicyByConnectionString(string? connectionString) - { - //Is this really the best way to determine if the database is an Azure database? - return connectionString?.Contains("database.windows.net") ?? false - ? GetDefaultSqlAzureConnectionRetryPolicy() - : GetDefaultSqlConnectionRetryPolicy(); - } - - public static RetryPolicy GetDefaultSqlConnectionRetryPolicy() - { - var retryStrategy = RetryStrategy.DefaultExponential; - var retryPolicy = new RetryPolicy(new NetworkConnectivityErrorDetectionStrategy(), retryStrategy); - - return retryPolicy; - } - - public static RetryPolicy GetDefaultSqlAzureConnectionRetryPolicy() - { - var retryStrategy = RetryStrategy.DefaultExponential; - var retryPolicy = new RetryPolicy(new SqlAzureTransientErrorDetectionStrategy(), retryStrategy); - return retryPolicy; - } - - public static RetryPolicy GetDefaultSqlCommandRetryPolicyByConnectionString(string? connectionString) - { - //Is this really the best way to determine if the database is an Azure database? - return connectionString?.Contains("database.windows.net") ?? false - ? GetDefaultSqlAzureCommandRetryPolicy() - : GetDefaultSqlCommandRetryPolicy(); - } - - public static RetryPolicy GetDefaultSqlCommandRetryPolicy() - { - var retryStrategy = RetryStrategy.DefaultFixed; - var retryPolicy = new RetryPolicy(new NetworkConnectivityErrorDetectionStrategy(), retryStrategy); - - return retryPolicy; - } - - public static RetryPolicy GetDefaultSqlAzureCommandRetryPolicy() - { - var retryStrategy = RetryStrategy.DefaultFixed; - var retryPolicy = new RetryPolicy(new SqlAzureTransientErrorDetectionStrategy(), retryStrategy); - - return retryPolicy; - } + RetryStrategy retryStrategy = RetryStrategy.DefaultFixed; + var retryPolicy = new RetryPolicy(new NetworkConnectivityErrorDetectionStrategy(), retryStrategy); + + return retryPolicy; + } + + public static RetryPolicy GetDefaultSqlAzureCommandRetryPolicy() + { + RetryStrategy retryStrategy = RetryStrategy.DefaultFixed; + var retryPolicy = new RetryPolicy(new SqlAzureTransientErrorDetectionStrategy(), retryStrategy); + + return retryPolicy; } } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryStrategy.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryStrategy.cs index 3f120261d7ad..c704167da97a 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryStrategy.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryStrategy.cs @@ -1,111 +1,124 @@ -using System; -using Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies; - -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling +using Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies; + +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling; + +/// +/// Defines a callback delegate that will be invoked whenever a retry condition is encountered. +/// +/// The current retry attempt count. +/// The exception which caused the retry conditions to occur. +/// +/// The delay indicating how long the current thread will be suspended for before the next iteration +/// will be invoked. +/// +/// Returns a callback delegate that will be invoked whenever to retry should be attempt. +public delegate bool ShouldRetry(int retryCount, Exception lastException, out TimeSpan delay); + +/// +/// Represents a retry strategy that determines how many times should be retried and the interval between retries. +/// +public abstract class RetryStrategy { /// - /// Defines a callback delegate that will be invoked whenever a retry condition is encountered. + /// The default amount of time defining an interval between retries. + /// + public static readonly TimeSpan DefaultRetryInterval = TimeSpan.FromSeconds(1.0); + + /// + /// Returns a default policy that does no retries, it just invokes action exactly once. + /// + public static readonly RetryStrategy NoRetry = new FixedInterval(0, DefaultRetryInterval); + + /// + /// The default number of retry attempts. + /// + public static readonly int DefaultClientRetryCount = 10; + + /// + /// Returns a default policy that implements a fixed retry interval configured with + /// and + /// parameters. + /// The default retry policy treats all caught exceptions as transient errors. + /// + public static readonly RetryStrategy + DefaultFixed = new FixedInterval(DefaultClientRetryCount, DefaultRetryInterval); + + /// + /// The default amount of time defining a time increment between retry attempts in the progressive delay policy. + /// + public static readonly TimeSpan DefaultRetryIncrement = TimeSpan.FromSeconds(1.0); + + /// + /// Returns a default policy that implements a progressive retry interval configured with + /// , and + /// parameters. + /// The default retry policy treats all caught exceptions as transient errors. + /// + public static readonly RetryStrategy DefaultProgressive = + new Incremental(DefaultClientRetryCount, DefaultRetryInterval, DefaultRetryIncrement); + + /// + /// The default amount of time used when calculating a random delta in the exponential delay between retries. + /// + public static readonly TimeSpan DefaultClientBackoff = TimeSpan.FromSeconds(10.0); + + /// + /// The default maximum amount of time used when calculating the exponential delay between retries. + /// + public static readonly TimeSpan DefaultMaxBackoff = TimeSpan.FromSeconds(30.0); + + /// + /// The default minimum amount of time used when calculating the exponential delay between retries. /// - /// The current retry attempt count. - /// The exception which caused the retry conditions to occur. - /// The delay indicating how long the current thread will be suspended for before the next iteration will be invoked. - /// Returns a callback delegate that will be invoked whenever to retry should be attempt. - public delegate bool ShouldRetry(int retryCount, Exception lastException, out TimeSpan delay); + public static readonly TimeSpan DefaultMinBackoff = TimeSpan.FromSeconds(1.0); /// - /// Represents a retry strategy that determines how many times should be retried and the interval between retries. + /// Returns a default policy that implements a random exponential retry interval configured with + /// , , + /// and parameters. + /// The default retry policy treats all caught exceptions as transient errors. /// - public abstract class RetryStrategy + public static readonly RetryStrategy DefaultExponential = new ExponentialBackoff(DefaultClientRetryCount, + DefaultMinBackoff, DefaultMaxBackoff, DefaultClientBackoff); + + /// + /// Initializes a new instance of the class. + /// + /// The name of the retry strategy. + /// + /// a value indicating whether or not the very first retry attempt will be made immediately + /// whereas the subsequent retries will remain subject to retry interval. + /// + protected RetryStrategy(string? name, bool firstFastRetry) { - #region Public members - /// - /// The default number of retry attempts. - /// - public static readonly int DefaultClientRetryCount = 10; - - /// - /// The default amount of time used when calculating a random delta in the exponential delay between retries. - /// - public static readonly TimeSpan DefaultClientBackoff = TimeSpan.FromSeconds(10.0); - - /// - /// The default maximum amount of time used when calculating the exponential delay between retries. - /// - public static readonly TimeSpan DefaultMaxBackoff = TimeSpan.FromSeconds(30.0); - - /// - /// The default minimum amount of time used when calculating the exponential delay between retries. - /// - public static readonly TimeSpan DefaultMinBackoff = TimeSpan.FromSeconds(1.0); - - /// - /// The default amount of time defining an interval between retries. - /// - public static readonly TimeSpan DefaultRetryInterval = TimeSpan.FromSeconds(1.0); - - /// - /// The default amount of time defining a time increment between retry attempts in the progressive delay policy. - /// - public static readonly TimeSpan DefaultRetryIncrement = TimeSpan.FromSeconds(1.0); - - /// - /// The default flag indicating whether or not the very first retry attempt will be made immediately - /// whereas the subsequent retries will remain subject to retry interval. - /// - public static readonly bool DefaultFirstFastRetry = true; - - #endregion - - /// - /// Returns a default policy that does no retries, it just invokes action exactly once. - /// - public static readonly RetryStrategy NoRetry = new FixedInterval(0, DefaultRetryInterval); - - /// - /// Returns a default policy that implements a fixed retry interval configured with and parameters. - /// The default retry policy treats all caught exceptions as transient errors. - /// - public static readonly RetryStrategy DefaultFixed = new FixedInterval(DefaultClientRetryCount, DefaultRetryInterval); - - /// - /// Returns a default policy that implements a progressive retry interval configured with , and parameters. - /// The default retry policy treats all caught exceptions as transient errors. - /// - public static readonly RetryStrategy DefaultProgressive = new Incremental(DefaultClientRetryCount, DefaultRetryInterval, DefaultRetryIncrement); - - /// - /// Returns a default policy that implements a random exponential retry interval configured with , , and parameters. - /// The default retry policy treats all caught exceptions as transient errors. - /// - public static readonly RetryStrategy DefaultExponential = new ExponentialBackoff(DefaultClientRetryCount, DefaultMinBackoff, DefaultMaxBackoff, DefaultClientBackoff); - - /// - /// Initializes a new instance of the class. - /// - /// The name of the retry strategy. - /// a value indicating whether or not the very first retry attempt will be made immediately - /// whereas the subsequent retries will remain subject to retry interval. - protected RetryStrategy(string? name, bool firstFastRetry) - { - this.Name = name; - this.FastFirstRetry = firstFastRetry; - } - - /// - /// Gets or sets a value indicating whether or not the very first retry attempt will be made immediately - /// whereas the subsequent retries will remain subject to retry interval. - /// - public bool FastFirstRetry { get; set; } - - /// - /// Gets the name of the retry strategy. - /// - public string? Name { get; private set; } - - /// - /// Returns the corresponding ShouldRetry delegate. - /// - /// The ShouldRetry delegate. - public abstract ShouldRetry GetShouldRetry(); + Name = name; + FastFirstRetry = firstFastRetry; } + + /// + /// Gets or sets a value indicating whether or not the very first retry attempt will be made immediately + /// whereas the subsequent retries will remain subject to retry interval. + /// + public bool FastFirstRetry { get; set; } + + /// + /// Gets the name of the retry strategy. + /// + public string? Name { get; } + + /// + /// Returns the corresponding ShouldRetry delegate. + /// + /// The ShouldRetry delegate. + public abstract ShouldRetry GetShouldRetry(); + + #region Public members + + /// + /// The default flag indicating whether or not the very first retry attempt will be made immediately + /// whereas the subsequent retries will remain subject to retry interval. + /// + public static readonly bool DefaultFirstFastRetry = true; + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryingEventArgs.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryingEventArgs.cs index 456dc873919f..5240f11a7fe7 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryingEventArgs.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryingEventArgs.cs @@ -1,40 +1,41 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling; -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling +/// +/// Contains information required for the event. +/// +public class RetryingEventArgs : EventArgs { /// - /// Contains information required for the event. + /// Initializes a new instance of the class. /// - public class RetryingEventArgs : EventArgs + /// The current retry attempt count. + /// + /// The delay indicating how long the current thread will be suspended for before the next iteration + /// will be invoked. + /// + /// The exception which caused the retry conditions to occur. + public RetryingEventArgs(int currentRetryCount, TimeSpan delay, Exception lastException) { - /// - /// Initializes a new instance of the class. - /// - /// The current retry attempt count. - /// The delay indicating how long the current thread will be suspended for before the next iteration will be invoked. - /// The exception which caused the retry conditions to occur. - public RetryingEventArgs(int currentRetryCount, TimeSpan delay, Exception lastException) - { - //Guard.ArgumentNotNull(lastException, "lastException"); + //Guard.ArgumentNotNull(lastException, "lastException"); - this.CurrentRetryCount = currentRetryCount; - this.Delay = delay; - this.LastException = lastException; - } + CurrentRetryCount = currentRetryCount; + Delay = delay; + LastException = lastException; + } - /// - /// Gets the current retry count. - /// - public int CurrentRetryCount { get; private set; } + /// + /// Gets the current retry count. + /// + public int CurrentRetryCount { get; } - /// - /// Gets the delay indicating how long the current thread will be suspended for before the next iteration will be invoked. - /// - public TimeSpan Delay { get; private set; } + /// + /// Gets the delay indicating how long the current thread will be suspended for before the next iteration will be + /// invoked. + /// + public TimeSpan Delay { get; } - /// - /// Gets the exception which caused the retry conditions to occur. - /// - public Exception LastException { get; private set; } - } + /// + /// Gets the exception which caused the retry conditions to occur. + /// + public Exception LastException { get; } } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/ExponentialBackoff.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/ExponentialBackoff.cs index 91dcaf9feb05..09e2e1bb945d 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/ExponentialBackoff.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/ExponentialBackoff.cs @@ -1,100 +1,107 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies; -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies +/// +/// A retry strategy with back-off parameters for calculating the exponential delay between retries. +/// +public class ExponentialBackoff : RetryStrategy { + private readonly TimeSpan deltaBackoff; + private readonly TimeSpan maxBackoff; + private readonly TimeSpan minBackoff; + private readonly int retryCount; + /// - /// A retry strategy with back-off parameters for calculating the exponential delay between retries. + /// Initializes a new instance of the class. /// - public class ExponentialBackoff : RetryStrategy + public ExponentialBackoff() + : this(DefaultClientRetryCount, DefaultMinBackoff, DefaultMaxBackoff, DefaultClientBackoff) { - private readonly int retryCount; - private readonly TimeSpan minBackoff; - private readonly TimeSpan maxBackoff; - private readonly TimeSpan deltaBackoff; - - /// - /// Initializes a new instance of the class. - /// - public ExponentialBackoff() - : this(DefaultClientRetryCount, DefaultMinBackoff, DefaultMaxBackoff, DefaultClientBackoff) - { - } + } - /// - /// Initializes a new instance of the class. - /// - /// The maximum number of retry attempts. - /// The minimum back-off time - /// The maximum back-off time. - /// The value that will be used for calculating a random delta in the exponential delay between retries. - public ExponentialBackoff(int retryCount, TimeSpan minBackoff, TimeSpan maxBackoff, TimeSpan deltaBackoff) - : this(null, retryCount, minBackoff, maxBackoff, deltaBackoff, DefaultFirstFastRetry) - { - } + /// + /// Initializes a new instance of the class. + /// + /// The maximum number of retry attempts. + /// The minimum back-off time + /// The maximum back-off time. + /// + /// The value that will be used for calculating a random delta in the exponential delay between + /// retries. + /// + public ExponentialBackoff(int retryCount, TimeSpan minBackoff, TimeSpan maxBackoff, TimeSpan deltaBackoff) + : this(null, retryCount, minBackoff, maxBackoff, deltaBackoff, DefaultFirstFastRetry) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The name of the retry strategy. - /// The maximum number of retry attempts. - /// The minimum back-off time - /// The maximum back-off time. - /// The value that will be used for calculating a random delta in the exponential delay between retries. - public ExponentialBackoff(string name, int retryCount, TimeSpan minBackoff, TimeSpan maxBackoff, TimeSpan deltaBackoff) - : this(name, retryCount, minBackoff, maxBackoff, deltaBackoff, DefaultFirstFastRetry) - { - } + /// + /// Initializes a new instance of the class. + /// + /// The name of the retry strategy. + /// The maximum number of retry attempts. + /// The minimum back-off time + /// The maximum back-off time. + /// + /// The value that will be used for calculating a random delta in the exponential delay between + /// retries. + /// + public ExponentialBackoff(string name, int retryCount, TimeSpan minBackoff, TimeSpan maxBackoff, + TimeSpan deltaBackoff) + : this(name, retryCount, minBackoff, maxBackoff, deltaBackoff, DefaultFirstFastRetry) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The name of the retry strategy. - /// The maximum number of retry attempts. - /// The minimum back-off time - /// The maximum back-off time. - /// The value that will be used for calculating a random delta in the exponential delay between retries. - /// - /// Indicates whether or not the very first retry attempt will be made immediately - /// whereas the subsequent retries will remain subject to retry interval. - /// - public ExponentialBackoff(string? name, int retryCount, TimeSpan minBackoff, TimeSpan maxBackoff, TimeSpan deltaBackoff, bool firstFastRetry) - : base(name, firstFastRetry) - { - //Guard.ArgumentNotNegativeValue(retryCount, "retryCount"); - //Guard.ArgumentNotNegativeValue(minBackoff.Ticks, "minBackoff"); - //Guard.ArgumentNotNegativeValue(maxBackoff.Ticks, "maxBackoff"); - //Guard.ArgumentNotNegativeValue(deltaBackoff.Ticks, "deltaBackoff"); - //Guard.ArgumentNotGreaterThan(minBackoff.TotalMilliseconds, maxBackoff.TotalMilliseconds, "minBackoff"); + /// + /// Initializes a new instance of the class. + /// + /// The name of the retry strategy. + /// The maximum number of retry attempts. + /// The minimum back-off time + /// The maximum back-off time. + /// + /// The value that will be used for calculating a random delta in the exponential delay between + /// retries. + /// + /// + /// Indicates whether or not the very first retry attempt will be made immediately + /// whereas the subsequent retries will remain subject to retry interval. + /// + public ExponentialBackoff(string? name, int retryCount, TimeSpan minBackoff, TimeSpan maxBackoff, + TimeSpan deltaBackoff, bool firstFastRetry) + : base(name, firstFastRetry) + { + //Guard.ArgumentNotNegativeValue(retryCount, "retryCount"); + //Guard.ArgumentNotNegativeValue(minBackoff.Ticks, "minBackoff"); + //Guard.ArgumentNotNegativeValue(maxBackoff.Ticks, "maxBackoff"); + //Guard.ArgumentNotNegativeValue(deltaBackoff.Ticks, "deltaBackoff"); + //Guard.ArgumentNotGreaterThan(minBackoff.TotalMilliseconds, maxBackoff.TotalMilliseconds, "minBackoff"); - this.retryCount = retryCount; - this.minBackoff = minBackoff; - this.maxBackoff = maxBackoff; - this.deltaBackoff = deltaBackoff; - } + this.retryCount = retryCount; + this.minBackoff = minBackoff; + this.maxBackoff = maxBackoff; + this.deltaBackoff = deltaBackoff; + } - /// - /// Returns the corresponding ShouldRetry delegate. - /// - /// The ShouldRetry delegate. - public override ShouldRetry GetShouldRetry() + /// + /// Returns the corresponding ShouldRetry delegate. + /// + /// The ShouldRetry delegate. + public override ShouldRetry GetShouldRetry() => + delegate(int currentRetryCount, Exception lastException, out TimeSpan retryInterval) { - return delegate(int currentRetryCount, Exception lastException, out TimeSpan retryInterval) + if (currentRetryCount < retryCount) { - if (currentRetryCount < this.retryCount) - { - var random = new Random(); + var random = new Random(); - var delta = (int)((Math.Pow(2.0, currentRetryCount) - 1.0) * random.Next((int)(this.deltaBackoff.TotalMilliseconds * 0.8), (int)(this.deltaBackoff.TotalMilliseconds * 1.2))); - var interval = (int)Math.Min(checked(this.minBackoff.TotalMilliseconds + delta), this.maxBackoff.TotalMilliseconds); + var delta = (int)((Math.Pow(2.0, currentRetryCount) - 1.0) * random.Next( + (int)(deltaBackoff.TotalMilliseconds * 0.8), (int)(deltaBackoff.TotalMilliseconds * 1.2))); + var interval = (int)Math.Min(minBackoff.TotalMilliseconds + delta, maxBackoff.TotalMilliseconds); - retryInterval = TimeSpan.FromMilliseconds(interval); + retryInterval = TimeSpan.FromMilliseconds(interval); - return true; - } + return true; + } - retryInterval = TimeSpan.Zero; - return false; - }; - } - } + retryInterval = TimeSpan.Zero; + return false; + }; } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/FixedInterval.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/FixedInterval.cs index 546b10b55aa7..1ac460e8474e 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/FixedInterval.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/FixedInterval.cs @@ -1,96 +1,96 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies; -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies +/// +/// A retry strategy with a specified number of retry attempts and a default fixed time interval between retries. +/// +public class FixedInterval : RetryStrategy { + private readonly int retryCount; + private readonly TimeSpan retryInterval; + /// - /// A retry strategy with a specified number of retry attempts and a default fixed time interval between retries. + /// Initializes a new instance of the class. /// - public class FixedInterval : RetryStrategy + public FixedInterval() + : this(DefaultClientRetryCount) { - private readonly int retryCount; - private readonly TimeSpan retryInterval; - - /// - /// Initializes a new instance of the class. - /// - public FixedInterval() - : this(DefaultClientRetryCount) - { - } + } - /// - /// Initializes a new instance of the class. - /// - /// The number of retry attempts. - public FixedInterval(int retryCount) - : this(retryCount, DefaultRetryInterval) - { - } + /// + /// Initializes a new instance of the class. + /// + /// The number of retry attempts. + public FixedInterval(int retryCount) + : this(retryCount, DefaultRetryInterval) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The number of retry attempts. - /// The time interval between retries. - public FixedInterval(int retryCount, TimeSpan retryInterval) - : this(null, retryCount, retryInterval, DefaultFirstFastRetry) - { - } + /// + /// Initializes a new instance of the class. + /// + /// The number of retry attempts. + /// The time interval between retries. + public FixedInterval(int retryCount, TimeSpan retryInterval) + : this(null, retryCount, retryInterval, DefaultFirstFastRetry) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The retry strategy name. - /// The number of retry attempts. - /// The time interval between retries. - public FixedInterval(string name, int retryCount, TimeSpan retryInterval) - : this(name, retryCount, retryInterval, DefaultFirstFastRetry) - { - } + /// + /// Initializes a new instance of the class. + /// + /// The retry strategy name. + /// The number of retry attempts. + /// The time interval between retries. + public FixedInterval(string name, int retryCount, TimeSpan retryInterval) + : this(name, retryCount, retryInterval, DefaultFirstFastRetry) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The retry strategy name. - /// The number of retry attempts. - /// The time interval between retries. - /// a value indicating whether or not the very first retry attempt will be made immediately whereas the subsequent retries will remain subject to retry interval. - public FixedInterval(string? name, int retryCount, TimeSpan retryInterval, bool firstFastRetry) - : base(name, firstFastRetry) - { - //Guard.ArgumentNotNegativeValue(retryCount, "retryCount"); - //Guard.ArgumentNotNegativeValue(retryInterval.Ticks, "retryInterval"); + /// + /// Initializes a new instance of the class. + /// + /// The retry strategy name. + /// The number of retry attempts. + /// The time interval between retries. + /// + /// a value indicating whether or not the very first retry attempt will be made immediately + /// whereas the subsequent retries will remain subject to retry interval. + /// + public FixedInterval(string? name, int retryCount, TimeSpan retryInterval, bool firstFastRetry) + : base(name, firstFastRetry) + { + //Guard.ArgumentNotNegativeValue(retryCount, "retryCount"); + //Guard.ArgumentNotNegativeValue(retryInterval.Ticks, "retryInterval"); - this.retryCount = retryCount; - this.retryInterval = retryInterval; - } + this.retryCount = retryCount; + this.retryInterval = retryInterval; + } - /// - /// Returns the corresponding ShouldRetry delegate. - /// - /// The ShouldRetry delegate. - public override ShouldRetry GetShouldRetry() + /// + /// Returns the corresponding ShouldRetry delegate. + /// + /// The ShouldRetry delegate. + public override ShouldRetry GetShouldRetry() + { + if (retryCount == 0) { - if (this.retryCount == 0) - { - return delegate(int currentRetryCount, Exception lastException, out TimeSpan interval) - { - interval = TimeSpan.Zero; - return false; - }; - } - return delegate(int currentRetryCount, Exception lastException, out TimeSpan interval) { - if (currentRetryCount < this.retryCount) - { - interval = this.retryInterval; - return true; - } - interval = TimeSpan.Zero; return false; }; } + + return delegate(int currentRetryCount, Exception lastException, out TimeSpan interval) + { + if (currentRetryCount < retryCount) + { + interval = retryInterval; + return true; + } + + interval = TimeSpan.Zero; + return false; + }; } } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/Incremental.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/Incremental.cs index 1848436ae115..ac6d235b87c2 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/Incremental.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/Incremental.cs @@ -1,86 +1,94 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies; -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies +/// +/// A retry strategy with a specified number of retry attempts and an incremental time interval between retries. +/// +public class Incremental : RetryStrategy { + private readonly TimeSpan increment; + private readonly TimeSpan initialInterval; + private readonly int retryCount; + /// - /// A retry strategy with a specified number of retry attempts and an incremental time interval between retries. + /// Initializes a new instance of the class. /// - public class Incremental : RetryStrategy + public Incremental() + : this(DefaultClientRetryCount, DefaultRetryInterval, DefaultRetryIncrement) { - private readonly int retryCount; - private readonly TimeSpan initialInterval; - private readonly TimeSpan increment; - - /// - /// Initializes a new instance of the class. - /// - public Incremental() - : this(DefaultClientRetryCount, DefaultRetryInterval, DefaultRetryIncrement) - { - } + } - /// - /// Initializes a new instance of the class. - /// - /// The number of retry attempts. - /// The initial interval that will apply for the first retry. - /// The incremental time value that will be used for calculating the progressive delay between retries. - public Incremental(int retryCount, TimeSpan initialInterval, TimeSpan increment) - : this(null, retryCount, initialInterval, increment) - { - } + /// + /// Initializes a new instance of the class. + /// + /// The number of retry attempts. + /// The initial interval that will apply for the first retry. + /// + /// The incremental time value that will be used for calculating the progressive delay between + /// retries. + /// + public Incremental(int retryCount, TimeSpan initialInterval, TimeSpan increment) + : this(null, retryCount, initialInterval, increment) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The retry strategy name. - /// The number of retry attempts. - /// The initial interval that will apply for the first retry. - /// The incremental time value that will be used for calculating the progressive delay between retries. - public Incremental(string? name, int retryCount, TimeSpan initialInterval, TimeSpan increment) - : this(name, retryCount, initialInterval, increment, DefaultFirstFastRetry) - { - } + /// + /// Initializes a new instance of the class. + /// + /// The retry strategy name. + /// The number of retry attempts. + /// The initial interval that will apply for the first retry. + /// + /// The incremental time value that will be used for calculating the progressive delay between + /// retries. + /// + public Incremental(string? name, int retryCount, TimeSpan initialInterval, TimeSpan increment) + : this(name, retryCount, initialInterval, increment, DefaultFirstFastRetry) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The retry strategy name. - /// The number of retry attempts. - /// The initial interval that will apply for the first retry. - /// The incremental time value that will be used for calculating the progressive delay between retries. - /// a value indicating whether or not the very first retry attempt will be made immediately whereas the subsequent retries will remain subject to retry interval. - public Incremental(string? name, int retryCount, TimeSpan initialInterval, TimeSpan increment, bool firstFastRetry) - : base(name, firstFastRetry) - { - //Guard.ArgumentNotNegativeValue(retryCount, "retryCount"); - //Guard.ArgumentNotNegativeValue(initialInterval.Ticks, "initialInterval"); - //Guard.ArgumentNotNegativeValue(increment.Ticks, "increment"); + /// + /// Initializes a new instance of the class. + /// + /// The retry strategy name. + /// The number of retry attempts. + /// The initial interval that will apply for the first retry. + /// + /// The incremental time value that will be used for calculating the progressive delay between + /// retries. + /// + /// + /// a value indicating whether or not the very first retry attempt will be made immediately + /// whereas the subsequent retries will remain subject to retry interval. + /// + public Incremental(string? name, int retryCount, TimeSpan initialInterval, TimeSpan increment, bool firstFastRetry) + : base(name, firstFastRetry) + { + //Guard.ArgumentNotNegativeValue(retryCount, "retryCount"); + //Guard.ArgumentNotNegativeValue(initialInterval.Ticks, "initialInterval"); + //Guard.ArgumentNotNegativeValue(increment.Ticks, "increment"); - this.retryCount = retryCount; - this.initialInterval = initialInterval; - this.increment = increment; - } + this.retryCount = retryCount; + this.initialInterval = initialInterval; + this.increment = increment; + } - /// - /// Returns the corresponding ShouldRetry delegate. - /// - /// The ShouldRetry delegate. - public override ShouldRetry GetShouldRetry() + /// + /// Returns the corresponding ShouldRetry delegate. + /// + /// The ShouldRetry delegate. + public override ShouldRetry GetShouldRetry() => + delegate(int currentRetryCount, Exception lastException, out TimeSpan retryInterval) { - return delegate(int currentRetryCount, Exception lastException, out TimeSpan retryInterval) + if (currentRetryCount < retryCount) { - if (currentRetryCount < this.retryCount) - { - retryInterval = TimeSpan.FromMilliseconds(this.initialInterval.TotalMilliseconds + (this.increment.TotalMilliseconds * currentRetryCount)); + retryInterval = TimeSpan.FromMilliseconds(initialInterval.TotalMilliseconds + + (increment.TotalMilliseconds * currentRetryCount)); - return true; - } + return true; + } - retryInterval = TimeSpan.Zero; + retryInterval = TimeSpan.Zero; - return false; - }; - } - } + return false; + }; } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/NetworkConnectivityErrorDetectionStrategy.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/NetworkConnectivityErrorDetectionStrategy.cs index fc7bb72b6bda..909b9a62ccb6 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/NetworkConnectivityErrorDetectionStrategy.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/NetworkConnectivityErrorDetectionStrategy.cs @@ -1,31 +1,29 @@ -using System; -using Microsoft.Data.SqlClient; +using Microsoft.Data.SqlClient; -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies; + +/// +/// Implements a strategy that detects network connectivity errors such as host not found. +/// +public class NetworkConnectivityErrorDetectionStrategy : ITransientErrorDetectionStrategy { - /// - /// Implements a strategy that detects network connectivity errors such as host not found. - /// - public class NetworkConnectivityErrorDetectionStrategy : ITransientErrorDetectionStrategy + public bool IsTransient(Exception ex) { - public bool IsTransient(Exception ex) - { - SqlException? sqlException; + SqlException? sqlException; - if (ex != null && (sqlException = ex as SqlException) != null) + if (ex != null && (sqlException = ex as SqlException) != null) + { + switch (sqlException.Number) { - switch (sqlException.Number) - { - // SQL Error Code: 11001 - // A network-related or instance-specific error occurred while establishing a connection to SQL Server. - // The server was not found or was not accessible. Verify that the instance name is correct and that SQL - // Server is configured to allow remote connections. (provider: TCP Provider, error: 0 - No such host is known.) - case 11001: - return true; - } + // SQL Error Code: 11001 + // A network-related or instance-specific error occurred while establishing a connection to SQL Server. + // The server was not found or was not accessible. Verify that the instance name is correct and that SQL + // Server is configured to allow remote connections. (provider: TCP Provider, error: 0 - No such host is known.) + case 11001: + return true; } - - return false; } + + return false; } } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/SqlAzureTransientErrorDetectionStrategy.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/SqlAzureTransientErrorDetectionStrategy.cs index 2711ce471427..c56d631aded7 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/SqlAzureTransientErrorDetectionStrategy.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/SqlAzureTransientErrorDetectionStrategy.cs @@ -1,174 +1,171 @@ -using System; -using Microsoft.Data.SqlClient; +using Microsoft.Data.SqlClient; -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies; +// See https://docs.microsoft.com/en-us/azure/azure-sql/database/troubleshoot-common-connectivity-issues +// Also we could just use the nuget package instead https://www.nuget.org/packages/EnterpriseLibrary.TransientFaultHandling/ ? +// but i guess that's not netcore so we'll just leave it. + +/// +/// Provides the transient error detection logic for transient faults that are specific to SQL Azure. +/// +public class SqlAzureTransientErrorDetectionStrategy : ITransientErrorDetectionStrategy { - // See https://docs.microsoft.com/en-us/azure/azure-sql/database/troubleshoot-common-connectivity-issues - // Also we could just use the nuget package instead https://www.nuget.org/packages/EnterpriseLibrary.TransientFaultHandling/ ? - // but i guess that's not netcore so we'll just leave it. + #region ITransientErrorDetectionStrategy implementation /// - /// Provides the transient error detection logic for transient faults that are specific to SQL Azure. + /// Determines whether the specified exception represents a transient failure that can be compensated by a retry. /// - public class SqlAzureTransientErrorDetectionStrategy : ITransientErrorDetectionStrategy + /// The exception object to be verified. + /// true if the specified exception is considered as transient; otherwise, false. + public bool IsTransient(Exception ex) { - #region ProcessNetLibErrorCode enumeration - - /// - /// Error codes reported by the DBNETLIB module. - /// - private enum ProcessNetLibErrorCode + if (ex != null) { - ZeroBytes = -3, - - Timeout = -2, /* Timeout expired. The timeout period elapsed prior to completion of the operation or the server is not responding. */ + SqlException? sqlException; + if ((sqlException = ex as SqlException) != null) + { + // Enumerate through all errors found in the exception. + foreach (SqlError err in sqlException.Errors) + { + switch (err.Number) + { + // SQL Error Code: 40501 + // The service is currently busy. Retry the request after 10 seconds. Code: (reason code to be decoded). + case ThrottlingCondition.ThrottlingErrorNumber: + // Decode the reason code from the error message to determine the grounds for throttling. + var condition = ThrottlingCondition.FromError(err); + + // Attach the decoded values as additional attributes to the original SQL exception. + sqlException.Data[condition.ThrottlingMode.GetType().Name] = + condition.ThrottlingMode.ToString(); + sqlException.Data[condition.GetType().Name] = condition; + + return true; + + // SQL Error Code: 10928 + // Resource ID: %d. The %s limit for the database is %d and has been reached. + case 10928: + // SQL Error Code: 10929 + // Resource ID: %d. The %s minimum guarantee is %d, maximum limit is %d and the current usage for the database is %d. + // However, the server is currently too busy to support requests greater than %d for this database. + case 10929: + // SQL Error Code: 10053 + // A transport-level error has occurred when receiving results from the server. + // An established connection was aborted by the software in your host machine. + case 10053: + // SQL Error Code: 10054 + // A transport-level error has occurred when sending the request to the server. + // (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.) + case 10054: + // SQL Error Code: 10060 + // A network-related or instance-specific error occurred while establishing a connection to SQL Server. + // The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server + // is configured to allow remote connections. (provider: TCP Provider, error: 0 - A connection attempt failed + // because the connected party did not properly respond after a period of time, or established connection failed + // because connected host has failed to respond.)"} + case 10060: + // SQL Error Code: 40197 + // The service has encountered an error processing your request. Please try again. + case 40197: + // SQL Error Code: 40540 + // The service has encountered an error processing your request. Please try again. + case 40540: + // SQL Error Code: 40613 + // Database XXXX on server YYYY is not currently available. Please retry the connection later. If the problem persists, contact customer + // support, and provide them the session tracing ID of ZZZZZ. + case 40613: + // SQL Error Code: 40143 + // The service has encountered an error processing your request. Please try again. + case 40143: + // SQL Error Code: 233 + // The client was unable to establish a connection because of an error during connection initialization process before login. + // Possible causes include the following: the client tried to connect to an unsupported version of SQL Server; the server was too busy + // to accept new connections; or there was a resource limitation (insufficient memory or maximum allowed connections) on the server. + // (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.) + case 233: + // SQL Error Code: 64 + // A connection was successfully established with the server, but then an error occurred during the login process. + // (provider: TCP Provider, error: 0 - The specified network name is no longer available.) + case 64: + // DBNETLIB Error Code: 20 + // The instance of SQL Server you attempted to connect to does not support encryption. + case (int)ProcessNetLibErrorCode.EncryptionNotSupported: + return true; + } + } + } + else if (ex is TimeoutException) + { + return true; + } + // else + // { + // EntityException entityException; + // if ((entityException = ex as EntityException) != null) + // { + // return this.IsTransient(entityException.InnerException); + // } + // } + } - Unknown = -1, + return false; + } - InsufficientMemory = 1, + #endregion - AccessDenied = 2, + #region ProcessNetLibErrorCode enumeration - ConnectionBusy = 3, + /// + /// Error codes reported by the DBNETLIB module. + /// + private enum ProcessNetLibErrorCode + { + ZeroBytes = -3, - ConnectionBroken = 4, + Timeout = -2, /* Timeout expired. The timeout period elapsed prior to completion of the operation or the server is not responding. */ - ConnectionLimit = 5, + Unknown = -1, - ServerNotFound = 6, + InsufficientMemory = 1, - NetworkNotFound = 7, + AccessDenied = 2, - InsufficientResources = 8, + ConnectionBusy = 3, - NetworkBusy = 9, + ConnectionBroken = 4, - NetworkAccessDenied = 10, + ConnectionLimit = 5, - GeneralError = 11, + ServerNotFound = 6, - IncorrectMode = 12, + NetworkNotFound = 7, - NameNotFound = 13, + InsufficientResources = 8, - InvalidConnection = 14, + NetworkBusy = 9, - ReadWriteError = 15, + NetworkAccessDenied = 10, - TooManyHandles = 16, + GeneralError = 11, - ServerError = 17, + IncorrectMode = 12, - SSLError = 18, + NameNotFound = 13, - EncryptionError = 19, + InvalidConnection = 14, - EncryptionNotSupported = 20 - } + ReadWriteError = 15, - #endregion + TooManyHandles = 16, - #region ITransientErrorDetectionStrategy implementation + ServerError = 17, - /// - /// Determines whether the specified exception represents a transient failure that can be compensated by a retry. - /// - /// The exception object to be verified. - /// true if the specified exception is considered as transient; otherwise, false. - public bool IsTransient(Exception ex) - { - if (ex != null) - { - SqlException? sqlException; - if ((sqlException = ex as SqlException) != null) - { - // Enumerate through all errors found in the exception. - foreach (SqlError err in sqlException.Errors) - { - switch (err.Number) - { - // SQL Error Code: 40501 - // The service is currently busy. Retry the request after 10 seconds. Code: (reason code to be decoded). - case ThrottlingCondition.ThrottlingErrorNumber: - // Decode the reason code from the error message to determine the grounds for throttling. - var condition = ThrottlingCondition.FromError(err); - - // Attach the decoded values as additional attributes to the original SQL exception. - sqlException.Data[condition.ThrottlingMode.GetType().Name] = - condition.ThrottlingMode.ToString(); - sqlException.Data[condition.GetType().Name] = condition; - - return true; - - // SQL Error Code: 10928 - // Resource ID: %d. The %s limit for the database is %d and has been reached. - case 10928: - // SQL Error Code: 10929 - // Resource ID: %d. The %s minimum guarantee is %d, maximum limit is %d and the current usage for the database is %d. - // However, the server is currently too busy to support requests greater than %d for this database. - case 10929: - // SQL Error Code: 10053 - // A transport-level error has occurred when receiving results from the server. - // An established connection was aborted by the software in your host machine. - case 10053: - // SQL Error Code: 10054 - // A transport-level error has occurred when sending the request to the server. - // (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.) - case 10054: - // SQL Error Code: 10060 - // A network-related or instance-specific error occurred while establishing a connection to SQL Server. - // The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server - // is configured to allow remote connections. (provider: TCP Provider, error: 0 - A connection attempt failed - // because the connected party did not properly respond after a period of time, or established connection failed - // because connected host has failed to respond.)"} - case 10060: - // SQL Error Code: 40197 - // The service has encountered an error processing your request. Please try again. - case 40197: - // SQL Error Code: 40540 - // The service has encountered an error processing your request. Please try again. - case 40540: - // SQL Error Code: 40613 - // Database XXXX on server YYYY is not currently available. Please retry the connection later. If the problem persists, contact customer - // support, and provide them the session tracing ID of ZZZZZ. - case 40613: - // SQL Error Code: 40143 - // The service has encountered an error processing your request. Please try again. - case 40143: - // SQL Error Code: 233 - // The client was unable to establish a connection because of an error during connection initialization process before login. - // Possible causes include the following: the client tried to connect to an unsupported version of SQL Server; the server was too busy - // to accept new connections; or there was a resource limitation (insufficient memory or maximum allowed connections) on the server. - // (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.) - case 233: - // SQL Error Code: 64 - // A connection was successfully established with the server, but then an error occurred during the login process. - // (provider: TCP Provider, error: 0 - The specified network name is no longer available.) - case 64: - // DBNETLIB Error Code: 20 - // The instance of SQL Server you attempted to connect to does not support encryption. - case (int)ProcessNetLibErrorCode.EncryptionNotSupported: - return true; - } - } - } - else if (ex is TimeoutException) - { - return true; - } - // else - // { - // EntityException entityException; - // if ((entityException = ex as EntityException) != null) - // { - // return this.IsTransient(entityException.InnerException); - // } - // } - } + SSLError = 18, - return false; - } + EncryptionError = 19, - #endregion + EncryptionNotSupported = 20 } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/ThrottlingCondition.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/ThrottlingCondition.cs index 96d42a948172..c49c7822c347 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/ThrottlingCondition.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/ThrottlingCondition.cs @@ -1,330 +1,341 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; +using System.Globalization; using System.Text; using System.Text.RegularExpressions; using Microsoft.Data.SqlClient; -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling; + +/// +/// Defines the possible throttling modes in SQL Azure. +/// +public enum ThrottlingMode { /// - /// Defines the possible throttling modes in SQL Azure. + /// Corresponds to "No Throttling" throttling mode whereby all SQL statements can be processed. /// - public enum ThrottlingMode - { - /// - /// Corresponds to "No Throttling" throttling mode whereby all SQL statements can be processed. - /// - NoThrottling = 0, - - /// - /// Corresponds to "Reject Update / Insert" throttling mode whereby SQL statements such as INSERT, UPDATE, CREATE TABLE and CREATE INDEX are rejected. - /// - RejectUpdateInsert = 1, - - /// - /// Corresponds to "Reject All Writes" throttling mode whereby SQL statements such as INSERT, UPDATE, DELETE, CREATE, DROP are rejected. - /// - RejectAllWrites = 2, - - /// - /// Corresponds to "Reject All" throttling mode whereby all SQL statements are rejected. - /// - RejectAll = 3, - - /// - /// Corresponds to an unknown throttling mode whereby throttling mode cannot be determined with certainty. - /// - Unknown = -1 - } + NoThrottling = 0, /// - /// Defines the possible throttling types in SQL Azure. + /// Corresponds to "Reject Update / Insert" throttling mode whereby SQL statements such as INSERT, UPDATE, CREATE TABLE + /// and CREATE INDEX are rejected. /// - public enum ThrottlingType - { - /// - /// Indicates that no throttling was applied to a given resource. - /// - None = 0, - - /// - /// Corresponds to a Soft throttling type. Soft throttling is applied when machine resources such as, CPU, IO, storage, and worker threads exceed - /// predefined safety thresholds despite the load balancer’s best efforts. - /// - Soft = 1, - - /// - /// Corresponds to a Hard throttling type. Hard throttling is applied when the machine is out of resources, for example storage space. - /// With hard throttling, no new connections are allowed to the databases hosted on the machine until resources are freed up. - /// - Hard = 2, - - /// - /// Corresponds to an unknown throttling type in the event when the throttling type cannot be determined with certainty. - /// - Unknown = 3 - } + RejectUpdateInsert = 1, /// - /// Defines the types of resources in SQL Azure which may be subject to throttling conditions. + /// Corresponds to "Reject All Writes" throttling mode whereby SQL statements such as INSERT, UPDATE, DELETE, CREATE, + /// DROP are rejected. /// - public enum ThrottledResourceType - { - /// - /// Corresponds to "Physical Database Space" resource which may be subject to throttling. - /// - PhysicalDatabaseSpace = 0, - - /// - /// Corresponds to "Physical Log File Space" resource which may be subject to throttling. - /// - PhysicalLogSpace = 1, - - /// - /// Corresponds to "Transaction Log Write IO Delay" resource which may be subject to throttling. - /// - LogWriteIoDelay = 2, - - /// - /// Corresponds to "Database Read IO Delay" resource which may be subject to throttling. - /// - DataReadIoDelay = 3, - - /// - /// Corresponds to "CPU" resource which may be subject to throttling. - /// - Cpu = 4, - - /// - /// Corresponds to "Database Size" resource which may be subject to throttling. - /// - DatabaseSize = 5, - - /// - /// Corresponds to "SQL Worker Thread Pool" resource which may be subject to throttling. - /// - WorkerThreads = 7, - - /// - /// Corresponds to an internal resource which may be subject to throttling. - /// - Internal = 6, - - /// - /// Corresponds to an unknown resource type in the event when the actual resource cannot be determined with certainty. - /// - Unknown = -1 - } + RejectAllWrites = 2, + + /// + /// Corresponds to "Reject All" throttling mode whereby all SQL statements are rejected. + /// + RejectAll = 3, + + /// + /// Corresponds to an unknown throttling mode whereby throttling mode cannot be determined with certainty. + /// + Unknown = -1 +} + +/// +/// Defines the possible throttling types in SQL Azure. +/// +public enum ThrottlingType +{ + /// + /// Indicates that no throttling was applied to a given resource. + /// + None = 0, + + /// + /// Corresponds to a Soft throttling type. Soft throttling is applied when machine resources such as, CPU, IO, storage, + /// and worker threads exceed + /// predefined safety thresholds despite the load balancer’s best efforts. + /// + Soft = 1, + + /// + /// Corresponds to a Hard throttling type. Hard throttling is applied when the machine is out of resources, for example + /// storage space. + /// With hard throttling, no new connections are allowed to the databases hosted on the machine until resources are + /// freed up. + /// + Hard = 2, + + /// + /// Corresponds to an unknown throttling type in the event when the throttling type cannot be determined with + /// certainty. + /// + Unknown = 3 +} + +/// +/// Defines the types of resources in SQL Azure which may be subject to throttling conditions. +/// +public enum ThrottledResourceType +{ + /// + /// Corresponds to "Physical Database Space" resource which may be subject to throttling. + /// + PhysicalDatabaseSpace = 0, + + /// + /// Corresponds to "Physical Log File Space" resource which may be subject to throttling. + /// + PhysicalLogSpace = 1, /// - /// Implements an object holding the decoded reason code returned from SQL Azure when encountering throttling conditions. + /// Corresponds to "Transaction Log Write IO Delay" resource which may be subject to throttling. /// - [Serializable] - public class ThrottlingCondition + LogWriteIoDelay = 2, + + /// + /// Corresponds to "Database Read IO Delay" resource which may be subject to throttling. + /// + DataReadIoDelay = 3, + + /// + /// Corresponds to "CPU" resource which may be subject to throttling. + /// + Cpu = 4, + + /// + /// Corresponds to "Database Size" resource which may be subject to throttling. + /// + DatabaseSize = 5, + + /// + /// Corresponds to "SQL Worker Thread Pool" resource which may be subject to throttling. + /// + WorkerThreads = 7, + + /// + /// Corresponds to an internal resource which may be subject to throttling. + /// + Internal = 6, + + /// + /// Corresponds to an unknown resource type in the event when the actual resource cannot be determined with certainty. + /// + Unknown = -1 +} + +/// +/// Implements an object holding the decoded reason code returned from SQL Azure when encountering throttling +/// conditions. +/// +[Serializable] +public class ThrottlingCondition +{ + /// + /// Gets the error number that corresponds to throttling conditions reported by SQL Azure. + /// + public const int ThrottlingErrorNumber = 40501; + + /// + /// Provides a compiled regular expression used for extracting the reason code from the error message. + /// + private static readonly Regex sqlErrorCodeRegEx = + new(@"Code:\s*(\d+)", RegexOptions.IgnoreCase | RegexOptions.Compiled); + + /// + /// Maintains a collection of key-value pairs where a key is resource type and a value is the type of throttling + /// applied to the given resource type. + /// + private readonly IList> throttledResources = + new List>(9); + + /// + /// Gets an unknown throttling condition in the event the actual throttling condition cannot be determined. + /// + public static ThrottlingCondition Unknown { - /// - /// Gets the error number that corresponds to throttling conditions reported by SQL Azure. - /// - public const int ThrottlingErrorNumber = 40501; - - /// - /// Maintains a collection of key-value pairs where a key is resource type and a value is the type of throttling applied to the given resource type. - /// - private readonly IList> throttledResources = new List>(9); - - /// - /// Provides a compiled regular expression used for extracting the reason code from the error message. - /// - private static readonly Regex sqlErrorCodeRegEx = new Regex(@"Code:\s*(\d+)", RegexOptions.IgnoreCase | RegexOptions.Compiled); - - /// - /// Gets an unknown throttling condition in the event the actual throttling condition cannot be determined. - /// - public static ThrottlingCondition Unknown + get { - get - { - var unknownCondition = new ThrottlingCondition { ThrottlingMode = ThrottlingMode.Unknown }; - unknownCondition.throttledResources.Add(Tuple.Create(ThrottledResourceType.Unknown, ThrottlingType.Unknown)); + var unknownCondition = new ThrottlingCondition {ThrottlingMode = ThrottlingMode.Unknown}; + unknownCondition.throttledResources.Add(Tuple.Create(ThrottledResourceType.Unknown, + ThrottlingType.Unknown)); - return unknownCondition; - } + return unknownCondition; } + } - /// - /// Gets the value that reflects the throttling mode in SQL Azure. - /// - public ThrottlingMode ThrottlingMode { get; private set; } + /// + /// Gets the value that reflects the throttling mode in SQL Azure. + /// + public ThrottlingMode ThrottlingMode { get; private set; } - /// - /// Gets a list of resources in SQL Azure that were subject to throttling conditions. - /// - public IEnumerable> ThrottledResources - { - get { return this.throttledResources; } - } + /// + /// Gets a list of resources in SQL Azure that were subject to throttling conditions. + /// + public IEnumerable> ThrottledResources => throttledResources; - /// - /// Gets a value indicating whether physical data file space throttling was reported by SQL Azure. - /// - public bool IsThrottledOnDataSpace - { - get { return throttledResources.Any(x => x.Item1 == ThrottledResourceType.PhysicalDatabaseSpace); } - } + /// + /// Gets a value indicating whether physical data file space throttling was reported by SQL Azure. + /// + public bool IsThrottledOnDataSpace => + throttledResources.Any(x => x.Item1 == ThrottledResourceType.PhysicalDatabaseSpace); - /// - /// Gets a value indicating whether physical log space throttling was reported by SQL Azure. - /// - public bool IsThrottledOnLogSpace - { - get { return this.throttledResources.Any(x => x.Item1 == ThrottledResourceType.PhysicalLogSpace); } - } + /// + /// Gets a value indicating whether physical log space throttling was reported by SQL Azure. + /// + public bool IsThrottledOnLogSpace => throttledResources.Any(x => x.Item1 == ThrottledResourceType.PhysicalLogSpace); - /// - /// Gets a value indicating whether transaction activity throttling was reported by SQL Azure. - /// - public bool IsThrottledOnLogWrite - { - get { return this.throttledResources.Any(x => x.Item1 == ThrottledResourceType.LogWriteIoDelay); } - } + /// + /// Gets a value indicating whether transaction activity throttling was reported by SQL Azure. + /// + public bool IsThrottledOnLogWrite => throttledResources.Any(x => x.Item1 == ThrottledResourceType.LogWriteIoDelay); - /// - /// Gets a value indicating whether data read activity throttling was reported by SQL Azure. - /// - public bool IsThrottledOnDataRead - { - get { return this.throttledResources.Any(x => x.Item1 == ThrottledResourceType.DataReadIoDelay); } - } + /// + /// Gets a value indicating whether data read activity throttling was reported by SQL Azure. + /// + public bool IsThrottledOnDataRead => throttledResources.Any(x => x.Item1 == ThrottledResourceType.DataReadIoDelay); - /// - /// Gets a value indicating whether CPU throttling was reported by SQL Azure. - /// - public bool IsThrottledOnCpu - { - get { return this.throttledResources.Any(x => x.Item1 == ThrottledResourceType.Cpu); } - } + /// + /// Gets a value indicating whether CPU throttling was reported by SQL Azure. + /// + public bool IsThrottledOnCpu => throttledResources.Any(x => x.Item1 == ThrottledResourceType.Cpu); - /// - /// Gets a value indicating whether database size throttling was reported by SQL Azure. - /// - public bool IsThrottledOnDatabaseSize - { - get { return this.throttledResources.Any(x => x.Item1 == ThrottledResourceType.DatabaseSize); } - } + /// + /// Gets a value indicating whether database size throttling was reported by SQL Azure. + /// + public bool IsThrottledOnDatabaseSize => throttledResources.Any(x => x.Item1 == ThrottledResourceType.DatabaseSize); - /// - /// Gets a value indicating whether concurrent requests throttling was reported by SQL Azure. - /// - public bool IsThrottledOnWorkerThreads - { - get { return this.throttledResources.Any(x => x.Item1 == ThrottledResourceType.WorkerThreads); } - } + /// + /// Gets a value indicating whether concurrent requests throttling was reported by SQL Azure. + /// + public bool IsThrottledOnWorkerThreads => + throttledResources.Any(x => x.Item1 == ThrottledResourceType.WorkerThreads); - /// - /// Gets a value indicating whether throttling conditions were not determined with certainty. - /// - public bool IsUnknown - { - get { return ThrottlingMode == ThrottlingMode.Unknown; } - } + /// + /// Gets a value indicating whether throttling conditions were not determined with certainty. + /// + public bool IsUnknown => ThrottlingMode == ThrottlingMode.Unknown; - /// - /// Determines throttling conditions from the specified SQL exception. - /// - /// The object containing information relevant to an error returned by SQL Server when encountering throttling conditions. - /// An instance of the object holding the decoded reason codes returned from SQL Azure upon encountering throttling conditions. - public static ThrottlingCondition FromException(SqlException ex) + /// + /// Determines throttling conditions from the specified SQL exception. + /// + /// + /// The object containing information relevant to an error returned by SQL + /// Server when encountering throttling conditions. + /// + /// + /// An instance of the object holding the decoded reason codes returned from SQL Azure upon encountering + /// throttling conditions. + /// + public static ThrottlingCondition FromException(SqlException ex) + { + if (ex != null) { - if (ex != null) + foreach (SqlError error in ex.Errors) { - foreach (SqlError error in ex.Errors) + if (error.Number == ThrottlingErrorNumber) { - if (error.Number == ThrottlingErrorNumber) - { - return FromError(error); - } + return FromError(error); } } - - return Unknown; } - /// - /// Determines the throttling conditions from the specified SQL error. - /// - /// The object containing information relevant to a warning or error returned by SQL Server. - /// An instance of the object holding the decoded reason codes returned from SQL Azure when encountering throttling conditions. - public static ThrottlingCondition FromError(SqlError error) + return Unknown; + } + + /// + /// Determines the throttling conditions from the specified SQL error. + /// + /// + /// The object containing information relevant to a warning or error returned + /// by SQL Server. + /// + /// + /// An instance of the object holding the decoded reason codes returned from SQL Azure when encountering + /// throttling conditions. + /// + public static ThrottlingCondition FromError(SqlError error) + { + if (error != null) { - if (error != null) - { - var match = sqlErrorCodeRegEx.Match(error.Message); - int reasonCode; + Match match = sqlErrorCodeRegEx.Match(error.Message); + int reasonCode; - if (match.Success && int.TryParse(match.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out reasonCode)) - { - return FromReasonCode(reasonCode); - } + if (match.Success && int.TryParse(match.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, + out reasonCode)) + { + return FromReasonCode(reasonCode); } - - return Unknown; } - /// - /// Determines the throttling conditions from the specified reason code. - /// - /// The reason code returned by SQL Azure which contains the throttling mode and the exceeded resource types. - /// An instance of the object holding the decoded reason codes returned from SQL Azure when encountering throttling conditions. - public static ThrottlingCondition FromReasonCode(int reasonCode) - { - if (reasonCode > 0) - { - // Decode throttling mode from the last 2 bits. - var throttlingMode = (ThrottlingMode)(reasonCode & 3); - - var condition = new ThrottlingCondition { ThrottlingMode = throttlingMode }; - - // Shift 8 bits to truncate throttling mode. - var groupCode = reasonCode >> 8; - - // Determine throttling type for all well-known resources that may be subject to throttling conditions. - condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.PhysicalDatabaseSpace, (ThrottlingType)(groupCode & 3))); - condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.PhysicalLogSpace, (ThrottlingType)((groupCode = groupCode >> 2) & 3))); - condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.LogWriteIoDelay, (ThrottlingType)((groupCode = groupCode >> 2) & 3))); - condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.DataReadIoDelay, (ThrottlingType)((groupCode = groupCode >> 2) & 3))); - condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.Cpu, (ThrottlingType)((groupCode = groupCode >> 2) & 3))); - condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.DatabaseSize, (ThrottlingType)((groupCode = groupCode >> 2) & 3))); - condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.Internal, (ThrottlingType)((groupCode = groupCode >> 2) & 3))); - condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.WorkerThreads, (ThrottlingType)((groupCode = groupCode >> 2) & 3))); - condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.Internal, (ThrottlingType)((groupCode >> 2) & 3))); - - return condition; - } + return Unknown; + } - return Unknown; + /// + /// Determines the throttling conditions from the specified reason code. + /// + /// + /// The reason code returned by SQL Azure which contains the throttling mode and the exceeded + /// resource types. + /// + /// + /// An instance of the object holding the decoded reason codes returned from SQL Azure when encountering + /// throttling conditions. + /// + public static ThrottlingCondition FromReasonCode(int reasonCode) + { + if (reasonCode > 0) + { + // Decode throttling mode from the last 2 bits. + var throttlingMode = (ThrottlingMode)(reasonCode & 3); + + var condition = new ThrottlingCondition {ThrottlingMode = throttlingMode}; + + // Shift 8 bits to truncate throttling mode. + var groupCode = reasonCode >> 8; + + // Determine throttling type for all well-known resources that may be subject to throttling conditions. + condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.PhysicalDatabaseSpace, + (ThrottlingType)(groupCode & 3))); + condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.PhysicalLogSpace, + (ThrottlingType)((groupCode = groupCode >> 2) & 3))); + condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.LogWriteIoDelay, + (ThrottlingType)((groupCode = groupCode >> 2) & 3))); + condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.DataReadIoDelay, + (ThrottlingType)((groupCode = groupCode >> 2) & 3))); + condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.Cpu, + (ThrottlingType)((groupCode = groupCode >> 2) & 3))); + condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.DatabaseSize, + (ThrottlingType)((groupCode = groupCode >> 2) & 3))); + condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.Internal, + (ThrottlingType)((groupCode = groupCode >> 2) & 3))); + condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.WorkerThreads, + (ThrottlingType)((groupCode = groupCode >> 2) & 3))); + condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.Internal, + (ThrottlingType)((groupCode >> 2) & 3))); + + return condition; } - /// - /// Returns a textual representation the current ThrottlingCondition object including the information held with respect to throttled resources. - /// - /// A string that represents the current ThrottlingCondition object. - public override string ToString() - { - var result = new StringBuilder(); + return Unknown; + } + + /// + /// Returns a textual representation the current ThrottlingCondition object including the information held with respect + /// to throttled resources. + /// + /// A string that represents the current ThrottlingCondition object. + public override string ToString() + { + var result = new StringBuilder(); - result.AppendFormat(CultureInfo.CurrentCulture, "Mode: {0} | ", ThrottlingMode); + result.AppendFormat(CultureInfo.CurrentCulture, "Mode: {0} | ", ThrottlingMode); - var resources = - this.throttledResources - .Where(x => x.Item1 != ThrottledResourceType.Internal) - .Select(x => string.Format(CultureInfo.CurrentCulture, "{0}: {1}", x.Item1, x.Item2)) - .OrderBy(x => x).ToArray(); + var resources = + throttledResources + .Where(x => x.Item1 != ThrottledResourceType.Internal) + .Select(x => string.Format(CultureInfo.CurrentCulture, "{0}: {1}", x.Item1, x.Item2)) + .OrderBy(x => x).ToArray(); - result.Append(string.Join(", ", resources)); + result.Append(string.Join(", ", resources)); - return result.ToString(); - } + return result.ToString(); } } diff --git a/src/Umbraco.Infrastructure/Persistence/IBulkSqlInsertProvider.cs b/src/Umbraco.Infrastructure/Persistence/IBulkSqlInsertProvider.cs index 6a928b6859de..176ee928ec72 100644 --- a/src/Umbraco.Infrastructure/Persistence/IBulkSqlInsertProvider.cs +++ b/src/Umbraco.Infrastructure/Persistence/IBulkSqlInsertProvider.cs @@ -1,10 +1,7 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Infrastructure.Persistence +public interface IBulkSqlInsertProvider { - public interface IBulkSqlInsertProvider - { - string ProviderName { get; } - int BulkInsertRecords(IUmbracoDatabase database, IEnumerable records); - } + string ProviderName { get; } + int BulkInsertRecords(IUmbracoDatabase database, IEnumerable records); } diff --git a/src/Umbraco.Infrastructure/Persistence/IDatabaseCreator.cs b/src/Umbraco.Infrastructure/Persistence/IDatabaseCreator.cs index 2d97cfbcd3e1..bf01728075e8 100644 --- a/src/Umbraco.Infrastructure/Persistence/IDatabaseCreator.cs +++ b/src/Umbraco.Infrastructure/Persistence/IDatabaseCreator.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +public interface IDatabaseCreator { - public interface IDatabaseCreator - { - string ProviderName { get; } + string ProviderName { get; } - void Create(string connectionString); - } + void Create(string connectionString); } diff --git a/src/Umbraco.Infrastructure/Persistence/IDatabaseProviderMetadata.cs b/src/Umbraco.Infrastructure/Persistence/IDatabaseProviderMetadata.cs index c766c50d695c..1c06dd089fb4 100644 --- a/src/Umbraco.Infrastructure/Persistence/IDatabaseProviderMetadata.cs +++ b/src/Umbraco.Infrastructure/Persistence/IDatabaseProviderMetadata.cs @@ -1,4 +1,3 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Install.Models; @@ -7,84 +6,84 @@ namespace Umbraco.Cms.Infrastructure.Persistence; public interface IDatabaseProviderMetadata { /// - /// Gets a unique identifier for this set of metadata used for filtering. + /// Gets a unique identifier for this set of metadata used for filtering. /// [DataMember(Name = "id")] Guid Id { get; } /// - /// Gets a value to determine display order and quick install priority. + /// Gets a value to determine display order and quick install priority. /// [DataMember(Name = "sortOrder")] int SortOrder { get; } /// - /// Gets a friendly name to describe the provider. + /// Gets a friendly name to describe the provider. /// [DataMember(Name = "displayName")] string DisplayName { get; } /// - /// Gets the default database name for the provider. + /// Gets the default database name for the provider. /// [DataMember(Name = "defaultDatabaseName")] string DefaultDatabaseName { get; } /// - /// Gets the database factory provider name. + /// Gets the database factory provider name. /// [DataMember(Name = "providerName")] string? ProviderName { get; } /// - /// Gets a value indicating whether can be used for one click install. + /// Gets a value indicating whether can be used for one click install. /// [DataMember(Name = "supportsQuickInstall")] bool SupportsQuickInstall { get; } /// - /// Gets a value indicating whether should be available for selection. + /// Gets a value indicating whether should be available for selection. /// [DataMember(Name = "isAvailable")] bool IsAvailable { get; } /// - /// Gets a value indicating whether the server/hostname field must be populated. + /// Gets a value indicating whether the server/hostname field must be populated. /// [DataMember(Name = "requiresServer")] bool RequiresServer { get; } /// - /// Gets a value used as input placeholder for server/hostnmae field. + /// Gets a value used as input placeholder for server/hostnmae field. /// [DataMember(Name = "serverPlaceholder")] string? ServerPlaceholder { get; } /// - /// Gets a value indicating whether a username and password are required (in general) to connect to the database + /// Gets a value indicating whether a username and password are required (in general) to connect to the database /// [DataMember(Name = "requiresCredentials")] bool RequiresCredentials { get; } /// - /// Gets a value indicating whether integrated authentication is supported (e.g. SQL Server & Oracle). + /// Gets a value indicating whether integrated authentication is supported (e.g. SQL Server & Oracle). /// [DataMember(Name = "supportsIntegratedAuthentication")] bool SupportsIntegratedAuthentication { get; } /// - /// Gets a value indicating whether the connection should be tested before continuing install process. + /// Gets a value indicating whether the connection should be tested before continuing install process. /// [DataMember(Name = "requiresConnectionTest")] bool RequiresConnectionTest { get; } /// - /// Gets a value indicating to ignore the value of GlobalSettings.InstallMissingDatabase + /// Gets a value indicating to ignore the value of GlobalSettings.InstallMissingDatabase /// public bool ForceCreateDatabase { get; } /// - /// Creates a connection string for this provider. + /// Creates a connection string for this provider. /// string? GenerateConnectionString(DatabaseModel databaseModel); } diff --git a/src/Umbraco.Infrastructure/Persistence/IDbProviderFactoryCreator.cs b/src/Umbraco.Infrastructure/Persistence/IDbProviderFactoryCreator.cs index 4ee6ce7d596a..e4041297f6ba 100644 --- a/src/Umbraco.Infrastructure/Persistence/IDbProviderFactoryCreator.cs +++ b/src/Umbraco.Infrastructure/Persistence/IDbProviderFactoryCreator.cs @@ -1,20 +1,16 @@ -using System.Collections.Generic; using System.Data.Common; -using System.Linq; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -namespace Umbraco.Cms.Infrastructure.Persistence -{ +namespace Umbraco.Cms.Infrastructure.Persistence; - public interface IDbProviderFactoryCreator - { - DbProviderFactory? CreateFactory(string? providerName); - ISqlSyntaxProvider GetSqlSyntaxProvider(string providerName); - IBulkSqlInsertProvider CreateBulkSqlInsertProvider(string providerName); - void CreateDatabase(string providerName, string connectionString); - NPocoMapperCollection ProviderSpecificMappers(string providerName); +public interface IDbProviderFactoryCreator +{ + DbProviderFactory? CreateFactory(string? providerName); + ISqlSyntaxProvider GetSqlSyntaxProvider(string providerName); + IBulkSqlInsertProvider CreateBulkSqlInsertProvider(string providerName); + void CreateDatabase(string providerName, string connectionString); + NPocoMapperCollection ProviderSpecificMappers(string providerName); - IEnumerable GetProviderSpecificInterceptors(string providerName) => - Enumerable.Empty(); - } + IEnumerable GetProviderSpecificInterceptors(string providerName) => + Enumerable.Empty(); } diff --git a/src/Umbraco.Infrastructure/Persistence/IProviderSpecificInterceptor.cs b/src/Umbraco.Infrastructure/Persistence/IProviderSpecificInterceptor.cs index 736ba80854db..41af78253aa9 100644 --- a/src/Umbraco.Infrastructure/Persistence/IProviderSpecificInterceptor.cs +++ b/src/Umbraco.Infrastructure/Persistence/IProviderSpecificInterceptor.cs @@ -23,6 +23,6 @@ public interface IProviderSpecificDataInterceptor : IProviderSpecificInterceptor { } -public interface IProviderSpecificTransactionInterceptor: IProviderSpecificInterceptor, ITransactionInterceptor +public interface IProviderSpecificTransactionInterceptor : IProviderSpecificInterceptor, ITransactionInterceptor { } diff --git a/src/Umbraco.Infrastructure/Persistence/IProviderSpecificMapperFactory.cs b/src/Umbraco.Infrastructure/Persistence/IProviderSpecificMapperFactory.cs index 3a73d647e817..7571fffaad98 100644 --- a/src/Umbraco.Infrastructure/Persistence/IProviderSpecificMapperFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/IProviderSpecificMapperFactory.cs @@ -1,8 +1,7 @@ -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +public interface IProviderSpecificMapperFactory { - public interface IProviderSpecificMapperFactory - { - string ProviderName { get; } - NPocoMapperCollection Mappers { get; } - } + string ProviderName { get; } + NPocoMapperCollection Mappers { get; } } diff --git a/src/Umbraco.Infrastructure/Persistence/IScalarMapper.cs b/src/Umbraco.Infrastructure/Persistence/IScalarMapper.cs index 29f1128a4465..c937880a226d 100644 --- a/src/Umbraco.Infrastructure/Persistence/IScalarMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/IScalarMapper.cs @@ -1,13 +1,12 @@ namespace Umbraco.Cms.Infrastructure.Persistence; /// -/// Provides a mapping function for +/// Provides a mapping function for /// public interface IScalarMapper { /// - /// Performs a mapping operation for a scalar value. + /// Performs a mapping operation for a scalar value. /// object Map(object value); } - diff --git a/src/Umbraco.Infrastructure/Persistence/ISqlContext.cs b/src/Umbraco.Infrastructure/Persistence/ISqlContext.cs index 9178ba8ae759..2f3defe45f9d 100644 --- a/src/Umbraco.Infrastructure/Persistence/ISqlContext.cs +++ b/src/Umbraco.Infrastructure/Persistence/ISqlContext.cs @@ -3,51 +3,50 @@ using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +/// +/// Specifies the Sql context. +/// +public interface ISqlContext { /// - /// Specifies the Sql context. - /// - public interface ISqlContext - { - /// - /// Gets the Sql syntax provider. - /// - ISqlSyntaxProvider SqlSyntax { get; } - - /// - /// Gets the database type. - /// - DatabaseType DatabaseType { get; } - - /// - /// Creates a new Sql expression. - /// - Sql Sql(); - - /// - /// Creates a new Sql expression. - /// - Sql Sql(string sql, params object[] args); - - /// - /// Creates a new query expression. - /// - IQuery Query(); - - /// - /// Gets the Sql templates. - /// - SqlTemplates Templates { get; } - - /// - /// Gets the Poco data factory. - /// - IPocoDataFactory PocoDataFactory { get; } - - /// - /// Gets the mappers. - /// - IMapperCollection? Mappers { get; } - } + /// Gets the Sql syntax provider. + /// + ISqlSyntaxProvider SqlSyntax { get; } + + /// + /// Gets the database type. + /// + DatabaseType DatabaseType { get; } + + /// + /// Gets the Sql templates. + /// + SqlTemplates Templates { get; } + + /// + /// Gets the Poco data factory. + /// + IPocoDataFactory PocoDataFactory { get; } + + /// + /// Gets the mappers. + /// + IMapperCollection? Mappers { get; } + + /// + /// Creates a new Sql expression. + /// + Sql Sql(); + + /// + /// Creates a new Sql expression. + /// + Sql Sql(string sql, params object[] args); + + /// + /// Creates a new query expression. + /// + IQuery Query(); } diff --git a/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabase.cs b/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabase.cs index c28b0d984d52..1b32c34ba2b8 100644 --- a/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabase.cs +++ b/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabase.cs @@ -1,32 +1,32 @@ -using System.Collections.Generic; -using NPoco; +using NPoco; using Umbraco.Cms.Infrastructure.Migrations.Install; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +public interface IUmbracoDatabase : IDatabase { - public interface IUmbracoDatabase : IDatabase - { - /// - /// Gets the Sql context. - /// - ISqlContext SqlContext { get; } + /// + /// Gets the Sql context. + /// + ISqlContext SqlContext { get; } - /// - /// Gets the database instance unique identifier as a string. - /// - /// UmbracoDatabase returns the first eight digits of its unique Guid and, in some - /// debug mode, the underlying database connection identifier (if any). - string InstanceId { get; } + /// + /// Gets the database instance unique identifier as a string. + /// + /// + /// UmbracoDatabase returns the first eight digits of its unique Guid and, in some + /// debug mode, the underlying database connection identifier (if any). + /// + string InstanceId { get; } - /// - /// Gets a value indicating whether the database is currently in a transaction. - /// - bool InTransaction { get; } + /// + /// Gets a value indicating whether the database is currently in a transaction. + /// + bool InTransaction { get; } - bool EnableSqlCount { get; set; } - int SqlCount { get; } - int BulkInsertRecords(IEnumerable records); - bool IsUmbracoInstalled(); - DatabaseSchemaResult ValidateSchema(); - } + bool EnableSqlCount { get; set; } + int SqlCount { get; } + int BulkInsertRecords(IEnumerable records); + bool IsUmbracoInstalled(); + DatabaseSchemaResult ValidateSchema(); } diff --git a/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabaseFactory.cs b/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabaseFactory.cs index a4d4259cbe85..972c98d151c1 100644 --- a/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabaseFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabaseFactory.cs @@ -1,82 +1,83 @@ -using System; using Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +/// +/// Creates and manages the "ambient" database. +/// +public interface IUmbracoDatabaseFactory : IDisposable { /// - /// Creates and manages the "ambient" database. + /// Gets a value indicating whether the database factory is configured, i.e. whether + /// its connection string and provider name have been set. The factory may however not + /// be initialized (see ). /// - public interface IUmbracoDatabaseFactory : IDisposable - { - /// - /// Creates a new database. - /// - /// - /// The new database must be disposed after being used. - /// Creating a database causes the factory to initialize if it is not already initialized. - /// - IUmbracoDatabase CreateDatabase(); + bool Configured { get; } - /// - /// Gets a value indicating whether the database factory is configured, i.e. whether - /// its connection string and provider name have been set. The factory may however not - /// be initialized (see ). - /// - bool Configured { get; } + /// + /// Gets a value indicating whether the database factory is initialized, i.e. whether + /// its internal state is ready and it has been possible to connect to the database. + /// + bool Initialized { get; } - /// - /// Gets a value indicating whether the database factory is initialized, i.e. whether - /// its internal state is ready and it has been possible to connect to the database. - /// - bool Initialized { get; } + /// + /// Gets the connection string. + /// + /// May return null if the database factory is not configured. + string? ConnectionString { get; } - /// - /// Gets the connection string. - /// - /// May return null if the database factory is not configured. - string? ConnectionString { get; } + /// + /// Gets the provider name. + /// + /// May return null if the database factory is not configured. + string? ProviderName { get; } - /// - /// Gets the provider name. - /// - /// May return null if the database factory is not configured. - string? ProviderName { get; } + /// + /// Gets a value indicating whether the database factory is configured (see ), + /// and it is possible to connect to the database. The factory may however not be initialized (see + /// ). + /// + bool CanConnect { get; } - /// - /// Gets a value indicating whether the database factory is configured (see ), - /// and it is possible to connect to the database. The factory may however not be initialized (see - /// ). - /// - bool CanConnect { get; } + /// + /// Gets the . + /// + /// + /// Getting the causes the factory to initialize if it is not already initialized. + /// + ISqlContext SqlContext { get; } - /// - /// Configures the database factory. - /// - void Configure(ConnectionStrings umbracoConnectionString); + /// + /// Gets the . + /// + /// + /// + /// Getting the causes the factory to initialize if it is not already + /// initialized. + /// + /// + IBulkSqlInsertProvider? BulkSqlInsertProvider { get; } - [Obsolete("Please use alternative Configure method.")] - void Configure(string connectionString, string providerName) => - Configure(new ConnectionStrings { ConnectionString = connectionString, ProviderName = providerName }); + /// + /// Creates a new database. + /// + /// + /// The new database must be disposed after being used. + /// Creating a database causes the factory to initialize if it is not already initialized. + /// + IUmbracoDatabase CreateDatabase(); - /// - /// Gets the . - /// - /// - /// Getting the causes the factory to initialize if it is not already initialized. - /// - ISqlContext SqlContext { get; } + /// + /// Configures the database factory. + /// + void Configure(ConnectionStrings umbracoConnectionString); - /// - /// Gets the . - /// - /// - /// Getting the causes the factory to initialize if it is not already initialized. - /// - IBulkSqlInsertProvider? BulkSqlInsertProvider { get; } + [Obsolete("Please use alternative Configure method.")] + void Configure(string connectionString, string providerName) => + Configure(new ConnectionStrings {ConnectionString = connectionString, ProviderName = providerName}); - /// - /// Configures the database factory for upgrades. - /// - void ConfigureForUpgrade(); - } + /// + /// Configures the database factory for upgrades. + /// + void ConfigureForUpgrade(); } diff --git a/src/Umbraco.Infrastructure/Persistence/LocalDb.cs b/src/Umbraco.Infrastructure/Persistence/LocalDb.cs index 709940f3cc16..d6c23abba824 100644 --- a/src/Umbraco.Infrastructure/Persistence/LocalDb.cs +++ b/src/Umbraco.Infrastructure/Persistence/LocalDb.cs @@ -1,961 +1,1047 @@ -using System; -using System.Collections.Generic; using System.Data; using System.Diagnostics; -using System.IO; -using System.Linq; using Microsoft.Data.SqlClient; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +/// +/// Manages LocalDB databases. +/// +/// +/// +/// Latest version is SQL Server 2016 Express LocalDB, +/// see https://docs.microsoft.com/en-us/sql/database-engine/configure-windows/sql-server-2016-express-localdb +/// which can be installed by downloading the Express installer from +/// https://www.microsoft.com/en-us/sql-server/sql-server-downloads +/// (about 5MB) then select 'download media' to download SqlLocalDB.msi (about 44MB), which you can execute. This +/// installs +/// LocalDB only. Though you probably want to install the full Express. You may also want to install SQL Server +/// Management +/// Studio which can be used to connect to LocalDB databases. +/// +/// See also https://github.com/ritterim/automation-sql which is a somewhat simpler version of this. +/// +public class LocalDb { + private string? _exe; + private bool _hasVersion; + private int _version; + + #region Availability & Version + /// - /// Manages LocalDB databases. + /// Gets the LocalDb installed version. /// /// - /// Latest version is SQL Server 2016 Express LocalDB, - /// see https://docs.microsoft.com/en-us/sql/database-engine/configure-windows/sql-server-2016-express-localdb - /// which can be installed by downloading the Express installer from https://www.microsoft.com/en-us/sql-server/sql-server-downloads - /// (about 5MB) then select 'download media' to download SqlLocalDB.msi (about 44MB), which you can execute. This installs - /// LocalDB only. Though you probably want to install the full Express. You may also want to install SQL Server Management - /// Studio which can be used to connect to LocalDB databases. - /// See also https://github.com/ritterim/automation-sql which is a somewhat simpler version of this. + /// If more than one version is installed, returns the highest available. Returns + /// the major version as an integer e.g. 11, 12... /// - public class LocalDb + /// Thrown when LocalDb is not available. + public int Version { - private int _version; - private bool _hasVersion; - private string? _exe; - - #region Availability & Version - - /// - /// Gets the LocalDb installed version. - /// - /// If more than one version is installed, returns the highest available. Returns - /// the major version as an integer e.g. 11, 12... - /// Thrown when LocalDb is not available. - public int Version + get { - get + EnsureVersion(); + if (_version <= 0) { - EnsureVersion(); - if (_version <= 0) - throw new InvalidOperationException("LocalDb is not available."); - return _version; + throw new InvalidOperationException("LocalDb is not available."); } + + return _version; } + } - /// - /// Ensures that the LocalDb version is detected. - /// - private void EnsureVersion() + /// + /// Ensures that the LocalDb version is detected. + /// + private void EnsureVersion() + { + if (_hasVersion) { - if (_hasVersion) return; - DetectVersion(); - _hasVersion = true; + return; } - /// - /// Gets a value indicating whether LocalDb is available. - /// - public bool IsAvailable + DetectVersion(); + _hasVersion = true; + } + + /// + /// Gets a value indicating whether LocalDb is available. + /// + public bool IsAvailable + { + get { - get - { - EnsureVersion(); - return _version > 0; - } + EnsureVersion(); + return _version > 0; } + } - /// - /// Ensures that LocalDb is available. - /// - /// Thrown when LocalDb is not available. - private void EnsureAvailable() + /// + /// Ensures that LocalDb is available. + /// + /// Thrown when LocalDb is not available. + private void EnsureAvailable() + { + if (IsAvailable == false) { - if (IsAvailable == false) - throw new InvalidOperationException("LocalDb is not available."); + throw new InvalidOperationException("LocalDb is not available."); } + } - /// - /// Detects LocalDb installed version. - /// - /// If more than one version is installed, the highest available is detected. - private void DetectVersion() - { - _hasVersion = true; - _version = -1; - _exe = null; - - var programFiles = Environment.GetEnvironmentVariable("ProgramFiles"); - - // MS SQL Server installs in e.g. "C:\Program Files\Microsoft SQL Server", so - // we want to detect it in "%ProgramFiles%\Microsoft SQL Server" - however, if - // Umbraco runs as a 32bits process (e.g. IISExpress configured as 32bits) - // on a 64bits system, %ProgramFiles% will point to "C:\Program Files (x86)" - // and SQL Server cannot be found. But then, %ProgramW6432% will point to - // the original "C:\Program Files". Using it to fix the path. - // see also: MSDN doc for WOW64 implementation - // - var programW6432 = Environment.GetEnvironmentVariable("ProgramW6432"); - if (string.IsNullOrWhiteSpace(programW6432) == false && programW6432 != programFiles) - programFiles = programW6432; - - if (string.IsNullOrWhiteSpace(programFiles)) return; - - // detect 15, 14, 13, 12, 11 - for (var i = 15; i > 10; i--) + /// + /// Detects LocalDb installed version. + /// + /// If more than one version is installed, the highest available is detected. + private void DetectVersion() + { + _hasVersion = true; + _version = -1; + _exe = null; + + var programFiles = Environment.GetEnvironmentVariable("ProgramFiles"); + + // MS SQL Server installs in e.g. "C:\Program Files\Microsoft SQL Server", so + // we want to detect it in "%ProgramFiles%\Microsoft SQL Server" - however, if + // Umbraco runs as a 32bits process (e.g. IISExpress configured as 32bits) + // on a 64bits system, %ProgramFiles% will point to "C:\Program Files (x86)" + // and SQL Server cannot be found. But then, %ProgramW6432% will point to + // the original "C:\Program Files". Using it to fix the path. + // see also: MSDN doc for WOW64 implementation + // + var programW6432 = Environment.GetEnvironmentVariable("ProgramW6432"); + if (string.IsNullOrWhiteSpace(programW6432) == false && programW6432 != programFiles) + { + programFiles = programW6432; + } + + if (string.IsNullOrWhiteSpace(programFiles)) + { + return; + } + + // detect 15, 14, 13, 12, 11 + for (var i = 15; i > 10; i--) + { + var exe = Path.Combine(programFiles, $@"Microsoft SQL Server\{i}0\Tools\Binn\SqlLocalDB.exe"); + if (File.Exists(exe) == false) { - var exe = Path.Combine(programFiles, $@"Microsoft SQL Server\{i}0\Tools\Binn\SqlLocalDB.exe"); - if (File.Exists(exe) == false) continue; - _version = i; - _exe = exe; - break; + continue; } + + _version = i; + _exe = exe; + break; } + } - #endregion + #endregion - #region Instances + #region Instances - /// - /// Gets the name of existing LocalDb instances. - /// - /// The name of existing LocalDb instances. - /// Thrown when LocalDb is not available. - public string[]? GetInstances() + /// + /// Gets the name of existing LocalDb instances. + /// + /// The name of existing LocalDb instances. + /// Thrown when LocalDb is not available. + public string[]? GetInstances() + { + EnsureAvailable(); + var rc = ExecuteSqlLocalDb("i", out var output, out var error); // info + if (rc != 0 || error != string.Empty) { - EnsureAvailable(); - var rc = ExecuteSqlLocalDb("i", out var output, out var error); // info - if (rc != 0 || error != string.Empty) return null; - return output.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); + return null; } - /// - /// Gets a value indicating whether a LocalDb instance exists. - /// - /// The name of the instance. - /// A value indicating whether a LocalDb instance with the specified name exists. - /// Thrown when LocalDb is not available. - public bool InstanceExists(string instanceName) + return output.Split(new[] {Environment.NewLine}, StringSplitOptions.RemoveEmptyEntries); + } + + /// + /// Gets a value indicating whether a LocalDb instance exists. + /// + /// The name of the instance. + /// A value indicating whether a LocalDb instance with the specified name exists. + /// Thrown when LocalDb is not available. + public bool InstanceExists(string instanceName) + { + EnsureAvailable(); + var instances = GetInstances(); + return instances != null && instances.Contains(instanceName, StringComparer.OrdinalIgnoreCase); + } + + /// + /// Creates a LocalDb instance. + /// + /// The name of the instance. + /// A value indicating whether the instance was created without errors. + /// Thrown when LocalDb is not available. + public bool CreateInstance(string instanceName) + { + EnsureAvailable(); + return ExecuteSqlLocalDb($"c \"{instanceName}\"", out _, out var error) == 0 && error == string.Empty; + } + + /// + /// Drops a LocalDb instance. + /// + /// The name of the instance. + /// A value indicating whether the instance was dropped without errors. + /// Thrown when LocalDb is not available. + /// + /// When an instance is dropped all the attached database files are deleted. + /// Successful if the instance does not exist. + /// + public bool DropInstance(string instanceName) + { + EnsureAvailable(); + Instance? instance = GetInstance(instanceName); + if (instance == null) { - EnsureAvailable(); - var instances = GetInstances(); - return instances != null && instances.Contains(instanceName, StringComparer.OrdinalIgnoreCase); + return true; } - /// - /// Creates a LocalDb instance. - /// - /// The name of the instance. - /// A value indicating whether the instance was created without errors. - /// Thrown when LocalDb is not available. - public bool CreateInstance(string instanceName) + instance.DropDatabases(); // else the files remain + + // -i force NOWAIT, -k kills + return ExecuteSqlLocalDb($"p \"{instanceName}\" -i", out _, out var error) == 0 && error == string.Empty + && ExecuteSqlLocalDb($"d \"{instanceName}\"", out _, out error) == 0 && error == string.Empty; + } + + /// + /// Stops a LocalDb instance. + /// + /// The name of the instance. + /// A value indicating whether the instance was stopped without errors. + /// Thrown when LocalDb is not available. + /// + /// Successful if the instance does not exist. + /// + public bool StopInstance(string instanceName) + { + EnsureAvailable(); + if (InstanceExists(instanceName) == false) { - EnsureAvailable(); - return ExecuteSqlLocalDb($"c \"{instanceName}\"", out _, out var error) == 0 && error == string.Empty; + return true; } - /// - /// Drops a LocalDb instance. - /// - /// The name of the instance. - /// A value indicating whether the instance was dropped without errors. - /// Thrown when LocalDb is not available. - /// - /// When an instance is dropped all the attached database files are deleted. - /// Successful if the instance does not exist. - /// - public bool DropInstance(string instanceName) - { - EnsureAvailable(); - var instance = GetInstance(instanceName); - if (instance == null) return true; - instance.DropDatabases(); // else the files remain + // -i force NOWAIT, -k kills + return ExecuteSqlLocalDb($"p \"{instanceName}\" -i", out _, out var error) == 0 && error == string.Empty; + } - // -i force NOWAIT, -k kills - return ExecuteSqlLocalDb($"p \"{instanceName}\" -i", out _, out var error) == 0 && error == string.Empty - && ExecuteSqlLocalDb($"d \"{instanceName}\"", out _, out error) == 0 && error == string.Empty; + /// + /// Stops a LocalDb instance. + /// + /// The name of the instance. + /// A value indicating whether the instance was started without errors. + /// Thrown when LocalDb is not available. + /// + /// Failed if the instance does not exist. + /// + public bool StartInstance(string instanceName) + { + EnsureAvailable(); + if (InstanceExists(instanceName) == false) + { + return false; } - /// - /// Stops a LocalDb instance. - /// - /// The name of the instance. - /// A value indicating whether the instance was stopped without errors. - /// Thrown when LocalDb is not available. - /// - /// Successful if the instance does not exist. - /// - public bool StopInstance(string instanceName) - { - EnsureAvailable(); - if (InstanceExists(instanceName) == false) return true; + return ExecuteSqlLocalDb($"s \"{instanceName}\"", out _, out var error) == 0 && error == string.Empty; + } - // -i force NOWAIT, -k kills - return ExecuteSqlLocalDb($"p \"{instanceName}\" -i", out _, out var error) == 0 && error == string.Empty; - } + /// + /// Gets a LocalDb instance. + /// + /// The name of the instance. + /// The instance with the specified name if it exists, otherwise null. + /// Thrown when LocalDb is not available. + public Instance? GetInstance(string instanceName) + { + EnsureAvailable(); + return InstanceExists(instanceName) ? new Instance(instanceName) : null; + } + + #endregion + + #region Databases + + /// + /// Represents a LocalDb instance. + /// + /// + /// LocalDb is assumed to be available, and the instance is assumed to exist. + /// + public class Instance + { + private readonly string _masterCstr; /// - /// Stops a LocalDb instance. + /// Initializes a new instance of the class. /// - /// The name of the instance. - /// A value indicating whether the instance was started without errors. - /// Thrown when LocalDb is not available. - /// - /// Failed if the instance does not exist. - /// - public bool StartInstance(string instanceName) + /// + public Instance(string instanceName) { - EnsureAvailable(); - if (InstanceExists(instanceName) == false) return false; - return ExecuteSqlLocalDb($"s \"{instanceName}\"", out _, out var error) == 0 && error == string.Empty; + InstanceName = instanceName; + _masterCstr = $@"Server=(localdb)\{instanceName};Integrated Security=True;"; } /// - /// Gets a LocalDb instance. + /// Gets the name of the instance. /// - /// The name of the instance. - /// The instance with the specified name if it exists, otherwise null. - /// Thrown when LocalDb is not available. - public Instance? GetInstance(string instanceName) - { - EnsureAvailable(); - return InstanceExists(instanceName) ? new Instance(instanceName) : null; - } + public string InstanceName { get; } - #endregion + public static string GetConnectionString(string instanceName, string databaseName) => + $@"Server=(localdb)\{instanceName};Integrated Security=True;Database={databaseName};"; - #region Databases + /// + /// Gets a LocalDb connection string. + /// + /// The name of the database. + /// The connection string for the specified database. + /// + /// The database should exist in the LocalDb instance. + /// + public string GetConnectionString(string databaseName) => _masterCstr + $@"Database={databaseName};"; /// - /// Represents a LocalDb instance. + /// Gets a LocalDb connection string for an attached database. /// + /// The name of the database. + /// The directory containing database files. + /// The connection string for the specified database. /// - /// LocalDb is assumed to be available, and the instance is assumed to exist. + /// The database should not exist in the LocalDb instance. + /// It will be attached with its name being its MDF filename (full path), uppercased, when + /// the first connection is opened, and remain attached until explicitly detached. /// - public class Instance + public string GetAttachedConnectionString(string databaseName, string filesPath) { - private readonly string _masterCstr; - - /// - /// Gets the name of the instance. - /// - public string InstanceName { get; } - - /// - /// Initializes a new instance of the class. - /// - /// - public Instance(string instanceName) - { - InstanceName = instanceName; - _masterCstr = $@"Server=(localdb)\{instanceName};Integrated Security=True;"; - } - - public static string GetConnectionString(string instanceName, string databaseName) - { - return $@"Server=(localdb)\{instanceName};Integrated Security=True;Database={databaseName};"; - } - - /// - /// Gets a LocalDb connection string. - /// - /// The name of the database. - /// The connection string for the specified database. - /// - /// The database should exist in the LocalDb instance. - /// - public string GetConnectionString(string databaseName) - { - return _masterCstr + $@"Database={databaseName};"; - } + GetDatabaseFiles(databaseName, filesPath, out _, out _, out _, out var mdfFilename, out _); - /// - /// Gets a LocalDb connection string for an attached database. - /// - /// The name of the database. - /// The directory containing database files. - /// The connection string for the specified database. - /// - /// The database should not exist in the LocalDb instance. - /// It will be attached with its name being its MDF filename (full path), uppercased, when - /// the first connection is opened, and remain attached until explicitly detached. - /// - public string GetAttachedConnectionString(string databaseName, string filesPath) - { - GetDatabaseFiles(databaseName, filesPath, out _, out _, out _, out var mdfFilename, out _); + return _masterCstr + $@"AttachDbFileName='{mdfFilename}';"; + } - return _masterCstr + $@"AttachDbFileName='{mdfFilename}';"; - } + /// + /// Gets the name of existing databases. + /// + /// The name of existing databases. + public string[] GetDatabases() + { + var userDatabases = new List(); - /// - /// Gets the name of existing databases. - /// - /// The name of existing databases. - public string[] GetDatabases() + using (var conn = new SqlConnection(_masterCstr)) + using (SqlCommand? cmd = conn.CreateCommand()) { - var userDatabases = new List(); + conn.Open(); - using (var conn = new SqlConnection(_masterCstr)) - using (var cmd = conn.CreateCommand()) - { - conn.Open(); + var databases = new Dictionary(); - var databases = new Dictionary(); - - SetCommand(cmd, @" + SetCommand(cmd, @" SELECT name, filename FROM sys.sysdatabases"); - using (var reader = cmd.ExecuteReader()) + using (SqlDataReader? reader = cmd.ExecuteReader()) + { + while (reader.Read()) { - while (reader.Read()) - { - databases[reader.GetString(0)] = reader.GetString(1); - } + databases[reader.GetString(0)] = reader.GetString(1); } + } - foreach (var database in databases) - { - var dbname = database.Key; + foreach (KeyValuePair database in databases) + { + var dbname = database.Key; - if (dbname == "master" || dbname == "tempdb" || dbname == "model" || dbname == "msdb") - continue; + if (dbname == "master" || dbname == "tempdb" || dbname == "model" || dbname == "msdb") + { + continue; + } - // TODO: shall we deal with stale databases? - // TODO: is it always ok to assume file names? - //var mdf = database.Value; - //var ldf = mdf.Replace(".mdf", "_log.ldf"); - //if (staleOnly && File.Exists(mdf) && File.Exists(ldf)) - // continue; + // TODO: shall we deal with stale databases? + // TODO: is it always ok to assume file names? + //var mdf = database.Value; + //var ldf = mdf.Replace(".mdf", "_log.ldf"); + //if (staleOnly && File.Exists(mdf) && File.Exists(ldf)) + // continue; - //ExecuteDropDatabase(cmd, dbname, mdf, ldf); - //count++; + //ExecuteDropDatabase(cmd, dbname, mdf, ldf); + //count++; - userDatabases.Add(dbname); - } + userDatabases.Add(dbname); } - - return userDatabases.ToArray(); } - /// - /// Gets a value indicating whether a database exists. - /// - /// The name of the database. - /// A value indicating whether a database with the specified name exists. - /// - /// A database exists if it is registered in the instance, and its files exist. If the database - /// is registered but some of its files are missing, the database is dropped. - /// - public bool DatabaseExists(string databaseName) - { - using (var conn = new SqlConnection(_masterCstr)) - using (var cmd = conn.CreateCommand()) - { - conn.Open(); + return userDatabases.ToArray(); + } - var mdf = GetDatabase(cmd, databaseName); - if (mdf == null) return false; + /// + /// Gets a value indicating whether a database exists. + /// + /// The name of the database. + /// A value indicating whether a database with the specified name exists. + /// + /// A database exists if it is registered in the instance, and its files exist. If the database + /// is registered but some of its files are missing, the database is dropped. + /// + public bool DatabaseExists(string databaseName) + { + using (var conn = new SqlConnection(_masterCstr)) + using (SqlCommand? cmd = conn.CreateCommand()) + { + conn.Open(); - // it can exist, even though its files have been deleted - // if files exist assume all is ok (should we try to connect?) - var ldf = GetLogFilename(mdf); - if (File.Exists(mdf) && File.Exists(ldf)) - return true; + var mdf = GetDatabase(cmd, databaseName); + if (mdf == null) + { + return false; + } - ExecuteDropDatabase(cmd, databaseName, mdf, ldf); + // it can exist, even though its files have been deleted + // if files exist assume all is ok (should we try to connect?) + var ldf = GetLogFilename(mdf); + if (File.Exists(mdf) && File.Exists(ldf)) + { + return true; } - return false; + ExecuteDropDatabase(cmd, databaseName, mdf, ldf); } - /// - /// Creates a new database. - /// - /// The name of the database. - /// The directory containing database files. - /// A value indicating whether the database was created without errors. - /// - /// Failed if a database with the specified name already exists in the instance, - /// or if the database files already exist in the specified directory. - /// - public bool CreateDatabase(string databaseName, string filesPath) + return false; + } + + /// + /// Creates a new database. + /// + /// The name of the database. + /// The directory containing database files. + /// A value indicating whether the database was created without errors. + /// + /// Failed if a database with the specified name already exists in the instance, + /// or if the database files already exist in the specified directory. + /// + public bool CreateDatabase(string databaseName, string filesPath) + { + GetDatabaseFiles(databaseName, filesPath, out var logName, out _, out _, out var mdfFilename, + out var ldfFilename); + + using (var conn = new SqlConnection(_masterCstr)) + using (SqlCommand? cmd = conn.CreateCommand()) { - GetDatabaseFiles(databaseName, filesPath, out var logName, out _, out _, out var mdfFilename, out var ldfFilename); + conn.Open(); - using (var conn = new SqlConnection(_masterCstr)) - using (var cmd = conn.CreateCommand()) + var mdf = GetDatabase(cmd, databaseName); + if (mdf != null) { - conn.Open(); - - var mdf = GetDatabase(cmd, databaseName); - if (mdf != null) return false; + return false; + } - // cannot use parameters on CREATE DATABASE - // ie "CREATE DATABASE @0 ..." does not work - SetCommand(cmd, $@" + // cannot use parameters on CREATE DATABASE + // ie "CREATE DATABASE @0 ..." does not work + SetCommand(cmd, $@" CREATE DATABASE {QuotedName(databaseName)} ON (NAME=N{QuotedName(databaseName, '\'')}, FILENAME={QuotedName(mdfFilename, '\'')}) LOG ON (NAME=N{QuotedName(logName, '\'')}, FILENAME={QuotedName(ldfFilename, '\'')})"); - var unused = cmd.ExecuteNonQuery(); - } - return true; + var unused = cmd.ExecuteNonQuery(); } - /// - /// Drops a database. - /// - /// The name of the database. - /// A value indicating whether the database was dropped without errors. - /// - /// Successful if the database does not exist. - /// Deletes the database files. - /// - public bool DropDatabase(string databaseName) + return true; + } + + /// + /// Drops a database. + /// + /// The name of the database. + /// A value indicating whether the database was dropped without errors. + /// + /// Successful if the database does not exist. + /// Deletes the database files. + /// + public bool DropDatabase(string databaseName) + { + using (var conn = new SqlConnection(_masterCstr)) + using (SqlCommand? cmd = conn.CreateCommand()) { - using (var conn = new SqlConnection(_masterCstr)) - using (var cmd = conn.CreateCommand()) - { - conn.Open(); + conn.Open(); - SetCommand(cmd, @" + SetCommand(cmd, @" SELECT name, filename FROM master.dbo.sysdatabases WHERE ('[' + name + ']' = @0 OR name = @0)", - databaseName); - - var mdf = GetDatabase(cmd, databaseName); - if (mdf == null) return true; + databaseName); - ExecuteDropDatabase(cmd, databaseName, mdf); + var mdf = GetDatabase(cmd, databaseName); + if (mdf == null) + { + return true; } - return true; + ExecuteDropDatabase(cmd, databaseName, mdf); } - /// - /// Drops stale databases. - /// - /// The number of databases that were dropped. - /// - /// A database is considered stale when its files cannot be found. - /// - public int DropStaleDatabases() - { - return DropDatabases(true); - } + return true; + } + + /// + /// Drops stale databases. + /// + /// The number of databases that were dropped. + /// + /// A database is considered stale when its files cannot be found. + /// + public int DropStaleDatabases() => DropDatabases(true); - /// - /// Drops databases. - /// - /// A value indicating whether to delete only stale database. - /// The number of databases that were dropped. - /// - /// A database is considered stale when its files cannot be found. - /// - public int DropDatabases(bool staleOnly = false) + /// + /// Drops databases. + /// + /// A value indicating whether to delete only stale database. + /// The number of databases that were dropped. + /// + /// A database is considered stale when its files cannot be found. + /// + public int DropDatabases(bool staleOnly = false) + { + var count = 0; + using (var conn = new SqlConnection(_masterCstr)) + using (SqlCommand? cmd = conn.CreateCommand()) { - var count = 0; - using (var conn = new SqlConnection(_masterCstr)) - using (var cmd = conn.CreateCommand()) - { - conn.Open(); + conn.Open(); - var databases = new Dictionary(); + var databases = new Dictionary(); - SetCommand(cmd, @" + SetCommand(cmd, @" SELECT name, filename FROM sys.sysdatabases"); - using (var reader = cmd.ExecuteReader()) + using (SqlDataReader? reader = cmd.ExecuteReader()) + { + while (reader.Read()) { - while (reader.Read()) - { - databases[reader.GetString(0)] = reader.GetString(1); - } + databases[reader.GetString(0)] = reader.GetString(1); } + } - foreach (var database in databases) - { - var dbname = database.Key; - - if (dbname == "master" || dbname == "tempdb" || dbname == "model" || dbname == "msdb") - continue; + foreach (KeyValuePair database in databases) + { + var dbname = database.Key; - var mdf = database.Value; - var ldf = mdf.Replace(".mdf", "_log.ldf"); - if (staleOnly && File.Exists(mdf) && File.Exists(ldf)) - continue; + if (dbname == "master" || dbname == "tempdb" || dbname == "model" || dbname == "msdb") + { + continue; + } - ExecuteDropDatabase(cmd, dbname, mdf, ldf); - count++; + var mdf = database.Value; + var ldf = mdf.Replace(".mdf", "_log.ldf"); + if (staleOnly && File.Exists(mdf) && File.Exists(ldf)) + { + continue; } - } - return count; + ExecuteDropDatabase(cmd, dbname, mdf, ldf); + count++; + } } - /// - /// Detaches a database. - /// - /// The name of the database. - /// The directory containing the database files. - /// Thrown when a database with the specified name does not exist. - public string? DetachDatabase(string databaseName) + return count; + } + + /// + /// Detaches a database. + /// + /// The name of the database. + /// The directory containing the database files. + /// Thrown when a database with the specified name does not exist. + public string? DetachDatabase(string databaseName) + { + using (var conn = new SqlConnection(_masterCstr)) + using (SqlCommand? cmd = conn.CreateCommand()) { - using (var conn = new SqlConnection(_masterCstr)) - using (var cmd = conn.CreateCommand()) - { - conn.Open(); + conn.Open(); - var mdf = GetDatabase(cmd, databaseName); - if (mdf == null) - throw new InvalidOperationException("Database does not exist."); + var mdf = GetDatabase(cmd, databaseName); + if (mdf == null) + { + throw new InvalidOperationException("Database does not exist."); + } - DetachDatabase(cmd, databaseName); + DetachDatabase(cmd, databaseName); - return Path.GetDirectoryName(mdf); - } + return Path.GetDirectoryName(mdf); } + } - /// - /// Attaches a database. - /// - /// The name of the database. - /// The directory containing database files. - /// Thrown when a database with the specified name already exists. - public void AttachDatabase(string databaseName, string filesPath) + /// + /// Attaches a database. + /// + /// The name of the database. + /// The directory containing database files. + /// Thrown when a database with the specified name already exists. + public void AttachDatabase(string databaseName, string filesPath) + { + using (var conn = new SqlConnection(_masterCstr)) + using (SqlCommand? cmd = conn.CreateCommand()) { - using (var conn = new SqlConnection(_masterCstr)) - using (var cmd = conn.CreateCommand()) - { - conn.Open(); + conn.Open(); - var mdf = GetDatabase(cmd, databaseName); - if (mdf != null) - throw new InvalidOperationException("Database already exists."); - - AttachDatabase(cmd, databaseName, filesPath); + var mdf = GetDatabase(cmd, databaseName); + if (mdf != null) + { + throw new InvalidOperationException("Database already exists."); } + + AttachDatabase(cmd, databaseName, filesPath); } + } - /// - /// Gets the file names of a database. - /// - /// The name of the database. - /// The MDF logical name. - /// The LDF logical name. - /// The MDF filename. - /// The LDF filename. - public void GetFilenames(string databaseName, - out string? mdfName, out string? ldfName, - out string? mdfFilename, out string? ldfFilename) + /// + /// Gets the file names of a database. + /// + /// The name of the database. + /// The MDF logical name. + /// The LDF logical name. + /// The MDF filename. + /// The LDF filename. + public void GetFilenames(string databaseName, + out string? mdfName, out string? ldfName, + out string? mdfFilename, out string? ldfFilename) + { + using (var conn = new SqlConnection(_masterCstr)) + using (SqlCommand? cmd = conn.CreateCommand()) { - using (var conn = new SqlConnection(_masterCstr)) - using (var cmd = conn.CreateCommand()) - { - conn.Open(); + conn.Open(); - GetFilenames(cmd, databaseName, out mdfName, out ldfName, out mdfFilename, out ldfFilename); - } + GetFilenames(cmd, databaseName, out mdfName, out ldfName, out mdfFilename, out ldfFilename); } + } - /// - /// Kills all existing connections. - /// - /// The name of the database. - public void KillConnections(string databaseName) + /// + /// Kills all existing connections. + /// + /// The name of the database. + public void KillConnections(string databaseName) + { + using (var conn = new SqlConnection(_masterCstr)) + using (SqlCommand? cmd = conn.CreateCommand()) { - using (var conn = new SqlConnection(_masterCstr)) - using (var cmd = conn.CreateCommand()) - { - conn.Open(); + conn.Open(); - SetCommand(cmd, @" + SetCommand(cmd, @" DECLARE @sql VARCHAR(MAX); SELECT @sql = COALESCE(@sql,'') + 'kill ' + CONVERT(VARCHAR, SPId) + ';' FROM master.sys.sysprocesses WHERE DBId = DB_ID(@0) AND SPId <> @@SPId; EXEC(@sql);", - databaseName); - cmd.ExecuteNonQuery(); - } + databaseName); + cmd.ExecuteNonQuery(); } + } - /// - /// Gets a database. - /// - /// The Sql Command. - /// The name of the database. - /// The full filename of the MDF file, if the database exists, otherwise null. - private static string? GetDatabase(SqlCommand cmd, string databaseName) - { - SetCommand(cmd, @" + /// + /// Gets a database. + /// + /// The Sql Command. + /// The name of the database. + /// The full filename of the MDF file, if the database exists, otherwise null. + private static string? GetDatabase(SqlCommand cmd, string databaseName) + { + SetCommand(cmd, @" SELECT name, filename FROM master.dbo.sysdatabases WHERE ('[' + name + ']' = @0 OR name = @0)", - databaseName); + databaseName); - string? mdf = null; - using (var reader = cmd.ExecuteReader()) + string? mdf = null; + using (SqlDataReader? reader = cmd.ExecuteReader()) + { + if (reader.Read()) { - if (reader.Read()) - mdf = reader.GetString(1) ?? string.Empty; - while (reader.Read()) - { - } + mdf = reader.GetString(1) ?? string.Empty; } - return mdf; + while (reader.Read()) + { + } } - /// - /// Drops a database and its files. - /// - /// The Sql command. - /// The name of the database. - /// The name of the database (MDF) file. - /// The name of the log (LDF) file. - private static void ExecuteDropDatabase(SqlCommand cmd, string databaseName, string mdf, string? ldf = null) + return mdf; + } + + /// + /// Drops a database and its files. + /// + /// The Sql command. + /// The name of the database. + /// The name of the database (MDF) file. + /// The name of the log (LDF) file. + private static void ExecuteDropDatabase(SqlCommand cmd, string databaseName, string mdf, string? ldf = null) + { + try { - try - { - // cannot use parameters on ALTER DATABASE - // ie "ALTER DATABASE @0 ..." does not work - SetCommand(cmd, $@" + // cannot use parameters on ALTER DATABASE + // ie "ALTER DATABASE @0 ..." does not work + SetCommand(cmd, $@" ALTER DATABASE {QuotedName(databaseName)} SET SINGLE_USER WITH ROLLBACK IMMEDIATE"); - var unused1 = cmd.ExecuteNonQuery(); + var unused1 = cmd.ExecuteNonQuery(); + } + catch (SqlException e) + { + if (e.Message.Contains("Unable to open the physical file") && + e.Message.Contains("Operating system error 2:")) + { + // quite probably, the files were missing + // yet, it should be possible to drop the database anyways + // but we'll have to deal with the files } - catch (SqlException e) + else { - if (e.Message.Contains("Unable to open the physical file") && e.Message.Contains("Operating system error 2:")) - { - // quite probably, the files were missing - // yet, it should be possible to drop the database anyways - // but we'll have to deal with the files - } - else - { - // no idea, throw - throw; - } + // no idea, throw + throw; } + } - // cannot use parameters on DROP DATABASE - // ie "DROP DATABASE @0 ..." does not work - SetCommand(cmd, $@" + // cannot use parameters on DROP DATABASE + // ie "DROP DATABASE @0 ..." does not work + SetCommand(cmd, $@" DROP DATABASE {QuotedName(databaseName)}"); - var unused2 = cmd.ExecuteNonQuery(); + var unused2 = cmd.ExecuteNonQuery(); - // be absolutely sure - if (File.Exists(mdf)) File.Delete(mdf); - ldf = ldf ?? GetLogFilename(mdf); - if (File.Exists(ldf)) File.Delete(ldf); + // be absolutely sure + if (File.Exists(mdf)) + { + File.Delete(mdf); } - /// - /// Gets the log (LDF) filename corresponding to a database (MDF) filename. - /// - /// The MDF filename. - /// - private static string GetLogFilename(string mdfFilename) + ldf = ldf ?? GetLogFilename(mdf); + if (File.Exists(ldf)) { - if (mdfFilename.EndsWith(".mdf") == false) - throw new ArgumentException("Not a valid MDF filename (no .mdf extension).", nameof(mdfFilename)); - return mdfFilename.Substring(0, mdfFilename.Length - ".mdf".Length) + "_log.ldf"; + File.Delete(ldf); } + } - /// - /// Detaches a database. - /// - /// The Sql command. - /// The name of the database. - private static void DetachDatabase(SqlCommand cmd, string databaseName) + /// + /// Gets the log (LDF) filename corresponding to a database (MDF) filename. + /// + /// The MDF filename. + /// + private static string GetLogFilename(string mdfFilename) + { + if (mdfFilename.EndsWith(".mdf") == false) { - // cannot use parameters on ALTER DATABASE - // ie "ALTER DATABASE @0 ..." does not work - SetCommand(cmd, $@" + throw new ArgumentException("Not a valid MDF filename (no .mdf extension).", nameof(mdfFilename)); + } + + return mdfFilename.Substring(0, mdfFilename.Length - ".mdf".Length) + "_log.ldf"; + } + + /// + /// Detaches a database. + /// + /// The Sql command. + /// The name of the database. + private static void DetachDatabase(SqlCommand cmd, string databaseName) + { + // cannot use parameters on ALTER DATABASE + // ie "ALTER DATABASE @0 ..." does not work + SetCommand(cmd, $@" ALTER DATABASE {QuotedName(databaseName)} SET SINGLE_USER WITH ROLLBACK IMMEDIATE"); - var unused1 = cmd.ExecuteNonQuery(); + var unused1 = cmd.ExecuteNonQuery(); - SetCommand(cmd, @" + SetCommand(cmd, @" EXEC sp_detach_db @dbname=@0", - databaseName); + databaseName); - var unused2 = cmd.ExecuteNonQuery(); - } + var unused2 = cmd.ExecuteNonQuery(); + } - /// - /// Attaches a database. - /// - /// The Sql command. - /// The name of the database. - /// The directory containing database files. - private static void AttachDatabase(SqlCommand cmd, string databaseName, string filesPath) - { - GetDatabaseFiles(databaseName, filesPath, - out var logName, out _, out _, out var mdfFilename, out var ldfFilename); + /// + /// Attaches a database. + /// + /// The Sql command. + /// The name of the database. + /// The directory containing database files. + private static void AttachDatabase(SqlCommand cmd, string databaseName, string filesPath) + { + GetDatabaseFiles(databaseName, filesPath, + out var logName, out _, out _, out var mdfFilename, out var ldfFilename); - // cannot use parameters on CREATE DATABASE - // ie "CREATE DATABASE @0 ..." does not work - SetCommand(cmd, $@" + // cannot use parameters on CREATE DATABASE + // ie "CREATE DATABASE @0 ..." does not work + SetCommand(cmd, $@" CREATE DATABASE {QuotedName(databaseName)} ON (NAME=N{QuotedName(databaseName, '\'')}, FILENAME={QuotedName(mdfFilename, '\'')}) LOG ON (NAME=N{QuotedName(logName, '\'')}, FILENAME={QuotedName(ldfFilename, '\'')}) FOR ATTACH"); - var unused = cmd.ExecuteNonQuery(); - } + var unused = cmd.ExecuteNonQuery(); + } - /// - /// Sets a database command. - /// - /// The command. - /// The command text. - /// The command arguments. - /// - /// The command text must refer to arguments as @0, @1... each referring - /// to the corresponding position in . - /// - private static void SetCommand(SqlCommand cmd, string sql, params object[] args) + /// + /// Sets a database command. + /// + /// The command. + /// The command text. + /// The command arguments. + /// + /// The command text must refer to arguments as @0, @1... each referring + /// to the corresponding position in . + /// + private static void SetCommand(SqlCommand cmd, string sql, params object[] args) + { + cmd.CommandType = CommandType.Text; + cmd.CommandText = sql; + cmd.Parameters.Clear(); + for (var i = 0; i < args.Length; i++) { - cmd.CommandType = CommandType.Text; - cmd.CommandText = sql; - cmd.Parameters.Clear(); - for (var i = 0; i < args.Length; i++) - cmd.Parameters.AddWithValue("@" + i, args[i]); + cmd.Parameters.AddWithValue("@" + i, args[i]); } + } - /// - /// Gets the file names of a database. - /// - /// The Sql command. - /// The name of the database. - /// The MDF logical name. - /// The LDF logical name. - /// The MDF filename. - /// The LDF filename. - private void GetFilenames(SqlCommand cmd, string databaseName, - out string? mdfName, out string? ldfName, - out string? mdfFilename, out string? ldfFilename) - { - mdfName = ldfName = mdfFilename = ldfFilename = null; + /// + /// Gets the file names of a database. + /// + /// The Sql command. + /// The name of the database. + /// The MDF logical name. + /// The LDF logical name. + /// The MDF filename. + /// The LDF filename. + private void GetFilenames(SqlCommand cmd, string databaseName, + out string? mdfName, out string? ldfName, + out string? mdfFilename, out string? ldfFilename) + { + mdfName = ldfName = mdfFilename = ldfFilename = null; - SetCommand(cmd, @" + SetCommand(cmd, @" SELECT DB_NAME(database_id), type_desc, name, physical_name FROM master.sys.master_files WHERE database_id=DB_ID(@0)", - databaseName); - using (var reader = cmd.ExecuteReader()) + databaseName); + using (SqlDataReader? reader = cmd.ExecuteReader()) + { + while (reader.Read()) { - while (reader.Read()) + var type = reader.GetString(1); + if (type == "ROWS") + { + mdfName = reader.GetString(2); + ldfName = reader.GetString(3); + } + else if (type == "LOG") { - var type = reader.GetString(1); - if (type == "ROWS") - { - mdfName = reader.GetString(2); - ldfName = reader.GetString(3); - } - else if (type == "LOG") - { - ldfName = reader.GetString(2); - ldfFilename = reader.GetString(3); - } + ldfName = reader.GetString(2); + ldfFilename = reader.GetString(3); } } } } + } - /// - /// Copy database files. - /// - /// The name of the source database. - /// The directory containing source database files. - /// The name of the target database. - /// The directory containing target database files. - /// The source database files extension. - /// The target database files extension. - /// A value indicating whether to overwrite the target files. - /// A value indicating whether to delete the source files. - /// - /// The , , - /// and parameters are optional. If they result in target being identical - /// to source, no copy is performed. If is false, nothing happens, otherwise the source - /// files are deleted. - /// If target is not identical to source, files are copied or moved, depending on the value of . - /// Extensions are used eg to copy MyDatabase.mdf to MyDatabase.mdf.temp. - /// - public void CopyDatabaseFiles(string databaseName, string filesPath, - string? targetDatabaseName = null, string? targetFilesPath = null, - string? sourceExtension = null, string? targetExtension = null, - bool overwrite = false, bool delete = false) + /// + /// Copy database files. + /// + /// The name of the source database. + /// The directory containing source database files. + /// The name of the target database. + /// The directory containing target database files. + /// The source database files extension. + /// The target database files extension. + /// A value indicating whether to overwrite the target files. + /// A value indicating whether to delete the source files. + /// + /// The , , + /// + /// and parameters are optional. If they result in target being identical + /// to source, no copy is performed. If is false, nothing happens, otherwise the source + /// files are deleted. + /// If target is not identical to source, files are copied or moved, depending on the value of + /// . + /// Extensions are used eg to copy MyDatabase.mdf to MyDatabase.mdf.temp. + /// + public void CopyDatabaseFiles(string databaseName, string filesPath, + string? targetDatabaseName = null, string? targetFilesPath = null, + string? sourceExtension = null, string? targetExtension = null, + bool overwrite = false, bool delete = false) + { + var nop = (targetFilesPath == null || targetFilesPath == filesPath) + && (targetDatabaseName == null || targetDatabaseName == databaseName) + && ((sourceExtension == null && targetExtension == null) || sourceExtension == targetExtension); + if (nop && delete == false) { - var nop = (targetFilesPath == null || targetFilesPath == filesPath) - && (targetDatabaseName == null || targetDatabaseName == databaseName) - && (sourceExtension == null && targetExtension == null || sourceExtension == targetExtension); - if (nop && delete == false) return; + return; + } - GetDatabaseFiles(databaseName, filesPath, - out _, out _, out _, out var mdfFilename, out var ldfFilename); + GetDatabaseFiles(databaseName, filesPath, + out _, out _, out _, out var mdfFilename, out var ldfFilename); - if (sourceExtension != null) + if (sourceExtension != null) + { + mdfFilename += "." + sourceExtension; + ldfFilename += "." + sourceExtension; + } + + if (nop) + { + // delete + if (File.Exists(mdfFilename)) { - mdfFilename += "." + sourceExtension; - ldfFilename += "." + sourceExtension; + File.Delete(mdfFilename); } - if (nop) + if (File.Exists(ldfFilename)) { - // delete - if (File.Exists(mdfFilename)) File.Delete(mdfFilename); - if (File.Exists(ldfFilename)) File.Delete(ldfFilename); + File.Delete(ldfFilename); } - else + } + else + { + // copy or copy+delete ie move + GetDatabaseFiles(targetDatabaseName ?? databaseName, targetFilesPath ?? filesPath, + out _, out _, out _, out var targetMdfFilename, out var targetLdfFilename); + + if (targetExtension != null) { - // copy or copy+delete ie move - GetDatabaseFiles(targetDatabaseName ?? databaseName, targetFilesPath ?? filesPath, - out _, out _, out _, out var targetMdfFilename, out var targetLdfFilename); + targetMdfFilename += "." + targetExtension; + targetLdfFilename += "." + targetExtension; + } - if (targetExtension != null) + if (delete) + { + if (overwrite && File.Exists(targetMdfFilename)) { - targetMdfFilename += "." + targetExtension; - targetLdfFilename += "." + targetExtension; + File.Delete(targetMdfFilename); } - if (delete) + if (overwrite && File.Exists(targetLdfFilename)) { - if (overwrite && File.Exists(targetMdfFilename)) File.Delete(targetMdfFilename); - if (overwrite && File.Exists(targetLdfFilename)) File.Delete(targetLdfFilename); - File.Move(mdfFilename, targetMdfFilename); - File.Move(ldfFilename, targetLdfFilename); + File.Delete(targetLdfFilename); } - else - { - File.Copy(mdfFilename, targetMdfFilename, overwrite); - File.Copy(ldfFilename, targetLdfFilename, overwrite); - } - } - } - /// - /// Gets a value indicating whether database files exist. - /// - /// The name of the source database. - /// The directory containing source database files. - /// The database files extension. - /// A value indicating whether the database files exist. - /// - /// Extensions are used eg to copy MyDatabase.mdf to MyDatabase.mdf.temp. - /// - public bool DatabaseFilesExist(string databaseName, string filesPath, string? extension = null) - { - GetDatabaseFiles(databaseName, filesPath, - out _, out _, out _, out var mdfFilename, out var ldfFilename); - - if (extension != null) + File.Move(mdfFilename, targetMdfFilename); + File.Move(ldfFilename, targetLdfFilename); + } + else { - mdfFilename += "." + extension; - ldfFilename += "." + extension; + File.Copy(mdfFilename, targetMdfFilename, overwrite); + File.Copy(ldfFilename, targetLdfFilename, overwrite); } - - return File.Exists(mdfFilename) && File.Exists(ldfFilename); } + } - /// - /// Gets the name of the database files. - /// - /// The name of the database. - /// The directory containing database files. - /// The name of the log. - /// The base filename (the MDF filename without the .mdf extension). - /// The base log filename (the LDF filename without the .ldf extension). - /// The MDF filename. - /// The LDF filename. - private static void GetDatabaseFiles(string databaseName, string filesPath, - out string logName, - out string baseFilename, out string baseLogFilename, - out string mdfFilename, out string ldfFilename) + /// + /// Gets a value indicating whether database files exist. + /// + /// The name of the source database. + /// The directory containing source database files. + /// The database files extension. + /// A value indicating whether the database files exist. + /// + /// Extensions are used eg to copy MyDatabase.mdf to MyDatabase.mdf.temp. + /// + public bool DatabaseFilesExist(string databaseName, string filesPath, string? extension = null) + { + GetDatabaseFiles(databaseName, filesPath, + out _, out _, out _, out var mdfFilename, out var ldfFilename); + + if (extension != null) { - logName = databaseName + "_log"; - baseFilename = Path.Combine(filesPath, databaseName); - baseLogFilename = Path.Combine(filesPath, logName); - mdfFilename = baseFilename + ".mdf"; - ldfFilename = baseFilename + "_log.ldf"; + mdfFilename += "." + extension; + ldfFilename += "." + extension; } - #endregion - - #region SqlLocalDB + return File.Exists(mdfFilename) && File.Exists(ldfFilename); + } - /// - /// Executes the SqlLocalDB command. - /// - /// The arguments. - /// The command standard output. - /// The command error output. - /// The process exit code. - /// - /// Execution is successful if the exit code is zero, and error is empty. - /// - private int ExecuteSqlLocalDb(string args, out string output, out string error) - { - if (_exe == null) // should never happen - we should not execute if not available - { - output = string.Empty; - error = "SqlLocalDB.exe not found"; - return -1; - } + /// + /// Gets the name of the database files. + /// + /// The name of the database. + /// The directory containing database files. + /// The name of the log. + /// The base filename (the MDF filename without the .mdf extension). + /// The base log filename (the LDF filename without the .ldf extension). + /// The MDF filename. + /// The LDF filename. + private static void GetDatabaseFiles(string databaseName, string filesPath, + out string logName, + out string baseFilename, out string baseLogFilename, + out string mdfFilename, out string ldfFilename) + { + logName = databaseName + "_log"; + baseFilename = Path.Combine(filesPath, databaseName); + baseLogFilename = Path.Combine(filesPath, logName); + mdfFilename = baseFilename + ".mdf"; + ldfFilename = baseFilename + "_log.ldf"; + } - using (var p = new Process - { - StartInfo = - { - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - FileName = _exe, - Arguments = args, - CreateNoWindow = true, - WindowStyle = ProcessWindowStyle.Hidden - } - }) - { - p.Start(); - output = p.StandardOutput.ReadToEnd(); - error = p.StandardError.ReadToEnd(); - p.WaitForExit(); + #endregion - return p.ExitCode; - } + #region SqlLocalDB + /// + /// Executes the SqlLocalDB command. + /// + /// The arguments. + /// The command standard output. + /// The command error output. + /// The process exit code. + /// + /// Execution is successful if the exit code is zero, and error is empty. + /// + private int ExecuteSqlLocalDb(string args, out string output, out string error) + { + if (_exe == null) // should never happen - we should not execute if not available + { + output = string.Empty; + error = "SqlLocalDB.exe not found"; + return -1; } - /// - /// Returns a Unicode string with the delimiters added to make the input string a valid SQL Server delimited identifier. - /// - /// The name to quote. - /// A quote character. - /// - /// - /// This is a C# implementation of T-SQL QUOTEDNAME. - /// is optional, it can be '[' (default), ']', '\'' or '"'. - /// - internal static string QuotedName(string name, char quote = '[') + using (var p = new Process + { + StartInfo = + { + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + FileName = _exe, + Arguments = args, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden + } + }) { - switch (quote) - { - case '[': - case ']': - return "[" + name.Replace("]", "]]") + "]"; - case '\'': - return "'" + name.Replace("'", "''") + "'"; - case '"': - return "\"" + name.Replace("\"", "\"\"") + "\""; - default: - throw new NotSupportedException("Not a valid quote character."); - } + p.Start(); + output = p.StandardOutput.ReadToEnd(); + error = p.StandardError.ReadToEnd(); + p.WaitForExit(); + + return p.ExitCode; } + } - #endregion + /// + /// Returns a Unicode string with the delimiters added to make the input string a valid SQL Server delimited + /// identifier. + /// + /// The name to quote. + /// A quote character. + /// + /// + /// This is a C# implementation of T-SQL QUOTEDNAME. + /// is optional, it can be '[' (default), ']', '\'' or '"'. + /// + internal static string QuotedName(string name, char quote = '[') + { + switch (quote) + { + case '[': + case ']': + return "[" + name.Replace("]", "]]") + "]"; + case '\'': + return "'" + name.Replace("'", "''") + "'"; + case '"': + return "\"" + name.Replace("\"", "\"\"") + "\""; + default: + throw new NotSupportedException("Not a valid quote character."); + } } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/AccessMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/AccessMapper.cs index 8d88b2d7df10..f8374a53bcdb 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/AccessMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/AccessMapper.cs @@ -1,25 +1,24 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - [MapperFor(typeof(PublicAccessEntry))] - public sealed class AccessMapper : BaseMapper +[MapperFor(typeof(PublicAccessEntry))] +public sealed class AccessMapper : BaseMapper +{ + public AccessMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public AccessMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(PublicAccessEntry.Key), nameof(AccessDto.Id)); - DefineMap(nameof(PublicAccessEntry.LoginNodeId), nameof(AccessDto.LoginNodeId)); - DefineMap(nameof(PublicAccessEntry.NoAccessNodeId), nameof(AccessDto.NoAccessNodeId)); - DefineMap(nameof(PublicAccessEntry.ProtectedNodeId), nameof(AccessDto.NodeId)); - DefineMap(nameof(PublicAccessEntry.CreateDate), nameof(AccessDto.CreateDate)); - DefineMap(nameof(PublicAccessEntry.UpdateDate), nameof(AccessDto.UpdateDate)); - } + protected override void DefineMaps() + { + DefineMap(nameof(PublicAccessEntry.Key), nameof(AccessDto.Id)); + DefineMap(nameof(PublicAccessEntry.LoginNodeId), nameof(AccessDto.LoginNodeId)); + DefineMap(nameof(PublicAccessEntry.NoAccessNodeId), + nameof(AccessDto.NoAccessNodeId)); + DefineMap(nameof(PublicAccessEntry.ProtectedNodeId), nameof(AccessDto.NodeId)); + DefineMap(nameof(PublicAccessEntry.CreateDate), nameof(AccessDto.CreateDate)); + DefineMap(nameof(PublicAccessEntry.UpdateDate), nameof(AccessDto.UpdateDate)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/AuditEntryMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/AuditEntryMapper.cs index 25bf413cc9e0..b44e0ffc4870 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/AuditEntryMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/AuditEntryMapper.cs @@ -1,31 +1,32 @@ -using System; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +/// +/// Represents a mapper for audit entry entities. +/// +[MapperFor(typeof(IAuditEntry))] +[MapperFor(typeof(AuditEntry))] +public sealed class AuditEntryMapper : BaseMapper { - /// - /// Represents a mapper for audit entry entities. - /// - [MapperFor(typeof(IAuditEntry))] - [MapperFor(typeof(AuditEntry))] - public sealed class AuditEntryMapper : BaseMapper + public AuditEntryMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public AuditEntryMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(AuditEntry.Id), nameof(AuditEntryDto.Id)); - DefineMap(nameof(AuditEntry.PerformingUserId), nameof(AuditEntryDto.PerformingUserId)); - DefineMap(nameof(AuditEntry.PerformingDetails), nameof(AuditEntryDto.PerformingDetails)); - DefineMap(nameof(AuditEntry.PerformingIp), nameof(AuditEntryDto.PerformingIp)); - DefineMap(nameof(AuditEntry.EventDateUtc), nameof(AuditEntryDto.EventDateUtc)); - DefineMap(nameof(AuditEntry.AffectedUserId), nameof(AuditEntryDto.AffectedUserId)); - DefineMap(nameof(AuditEntry.AffectedDetails), nameof(AuditEntryDto.AffectedDetails)); - DefineMap(nameof(AuditEntry.EventType), nameof(AuditEntryDto.EventType)); - DefineMap(nameof(AuditEntry.EventDetails), nameof(AuditEntryDto.EventDetails)); - } + protected override void DefineMaps() + { + DefineMap(nameof(AuditEntry.Id), nameof(AuditEntryDto.Id)); + DefineMap(nameof(AuditEntry.PerformingUserId), + nameof(AuditEntryDto.PerformingUserId)); + DefineMap(nameof(AuditEntry.PerformingDetails), + nameof(AuditEntryDto.PerformingDetails)); + DefineMap(nameof(AuditEntry.PerformingIp), nameof(AuditEntryDto.PerformingIp)); + DefineMap(nameof(AuditEntry.EventDateUtc), nameof(AuditEntryDto.EventDateUtc)); + DefineMap(nameof(AuditEntry.AffectedUserId), nameof(AuditEntryDto.AffectedUserId)); + DefineMap(nameof(AuditEntry.AffectedDetails), nameof(AuditEntryDto.AffectedDetails)); + DefineMap(nameof(AuditEntry.EventType), nameof(AuditEntryDto.EventType)); + DefineMap(nameof(AuditEntry.EventDetails), nameof(AuditEntryDto.EventDetails)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/AuditItemMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/AuditItemMapper.cs index 3267a5d14aac..2724a1f9307b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/AuditItemMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/AuditItemMapper.cs @@ -1,25 +1,24 @@ -using System; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +[MapperFor(typeof(AuditItem))] +[MapperFor(typeof(IAuditItem))] +public sealed class AuditItemMapper : BaseMapper { - [MapperFor(typeof(AuditItem))] - [MapperFor(typeof(IAuditItem))] - public sealed class AuditItemMapper : BaseMapper + public AuditItemMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public AuditItemMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(AuditItem.Id), nameof(LogDto.NodeId)); - DefineMap(nameof(AuditItem.CreateDate), nameof(LogDto.Datestamp)); - DefineMap(nameof(AuditItem.UserId), nameof(LogDto.UserId)); - // we cannot map that one - because AuditType is an enum but Header is a string - //DefineMap(nameof(AuditItem.AuditType), nameof(LogDto.Header)); - DefineMap(nameof(AuditItem.Comment), nameof(LogDto.Comment)); - } + protected override void DefineMaps() + { + DefineMap(nameof(AuditItem.Id), nameof(LogDto.NodeId)); + DefineMap(nameof(AuditItem.CreateDate), nameof(LogDto.Datestamp)); + DefineMap(nameof(AuditItem.UserId), nameof(LogDto.UserId)); + // we cannot map that one - because AuditType is an enum but Header is a string + //DefineMap(nameof(AuditItem.AuditType), nameof(LogDto.Header)); + DefineMap(nameof(AuditItem.Comment), nameof(LogDto.Comment)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/BaseMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/BaseMapper.cs index f046ee954806..1e76f9a2c949 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/BaseMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/BaseMapper.cs @@ -1,82 +1,109 @@ -using System; -using System.Collections.Concurrent; +using System.Collections.Concurrent; +using System.Reflection; using NPoco; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +public abstract class BaseMapper { - public abstract class BaseMapper - { - // note: using a Lazy here because during installs, we are resolving the - // mappers way before we have a configured IUmbracoDatabaseFactory, ie way before we - // have an ISqlContext - this is some nasty temporal coupling which we might want to - // cleanup eventually. + private readonly object _definedLock = new(); - private readonly Lazy _sqlContext; - private readonly object _definedLock = new object(); - private readonly MapperConfigurationStore _maps; + private readonly MapperConfigurationStore _maps; + // note: using a Lazy here because during installs, we are resolving the + // mappers way before we have a configured IUmbracoDatabaseFactory, ie way before we + // have an ISqlContext - this is some nasty temporal coupling which we might want to + // cleanup eventually. - private ISqlSyntaxProvider? _sqlSyntax; - private bool _defined; + private readonly Lazy _sqlContext; + private bool _defined; - protected BaseMapper(Lazy sqlContext, MapperConfigurationStore maps) - { - _sqlContext = sqlContext; - _maps = maps; - } + private ISqlSyntaxProvider? _sqlSyntax; + + protected BaseMapper(Lazy sqlContext, MapperConfigurationStore maps) + { + _sqlContext = sqlContext; + _maps = maps; + } - protected abstract void DefineMaps(); + protected abstract void DefineMaps(); - internal string Map(string? propertyName) + internal string Map(string? propertyName) + { + lock (_definedLock) { - lock (_definedLock) + if (!_defined) { - if (!_defined) + ISqlContext? sqlContext = _sqlContext.Value; + if (sqlContext == null) { - var sqlContext = _sqlContext.Value; - if (sqlContext == null) - throw new InvalidOperationException("Could not get an ISqlContext."); - _sqlSyntax = sqlContext.SqlSyntax; + throw new InvalidOperationException("Could not get an ISqlContext."); + } - DefineMaps(); + _sqlSyntax = sqlContext.SqlSyntax; - _defined = true; - } + DefineMaps(); + + _defined = true; } + } - if (!_maps.TryGetValue(GetType(), out var mapperMaps)) - throw new InvalidOperationException($"No maps defined for mapper {GetType().FullName}."); - if (propertyName is null || !mapperMaps.TryGetValue(propertyName, out var mappedName)) - throw new InvalidOperationException($"No map defined by mapper {GetType().FullName} for property {propertyName}."); - return mappedName; + if (!_maps.TryGetValue(GetType(), out ConcurrentDictionary? mapperMaps)) + { + throw new InvalidOperationException($"No maps defined for mapper {GetType().FullName}."); } - // fixme: TSource is used for nothing - protected void DefineMap(string sourceName, string targetName) + if (propertyName is null || !mapperMaps.TryGetValue(propertyName, out var mappedName)) { - if (_sqlSyntax == null) - throw new InvalidOperationException("Do not define maps outside of DefineMaps."); + throw new InvalidOperationException( + $"No map defined by mapper {GetType().FullName} for property {propertyName}."); + } - var targetType = typeof(TTarget); + return mappedName; + } - // TODO ensure that sourceName is a valid sourceType property (but, slow?) + // fixme: TSource is used for nothing + protected void DefineMap(string sourceName, string targetName) + { + if (_sqlSyntax == null) + { + throw new InvalidOperationException("Do not define maps outside of DefineMaps."); + } - var tableNameAttribute = targetType.FirstAttribute(); - if (tableNameAttribute == null) throw new InvalidOperationException($"Type {targetType.FullName} is not marked with a TableName attribute."); - var tableName = tableNameAttribute.Value; + Type targetType = typeof(TTarget); - // TODO maybe get all properties once and then index them - var targetProperty = targetType.GetProperty(targetName); - if (targetProperty == null) throw new InvalidOperationException($"Type {targetType.FullName} does not have a property named {targetName}."); - var columnAttribute = targetProperty.FirstAttribute(); - if (columnAttribute == null) throw new InvalidOperationException($"Property {targetType.FullName}.{targetName} is not marked with a Column attribute."); + // TODO ensure that sourceName is a valid sourceType property (but, slow?) + + TableNameAttribute? tableNameAttribute = targetType.FirstAttribute(); + if (tableNameAttribute == null) + { + throw new InvalidOperationException( + $"Type {targetType.FullName} is not marked with a TableName attribute."); + } - var columnName = columnAttribute.Name; - var columnMap = _sqlSyntax.GetQuotedTableName(tableName) + "." + _sqlSyntax.GetQuotedColumnName(columnName); + var tableName = tableNameAttribute.Value; - var mapperMaps = _maps.GetOrAdd(GetType(), type => new ConcurrentDictionary()); - mapperMaps[sourceName] = columnMap; + // TODO maybe get all properties once and then index them + PropertyInfo? targetProperty = targetType.GetProperty(targetName); + if (targetProperty == null) + { + throw new InvalidOperationException( + $"Type {targetType.FullName} does not have a property named {targetName}."); + } + + ColumnAttribute? columnAttribute = targetProperty.FirstAttribute(); + if (columnAttribute == null) + { + throw new InvalidOperationException( + $"Property {targetType.FullName}.{targetName} is not marked with a Column attribute."); } + + var columnName = columnAttribute.Name; + var columnMap = _sqlSyntax.GetQuotedTableName(tableName) + "." + _sqlSyntax.GetQuotedColumnName(columnName); + + ConcurrentDictionary mapperMaps = + _maps.GetOrAdd(GetType(), type => new ConcurrentDictionary()); + mapperMaps[sourceName] = columnMap; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/ConsentMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/ConsentMapper.cs index 884db7c09ee3..7cad1bc3790d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/ConsentMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/ConsentMapper.cs @@ -1,30 +1,29 @@ -using System; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +/// +/// Represents a mapper for consent entities. +/// +[MapperFor(typeof(IConsent))] +[MapperFor(typeof(Consent))] +public sealed class ConsentMapper : BaseMapper { - /// - /// Represents a mapper for consent entities. - /// - [MapperFor(typeof(IConsent))] - [MapperFor(typeof(Consent))] - public sealed class ConsentMapper : BaseMapper + public ConsentMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public ConsentMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(Consent.Id), nameof(ConsentDto.Id)); - DefineMap(nameof(Consent.Current), nameof(ConsentDto.Current)); - DefineMap(nameof(Consent.CreateDate), nameof(ConsentDto.CreateDate)); - DefineMap(nameof(Consent.Source), nameof(ConsentDto.Source)); - DefineMap(nameof(Consent.Context), nameof(ConsentDto.Context)); - DefineMap(nameof(Consent.Action), nameof(ConsentDto.Action)); - DefineMap(nameof(Consent.State), nameof(ConsentDto.State)); - DefineMap(nameof(Consent.Comment), nameof(ConsentDto.Comment)); - } + protected override void DefineMaps() + { + DefineMap(nameof(Consent.Id), nameof(ConsentDto.Id)); + DefineMap(nameof(Consent.Current), nameof(ConsentDto.Current)); + DefineMap(nameof(Consent.CreateDate), nameof(ConsentDto.CreateDate)); + DefineMap(nameof(Consent.Source), nameof(ConsentDto.Source)); + DefineMap(nameof(Consent.Context), nameof(ConsentDto.Context)); + DefineMap(nameof(Consent.Action), nameof(ConsentDto.Action)); + DefineMap(nameof(Consent.State), nameof(ConsentDto.State)); + DefineMap(nameof(Consent.Comment), nameof(ConsentDto.Comment)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/ContentMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/ContentMapper.cs index 77e3b4edc444..692f7c0b4b19 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/ContentMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/ContentMapper.cs @@ -1,45 +1,44 @@ -using System; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(Content))] +[MapperFor(typeof(IContent))] +public sealed class ContentMapper : BaseMapper { - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(Content))] - [MapperFor(typeof(IContent))] - public sealed class ContentMapper : BaseMapper + public ContentMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public ContentMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } - - protected override void DefineMaps() - { - DefineMap(nameof(Content.Id), nameof(NodeDto.NodeId)); - DefineMap(nameof(Content.Key), nameof(NodeDto.UniqueId)); - - DefineMap(nameof(Content.VersionId), nameof(ContentVersionDto.Id)); - DefineMap(nameof(Content.Name), nameof(ContentVersionDto.Text)); - - DefineMap(nameof(Content.ParentId), nameof(NodeDto.ParentId)); - DefineMap(nameof(Content.Level), nameof(NodeDto.Level)); - DefineMap(nameof(Content.Path), nameof(NodeDto.Path)); - DefineMap(nameof(Content.SortOrder), nameof(NodeDto.SortOrder)); - DefineMap(nameof(Content.Trashed), nameof(NodeDto.Trashed)); - - DefineMap(nameof(Content.CreateDate), nameof(NodeDto.CreateDate)); - DefineMap(nameof(Content.CreatorId), nameof(NodeDto.UserId)); - DefineMap(nameof(Content.ContentTypeId), nameof(ContentDto.ContentTypeId)); - - DefineMap(nameof(Content.UpdateDate), nameof(ContentVersionDto.VersionDate)); - DefineMap(nameof(Content.Published), nameof(DocumentDto.Published)); - - //DefineMap(nameof(Content.Name), nameof(DocumentDto.Alias)); - //CacheMap(src => src, dto => dto.Newest); - //DefineMap(nameof(Content.Template), nameof(DocumentDto.TemplateId)); - } + } + + protected override void DefineMaps() + { + DefineMap(nameof(Content.Id), nameof(NodeDto.NodeId)); + DefineMap(nameof(Content.Key), nameof(NodeDto.UniqueId)); + + DefineMap(nameof(Content.VersionId), nameof(ContentVersionDto.Id)); + DefineMap(nameof(Content.Name), nameof(ContentVersionDto.Text)); + + DefineMap(nameof(Content.ParentId), nameof(NodeDto.ParentId)); + DefineMap(nameof(Content.Level), nameof(NodeDto.Level)); + DefineMap(nameof(Content.Path), nameof(NodeDto.Path)); + DefineMap(nameof(Content.SortOrder), nameof(NodeDto.SortOrder)); + DefineMap(nameof(Content.Trashed), nameof(NodeDto.Trashed)); + + DefineMap(nameof(Content.CreateDate), nameof(NodeDto.CreateDate)); + DefineMap(nameof(Content.CreatorId), nameof(NodeDto.UserId)); + DefineMap(nameof(Content.ContentTypeId), nameof(ContentDto.ContentTypeId)); + + DefineMap(nameof(Content.UpdateDate), nameof(ContentVersionDto.VersionDate)); + DefineMap(nameof(Content.Published), nameof(DocumentDto.Published)); + + //DefineMap(nameof(Content.Name), nameof(DocumentDto.Alias)); + //CacheMap(src => src, dto => dto.Newest); + //DefineMap(nameof(Content.Template), nameof(DocumentDto.TemplateId)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/ContentTypeMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/ContentTypeMapper.cs index d24dac289468..655353aa0c6a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/ContentTypeMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/ContentTypeMapper.cs @@ -1,40 +1,39 @@ -using System; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(ContentType))] +[MapperFor(typeof(IContentType))] +public sealed class ContentTypeMapper : BaseMapper { - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(ContentType))] - [MapperFor(typeof(IContentType))] - public sealed class ContentTypeMapper : BaseMapper + public ContentTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public ContentTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(ContentType.Id), nameof(NodeDto.NodeId)); - DefineMap(nameof(ContentType.CreateDate), nameof(NodeDto.CreateDate)); - DefineMap(nameof(ContentType.Level), nameof(NodeDto.Level)); - DefineMap(nameof(ContentType.ParentId), nameof(NodeDto.ParentId)); - DefineMap(nameof(ContentType.Path), nameof(NodeDto.Path)); - DefineMap(nameof(ContentType.SortOrder), nameof(NodeDto.SortOrder)); - DefineMap(nameof(ContentType.Name), nameof(NodeDto.Text)); - DefineMap(nameof(ContentType.Trashed), nameof(NodeDto.Trashed)); - DefineMap(nameof(ContentType.Key), nameof(NodeDto.UniqueId)); - DefineMap(nameof(ContentType.CreatorId), nameof(NodeDto.UserId)); - DefineMap(nameof(ContentType.Alias), nameof(ContentTypeDto.Alias)); - DefineMap(nameof(ContentType.AllowedAsRoot), nameof(ContentTypeDto.AllowAtRoot)); - DefineMap(nameof(ContentType.Description), nameof(ContentTypeDto.Description)); - DefineMap(nameof(ContentType.Icon), nameof(ContentTypeDto.Icon)); - DefineMap(nameof(ContentType.IsContainer), nameof(ContentTypeDto.IsContainer)); - DefineMap(nameof(ContentType.IsElement), nameof(ContentTypeDto.IsElement)); - DefineMap(nameof(ContentType.Thumbnail), nameof(ContentTypeDto.Thumbnail)); - } + protected override void DefineMaps() + { + DefineMap(nameof(ContentType.Id), nameof(NodeDto.NodeId)); + DefineMap(nameof(ContentType.CreateDate), nameof(NodeDto.CreateDate)); + DefineMap(nameof(ContentType.Level), nameof(NodeDto.Level)); + DefineMap(nameof(ContentType.ParentId), nameof(NodeDto.ParentId)); + DefineMap(nameof(ContentType.Path), nameof(NodeDto.Path)); + DefineMap(nameof(ContentType.SortOrder), nameof(NodeDto.SortOrder)); + DefineMap(nameof(ContentType.Name), nameof(NodeDto.Text)); + DefineMap(nameof(ContentType.Trashed), nameof(NodeDto.Trashed)); + DefineMap(nameof(ContentType.Key), nameof(NodeDto.UniqueId)); + DefineMap(nameof(ContentType.CreatorId), nameof(NodeDto.UserId)); + DefineMap(nameof(ContentType.Alias), nameof(ContentTypeDto.Alias)); + DefineMap(nameof(ContentType.AllowedAsRoot), nameof(ContentTypeDto.AllowAtRoot)); + DefineMap(nameof(ContentType.Description), nameof(ContentTypeDto.Description)); + DefineMap(nameof(ContentType.Icon), nameof(ContentTypeDto.Icon)); + DefineMap(nameof(ContentType.IsContainer), nameof(ContentTypeDto.IsContainer)); + DefineMap(nameof(ContentType.IsElement), nameof(ContentTypeDto.IsElement)); + DefineMap(nameof(ContentType.Thumbnail), nameof(ContentTypeDto.Thumbnail)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/DataTypeMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/DataTypeMapper.cs index 8a84b8b153d0..d412a4f3eac3 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/DataTypeMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/DataTypeMapper.cs @@ -1,35 +1,34 @@ -using System; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(DataType))] +[MapperFor(typeof(IDataType))] +public sealed class DataTypeMapper : BaseMapper { - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(DataType))] - [MapperFor(typeof(IDataType))] - public sealed class DataTypeMapper : BaseMapper + public DataTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public DataTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(DataType.Id), nameof(NodeDto.NodeId)); - DefineMap(nameof(DataType.CreateDate), nameof(NodeDto.CreateDate)); - DefineMap(nameof(DataType.Level), nameof(NodeDto.Level)); - DefineMap(nameof(DataType.ParentId), nameof(NodeDto.ParentId)); - DefineMap(nameof(DataType.Path), nameof(NodeDto.Path)); - DefineMap(nameof(DataType.SortOrder), nameof(NodeDto.SortOrder)); - DefineMap(nameof(DataType.Name), nameof(NodeDto.Text)); - DefineMap(nameof(DataType.Trashed), nameof(NodeDto.Trashed)); - DefineMap(nameof(DataType.Key), nameof(NodeDto.UniqueId)); - DefineMap(nameof(DataType.CreatorId), nameof(NodeDto.UserId)); - DefineMap(nameof(DataType.EditorAlias), nameof(DataTypeDto.EditorAlias)); - DefineMap(nameof(DataType.DatabaseType), nameof(DataTypeDto.DbType)); - } + protected override void DefineMaps() + { + DefineMap(nameof(DataType.Id), nameof(NodeDto.NodeId)); + DefineMap(nameof(DataType.CreateDate), nameof(NodeDto.CreateDate)); + DefineMap(nameof(DataType.Level), nameof(NodeDto.Level)); + DefineMap(nameof(DataType.ParentId), nameof(NodeDto.ParentId)); + DefineMap(nameof(DataType.Path), nameof(NodeDto.Path)); + DefineMap(nameof(DataType.SortOrder), nameof(NodeDto.SortOrder)); + DefineMap(nameof(DataType.Name), nameof(NodeDto.Text)); + DefineMap(nameof(DataType.Trashed), nameof(NodeDto.Trashed)); + DefineMap(nameof(DataType.Key), nameof(NodeDto.UniqueId)); + DefineMap(nameof(DataType.CreatorId), nameof(NodeDto.UserId)); + DefineMap(nameof(DataType.EditorAlias), nameof(DataTypeDto.EditorAlias)); + DefineMap(nameof(DataType.DatabaseType), nameof(DataTypeDto.DbType)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/DictionaryMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/DictionaryMapper.cs index da04c254f872..7a42c5ba4729 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/DictionaryMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/DictionaryMapper.cs @@ -1,27 +1,26 @@ -using System; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(DictionaryItem))] +[MapperFor(typeof(IDictionaryItem))] +public sealed class DictionaryMapper : BaseMapper { - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(DictionaryItem))] - [MapperFor(typeof(IDictionaryItem))] - public sealed class DictionaryMapper : BaseMapper + public DictionaryMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public DictionaryMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(DictionaryItem.Id), nameof(DictionaryDto.PrimaryKey)); - DefineMap(nameof(DictionaryItem.Key), nameof(DictionaryDto.UniqueId)); - DefineMap(nameof(DictionaryItem.ItemKey), nameof(DictionaryDto.Key)); - DefineMap(nameof(DictionaryItem.ParentId), nameof(DictionaryDto.Parent)); - } + protected override void DefineMaps() + { + DefineMap(nameof(DictionaryItem.Id), nameof(DictionaryDto.PrimaryKey)); + DefineMap(nameof(DictionaryItem.Key), nameof(DictionaryDto.UniqueId)); + DefineMap(nameof(DictionaryItem.ItemKey), nameof(DictionaryDto.Key)); + DefineMap(nameof(DictionaryItem.ParentId), nameof(DictionaryDto.Parent)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/DictionaryTranslationMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/DictionaryTranslationMapper.cs index ead88959d3f4..ccaf9a59da5a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/DictionaryTranslationMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/DictionaryTranslationMapper.cs @@ -1,27 +1,30 @@ -using System; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(DictionaryTranslation))] +[MapperFor(typeof(IDictionaryTranslation))] +public sealed class DictionaryTranslationMapper : BaseMapper { - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(DictionaryTranslation))] - [MapperFor(typeof(IDictionaryTranslation))] - public sealed class DictionaryTranslationMapper : BaseMapper + public DictionaryTranslationMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public DictionaryTranslationMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(DictionaryTranslation.Id), nameof(LanguageTextDto.PrimaryKey)); - DefineMap(nameof(DictionaryTranslation.Key), nameof(LanguageTextDto.UniqueId)); - DefineMap(nameof(DictionaryTranslation.Language), nameof(LanguageTextDto.LanguageId)); - DefineMap(nameof(DictionaryTranslation.Value), nameof(LanguageTextDto.Value)); - } + protected override void DefineMaps() + { + DefineMap(nameof(DictionaryTranslation.Id), + nameof(LanguageTextDto.PrimaryKey)); + DefineMap(nameof(DictionaryTranslation.Key), + nameof(LanguageTextDto.UniqueId)); + DefineMap(nameof(DictionaryTranslation.Language), + nameof(LanguageTextDto.LanguageId)); + DefineMap(nameof(DictionaryTranslation.Value), + nameof(LanguageTextDto.Value)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/DomainMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/DomainMapper.cs index 860d34edbf53..c53277f23285 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/DomainMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/DomainMapper.cs @@ -1,23 +1,22 @@ -using System; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +[MapperFor(typeof(IDomain))] +[MapperFor(typeof(UmbracoDomain))] +public sealed class DomainMapper : BaseMapper { - [MapperFor(typeof(IDomain))] - [MapperFor(typeof(UmbracoDomain))] - public sealed class DomainMapper : BaseMapper + public DomainMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public DomainMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(UmbracoDomain.Id), nameof(DomainDto.Id)); - DefineMap(nameof(UmbracoDomain.RootContentId), nameof(DomainDto.RootStructureId)); - DefineMap(nameof(UmbracoDomain.LanguageId), nameof(DomainDto.DefaultLanguage)); - DefineMap(nameof(UmbracoDomain.DomainName), nameof(DomainDto.DomainName)); - } + protected override void DefineMaps() + { + DefineMap(nameof(UmbracoDomain.Id), nameof(DomainDto.Id)); + DefineMap(nameof(UmbracoDomain.RootContentId), nameof(DomainDto.RootStructureId)); + DefineMap(nameof(UmbracoDomain.LanguageId), nameof(DomainDto.DefaultLanguage)); + DefineMap(nameof(UmbracoDomain.DomainName), nameof(DomainDto.DomainName)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginMapper.cs index 85db7bf55341..db48b99debaa 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginMapper.cs @@ -1,25 +1,29 @@ -using System; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +[MapperFor(typeof(IIdentityUserLogin))] +[MapperFor(typeof(IdentityUserLogin))] +public sealed class ExternalLoginMapper : BaseMapper { - [MapperFor(typeof(IIdentityUserLogin))] - [MapperFor(typeof(IdentityUserLogin))] - public sealed class ExternalLoginMapper : BaseMapper + public ExternalLoginMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public ExternalLoginMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(IdentityUserLogin.Id), nameof(ExternalLoginDto.Id)); - DefineMap(nameof(IdentityUserLogin.CreateDate), nameof(ExternalLoginDto.CreateDate)); - DefineMap(nameof(IdentityUserLogin.LoginProvider), nameof(ExternalLoginDto.LoginProvider)); - DefineMap(nameof(IdentityUserLogin.ProviderKey), nameof(ExternalLoginDto.ProviderKey)); - DefineMap(nameof(IdentityUserLogin.Key), nameof(ExternalLoginDto.UserOrMemberKey)); - DefineMap(nameof(IdentityUserLogin.UserData), nameof(ExternalLoginDto.UserData)); - } + protected override void DefineMaps() + { + DefineMap(nameof(IdentityUserLogin.Id), nameof(ExternalLoginDto.Id)); + DefineMap(nameof(IdentityUserLogin.CreateDate), + nameof(ExternalLoginDto.CreateDate)); + DefineMap(nameof(IdentityUserLogin.LoginProvider), + nameof(ExternalLoginDto.LoginProvider)); + DefineMap(nameof(IdentityUserLogin.ProviderKey), + nameof(ExternalLoginDto.ProviderKey)); + DefineMap(nameof(IdentityUserLogin.Key), + nameof(ExternalLoginDto.UserOrMemberKey)); + DefineMap(nameof(IdentityUserLogin.UserData), + nameof(ExternalLoginDto.UserData)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginTokenMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginTokenMapper.cs index ca8360c626ac..067b2234f534 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginTokenMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginTokenMapper.cs @@ -1,25 +1,29 @@ -using System; -using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Security; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +[MapperFor(typeof(IIdentityUserToken))] +[MapperFor(typeof(IdentityUserToken))] +public sealed class ExternalLoginTokenMapper : BaseMapper { - [MapperFor(typeof(IIdentityUserToken))] - [MapperFor(typeof(IdentityUserToken))] - public sealed class ExternalLoginTokenMapper : BaseMapper + public ExternalLoginTokenMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public ExternalLoginTokenMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(IdentityUserToken.Id), nameof(ExternalLoginTokenDto.Id)); - DefineMap(nameof(IdentityUserToken.CreateDate), nameof(ExternalLoginTokenDto.CreateDate)); - DefineMap(nameof(IdentityUserToken.Name), nameof(ExternalLoginTokenDto.Name)); - DefineMap(nameof(IdentityUserToken.Value), nameof(ExternalLoginTokenDto.Value)); - // separate table - DefineMap(nameof(IdentityUserLogin.Key), nameof(ExternalLoginDto.UserOrMemberKey)); - } + protected override void DefineMaps() + { + DefineMap(nameof(IdentityUserToken.Id), + nameof(ExternalLoginTokenDto.Id)); + DefineMap(nameof(IdentityUserToken.CreateDate), + nameof(ExternalLoginTokenDto.CreateDate)); + DefineMap(nameof(IdentityUserToken.Name), + nameof(ExternalLoginTokenDto.Name)); + DefineMap(nameof(IdentityUserToken.Value), + nameof(ExternalLoginTokenDto.Value)); + // separate table + DefineMap(nameof(IdentityUserLogin.Key), + nameof(ExternalLoginDto.UserOrMemberKey)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/IMapperCollection.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/IMapperCollection.cs index db2f104ed77e..3a9cdf986eca 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/IMapperCollection.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/IMapperCollection.cs @@ -1,12 +1,10 @@ -using System; -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +public interface IMapperCollection : IBuilderCollection { - public interface IMapperCollection : IBuilderCollection - { - bool TryGetMapper(Type type, [MaybeNullWhen(false)] out BaseMapper mapper); - BaseMapper this[Type type] { get; } - } + BaseMapper this[Type type] { get; } + bool TryGetMapper(Type type, [MaybeNullWhen(false)] out BaseMapper mapper); } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/KeyValueMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/KeyValueMapper.cs index a133d4066c6b..05bb514e15b9 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/KeyValueMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/KeyValueMapper.cs @@ -1,22 +1,21 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +[MapperFor(typeof(KeyValue))] +[MapperFor(typeof(IKeyValue))] +public sealed class KeyValueMapper : BaseMapper { - [MapperFor(typeof(KeyValue))] - [MapperFor(typeof(IKeyValue))] - public sealed class KeyValueMapper : BaseMapper + public KeyValueMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public KeyValueMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(KeyValue.Identifier), nameof(KeyValueDto.Key)); - DefineMap(nameof(KeyValue.Value), nameof(KeyValueDto.Value)); - DefineMap(nameof(KeyValue.UpdateDate), nameof(KeyValueDto.UpdateDate)); - } + protected override void DefineMaps() + { + DefineMap(nameof(KeyValue.Identifier), nameof(KeyValueDto.Key)); + DefineMap(nameof(KeyValue.Value), nameof(KeyValueDto.Value)); + DefineMap(nameof(KeyValue.UpdateDate), nameof(KeyValueDto.UpdateDate)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/LanguageMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/LanguageMapper.cs index d4313ad4d581..3e49fcdea1bd 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/LanguageMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/LanguageMapper.cs @@ -1,26 +1,25 @@ -using System; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(ILanguage))] +[MapperFor(typeof(Language))] +public sealed class LanguageMapper : BaseMapper { - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(ILanguage))] - [MapperFor(typeof(Language))] - public sealed class LanguageMapper : BaseMapper + public LanguageMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public LanguageMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(Language.Id), nameof(LanguageDto.Id)); - DefineMap(nameof(Language.IsoCode), nameof(LanguageDto.IsoCode)); - DefineMap(nameof(Language.CultureName), nameof(LanguageDto.CultureName)); - } + protected override void DefineMaps() + { + DefineMap(nameof(Language.Id), nameof(LanguageDto.Id)); + DefineMap(nameof(Language.IsoCode), nameof(LanguageDto.IsoCode)); + DefineMap(nameof(Language.CultureName), nameof(LanguageDto.CultureName)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/LogViewerQueryMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/LogViewerQueryMapper.cs index 807e3b6c0202..d60caf5ed85a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/LogViewerQueryMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/LogViewerQueryMapper.cs @@ -1,22 +1,21 @@ -using System; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +[MapperFor(typeof(ILogViewerQuery))] +[MapperFor(typeof(LogViewerQuery))] +public sealed class LogViewerQueryMapper : BaseMapper { - [MapperFor(typeof(ILogViewerQuery))] - [MapperFor(typeof(LogViewerQuery))] - public sealed class LogViewerQueryMapper : BaseMapper + public LogViewerQueryMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public LogViewerQueryMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(ILogViewerQuery.Id), nameof(LogViewerQueryDto.Id)); - DefineMap(nameof(ILogViewerQuery.Name), nameof(LogViewerQueryDto.Name)); - DefineMap(nameof(ILogViewerQuery.Query), nameof(LogViewerQueryDto.Query)); - } + protected override void DefineMaps() + { + DefineMap(nameof(ILogViewerQuery.Id), nameof(LogViewerQueryDto.Id)); + DefineMap(nameof(ILogViewerQuery.Name), nameof(LogViewerQueryDto.Name)); + DefineMap(nameof(ILogViewerQuery.Query), nameof(LogViewerQueryDto.Query)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/MacroMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/MacroMapper.cs index f40dbdd47702..3daec2028ef8 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/MacroMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/MacroMapper.cs @@ -1,28 +1,27 @@ -using System; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +[MapperFor(typeof(Macro))] +[MapperFor(typeof(IMacro))] +internal sealed class MacroMapper : BaseMapper { - [MapperFor(typeof(Macro))] - [MapperFor(typeof(IMacro))] - internal sealed class MacroMapper : BaseMapper + public MacroMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public MacroMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(Macro.Id), nameof(MacroDto.Id)); - DefineMap(nameof(Macro.Alias), nameof(MacroDto.Alias)); - DefineMap(nameof(Macro.CacheByPage), nameof(MacroDto.CacheByPage)); - DefineMap(nameof(Macro.CacheByMember), nameof(MacroDto.CachePersonalized)); - DefineMap(nameof(Macro.DontRender), nameof(MacroDto.DontRender)); - DefineMap(nameof(Macro.Name), nameof(MacroDto.Name)); - DefineMap(nameof(Macro.CacheDuration), nameof(MacroDto.RefreshRate)); - DefineMap(nameof(Macro.MacroSource), nameof(MacroDto.MacroSource)); - DefineMap(nameof(Macro.UseInEditor), nameof(MacroDto.UseInEditor)); - } + protected override void DefineMaps() + { + DefineMap(nameof(Macro.Id), nameof(MacroDto.Id)); + DefineMap(nameof(Macro.Alias), nameof(MacroDto.Alias)); + DefineMap(nameof(Macro.CacheByPage), nameof(MacroDto.CacheByPage)); + DefineMap(nameof(Macro.CacheByMember), nameof(MacroDto.CachePersonalized)); + DefineMap(nameof(Macro.DontRender), nameof(MacroDto.DontRender)); + DefineMap(nameof(Macro.Name), nameof(MacroDto.Name)); + DefineMap(nameof(Macro.CacheDuration), nameof(MacroDto.RefreshRate)); + DefineMap(nameof(Macro.MacroSource), nameof(MacroDto.MacroSource)); + DefineMap(nameof(Macro.UseInEditor), nameof(MacroDto.UseInEditor)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollection.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollection.cs index aab89f8cd983..ceb8bfe49d65 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollection.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollection.cs @@ -1,50 +1,50 @@ -using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Umbraco.Cms.Core.Composing; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +public class MapperCollection : BuilderCollectionBase, IMapperCollection { - public class MapperCollection : BuilderCollectionBase, IMapperCollection - { - public MapperCollection(Func> items) - : base(items) - { + private readonly Lazy> _index; - _index = new Lazy>(() => + public MapperCollection(Func> items) + : base(items) => + _index = new Lazy>(() => + { + var d = new ConcurrentDictionary(); + foreach (BaseMapper mapper in this) { - var d = new ConcurrentDictionary(); - foreach(var mapper in this) + IEnumerable attributes = + mapper.GetType().GetCustomAttributes(false); + foreach (MapperForAttribute a in attributes) { - var attributes = mapper.GetType().GetCustomAttributes(false); - foreach(var a in attributes) - { - d.TryAdd(a.EntityType, mapper); - } + d.TryAdd(a.EntityType, mapper); } - return d; - }); - } + } - private readonly Lazy> _index; + return d; + }); - /// - /// Returns a mapper for this type, throw an exception if not found - /// - /// - /// - public BaseMapper this[Type type] + /// + /// Returns a mapper for this type, throw an exception if not found + /// + /// + /// + public BaseMapper this[Type type] + { + get { - get + if (_index.Value.TryGetValue(type, out BaseMapper? mapper)) { - if (_index.Value.TryGetValue(type, out var mapper)) - return mapper; - throw new Exception($"Could not find a mapper matching type {type.FullName}."); + return mapper; } - } - public bool TryGetMapper(Type type,[MaybeNullWhen(false)] out BaseMapper mapper) => _index.Value.TryGetValue(type, out mapper); + throw new Exception($"Could not find a mapper matching type {type.FullName}."); + } } + + public bool TryGetMapper(Type type, [MaybeNullWhen(false)] out BaseMapper mapper) => + _index.Value.TryGetValue(type, out mapper); } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollectionBuilder.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollectionBuilder.cs index 8b993365a040..bec325d2d209 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollectionBuilder.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollectionBuilder.cs @@ -1,62 +1,61 @@ using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +public class MapperCollectionBuilder : SetCollectionBuilderBase { - public class MapperCollectionBuilder : SetCollectionBuilderBase - { - protected override MapperCollectionBuilder This => this; + protected override MapperCollectionBuilder This => this; - public override void RegisterWith(IServiceCollection services) - { - base.RegisterWith(services); + public override void RegisterWith(IServiceCollection services) + { + base.RegisterWith(services); - // default initializer registers - // - service MapperCollectionBuilder, returns MapperCollectionBuilder - // - service MapperCollection, returns MapperCollectionBuilder's collection - // we want to register extra - // - service IMapperCollection, returns MappersCollectionBuilder's collection + // default initializer registers + // - service MapperCollectionBuilder, returns MapperCollectionBuilder + // - service MapperCollection, returns MapperCollectionBuilder's collection + // we want to register extra + // - service IMapperCollection, returns MappersCollectionBuilder's collection - services.AddSingleton(); - services.AddSingleton(factory => factory.GetRequiredService()); - } + services.AddSingleton(); + services.AddSingleton(factory => factory.GetRequiredService()); + } - public MapperCollectionBuilder AddCoreMappers() - { - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - return this; - } + public MapperCollectionBuilder AddCoreMappers() + { + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + return this; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/MapperConfigurationStore.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/MapperConfigurationStore.cs index 460e50677dda..9d9971597694 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/MapperConfigurationStore.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/MapperConfigurationStore.cs @@ -1,8 +1,7 @@ -using System; -using System.Collections.Concurrent; +using System.Collections.Concurrent; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +public class MapperConfigurationStore : ConcurrentDictionary> { - public class MapperConfigurationStore : ConcurrentDictionary> - { } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/MapperForAttribute.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/MapperForAttribute.cs index c2caa89bd936..9807b80899d8 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/MapperForAttribute.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/MapperForAttribute.cs @@ -1,16 +1,11 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +/// +/// An attribute used to decorate mappers to be associated with entities +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] +public sealed class MapperForAttribute : Attribute { - /// - /// An attribute used to decorate mappers to be associated with entities - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] - public sealed class MapperForAttribute : Attribute - { - public Type EntityType { get; private set; } - - public MapperForAttribute(Type entityType) => EntityType = entityType; - } - + public MapperForAttribute(Type entityType) => EntityType = entityType; + public Type EntityType { get; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/MediaMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/MediaMapper.cs index a2c5ff305bb6..1bacc35e2308 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/MediaMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/MediaMapper.cs @@ -1,38 +1,39 @@ -using System; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(IMedia))] +[MapperFor(typeof(Core.Models.Media))] +public sealed class MediaMapper : BaseMapper { - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(IMedia))] - [MapperFor(typeof(Core.Models.Media))] - public sealed class MediaMapper : BaseMapper + public MediaMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public MediaMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(Core.Models.Media.Id), nameof(NodeDto.NodeId)); - DefineMap(nameof(Core.Models.Media.Key), nameof(NodeDto.UniqueId)); + protected override void DefineMaps() + { + DefineMap(nameof(Core.Models.Media.Id), nameof(NodeDto.NodeId)); + DefineMap(nameof(Core.Models.Media.Key), nameof(NodeDto.UniqueId)); - DefineMap(nameof(Content.VersionId), nameof(ContentVersionDto.Id)); + DefineMap(nameof(Content.VersionId), nameof(ContentVersionDto.Id)); - DefineMap(nameof(Core.Models.Media.CreateDate), nameof(NodeDto.CreateDate)); - DefineMap(nameof(Core.Models.Media.Level), nameof(NodeDto.Level)); - DefineMap(nameof(Core.Models.Media.ParentId), nameof(NodeDto.ParentId)); - DefineMap(nameof(Core.Models.Media.Path), nameof(NodeDto.Path)); - DefineMap(nameof(Core.Models.Media.SortOrder), nameof(NodeDto.SortOrder)); - DefineMap(nameof(Core.Models.Media.Name), nameof(NodeDto.Text)); - DefineMap(nameof(Core.Models.Media.Trashed), nameof(NodeDto.Trashed)); - DefineMap(nameof(Core.Models.Media.CreatorId), nameof(NodeDto.UserId)); - DefineMap(nameof(Core.Models.Media.ContentTypeId), nameof(ContentDto.ContentTypeId)); - DefineMap(nameof(Core.Models.Media.UpdateDate), nameof(ContentVersionDto.VersionDate)); - } + DefineMap(nameof(Core.Models.Media.CreateDate), nameof(NodeDto.CreateDate)); + DefineMap(nameof(Core.Models.Media.Level), nameof(NodeDto.Level)); + DefineMap(nameof(Core.Models.Media.ParentId), nameof(NodeDto.ParentId)); + DefineMap(nameof(Core.Models.Media.Path), nameof(NodeDto.Path)); + DefineMap(nameof(Core.Models.Media.SortOrder), nameof(NodeDto.SortOrder)); + DefineMap(nameof(Core.Models.Media.Name), nameof(NodeDto.Text)); + DefineMap(nameof(Core.Models.Media.Trashed), nameof(NodeDto.Trashed)); + DefineMap(nameof(Core.Models.Media.CreatorId), nameof(NodeDto.UserId)); + DefineMap(nameof(Core.Models.Media.ContentTypeId), + nameof(ContentDto.ContentTypeId)); + DefineMap(nameof(Core.Models.Media.UpdateDate), + nameof(ContentVersionDto.VersionDate)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/MediaTypeMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/MediaTypeMapper.cs index 823ee7ce88d5..0fdcd1ccfc88 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/MediaTypeMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/MediaTypeMapper.cs @@ -1,40 +1,39 @@ -using System; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(IMediaType))] +[MapperFor(typeof(MediaType))] +public sealed class MediaTypeMapper : BaseMapper { - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(IMediaType))] - [MapperFor(typeof(MediaType))] - public sealed class MediaTypeMapper : BaseMapper + public MediaTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public MediaTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(MediaType.Id), nameof(NodeDto.NodeId)); - DefineMap(nameof(MediaType.CreateDate), nameof(NodeDto.CreateDate)); - DefineMap(nameof(MediaType.Level), nameof(NodeDto.Level)); - DefineMap(nameof(MediaType.ParentId), nameof(NodeDto.ParentId)); - DefineMap(nameof(MediaType.Path), nameof(NodeDto.Path)); - DefineMap(nameof(MediaType.SortOrder), nameof(NodeDto.SortOrder)); - DefineMap(nameof(MediaType.Name), nameof(NodeDto.Text)); - DefineMap(nameof(MediaType.Trashed), nameof(NodeDto.Trashed)); - DefineMap(nameof(MediaType.Key), nameof(NodeDto.UniqueId)); - DefineMap(nameof(MediaType.CreatorId), nameof(NodeDto.UserId)); - DefineMap(nameof(MediaType.Alias), nameof(ContentTypeDto.Alias)); - DefineMap(nameof(MediaType.AllowedAsRoot), nameof(ContentTypeDto.AllowAtRoot)); - DefineMap(nameof(MediaType.Description), nameof(ContentTypeDto.Description)); - DefineMap(nameof(MediaType.Icon), nameof(ContentTypeDto.Icon)); - DefineMap(nameof(MediaType.IsContainer), nameof(ContentTypeDto.IsContainer)); - DefineMap(nameof(MediaType.IsElement), nameof(ContentTypeDto.IsElement)); - DefineMap(nameof(MediaType.Thumbnail), nameof(ContentTypeDto.Thumbnail)); - } + protected override void DefineMaps() + { + DefineMap(nameof(MediaType.Id), nameof(NodeDto.NodeId)); + DefineMap(nameof(MediaType.CreateDate), nameof(NodeDto.CreateDate)); + DefineMap(nameof(MediaType.Level), nameof(NodeDto.Level)); + DefineMap(nameof(MediaType.ParentId), nameof(NodeDto.ParentId)); + DefineMap(nameof(MediaType.Path), nameof(NodeDto.Path)); + DefineMap(nameof(MediaType.SortOrder), nameof(NodeDto.SortOrder)); + DefineMap(nameof(MediaType.Name), nameof(NodeDto.Text)); + DefineMap(nameof(MediaType.Trashed), nameof(NodeDto.Trashed)); + DefineMap(nameof(MediaType.Key), nameof(NodeDto.UniqueId)); + DefineMap(nameof(MediaType.CreatorId), nameof(NodeDto.UserId)); + DefineMap(nameof(MediaType.Alias), nameof(ContentTypeDto.Alias)); + DefineMap(nameof(MediaType.AllowedAsRoot), nameof(ContentTypeDto.AllowAtRoot)); + DefineMap(nameof(MediaType.Description), nameof(ContentTypeDto.Description)); + DefineMap(nameof(MediaType.Icon), nameof(ContentTypeDto.Icon)); + DefineMap(nameof(MediaType.IsContainer), nameof(ContentTypeDto.IsContainer)); + DefineMap(nameof(MediaType.IsElement), nameof(ContentTypeDto.IsElement)); + DefineMap(nameof(MediaType.Thumbnail), nameof(ContentTypeDto.Thumbnail)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/MemberGroupMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/MemberGroupMapper.cs index 749335b0a2f6..4b431f41a1a7 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/MemberGroupMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/MemberGroupMapper.cs @@ -1,24 +1,23 @@ -using System; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +[MapperFor(typeof(IMemberGroup))] +[MapperFor(typeof(MemberGroup))] +public sealed class MemberGroupMapper : BaseMapper { - [MapperFor(typeof (IMemberGroup))] - [MapperFor(typeof (MemberGroup))] - public sealed class MemberGroupMapper : BaseMapper + public MemberGroupMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public MemberGroupMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(MemberGroup.Id), nameof(NodeDto.NodeId)); - DefineMap(nameof(MemberGroup.CreateDate), nameof(NodeDto.CreateDate)); - DefineMap(nameof(MemberGroup.CreatorId), nameof(NodeDto.UserId)); - DefineMap(nameof(MemberGroup.Name), nameof(NodeDto.Text)); - DefineMap(nameof(MemberGroup.Key), nameof(NodeDto.UniqueId)); - } + protected override void DefineMaps() + { + DefineMap(nameof(MemberGroup.Id), nameof(NodeDto.NodeId)); + DefineMap(nameof(MemberGroup.CreateDate), nameof(NodeDto.CreateDate)); + DefineMap(nameof(MemberGroup.CreatorId), nameof(NodeDto.UserId)); + DefineMap(nameof(MemberGroup.Name), nameof(NodeDto.Text)); + DefineMap(nameof(MemberGroup.Key), nameof(NodeDto.UniqueId)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/MemberMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/MemberMapper.cs index c9fce21a73c7..50211574cb98 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/MemberMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/MemberMapper.cs @@ -1,56 +1,56 @@ -using System; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(IMember))] +[MapperFor(typeof(Member))] +public sealed class MemberMapper : BaseMapper { - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(IMember))] - [MapperFor(typeof(Member))] - public sealed class MemberMapper : BaseMapper + public MemberMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public MemberMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(Member.Id), nameof(NodeDto.NodeId)); - DefineMap(nameof(Member.CreateDate), nameof(NodeDto.CreateDate)); - DefineMap(nameof(Member.Level), nameof(NodeDto.Level)); - DefineMap(nameof(Member.ParentId), nameof(NodeDto.ParentId)); - DefineMap(nameof(Member.Path), nameof(NodeDto.Path)); - DefineMap(nameof(Member.SortOrder), nameof(NodeDto.SortOrder)); - DefineMap(nameof(Member.CreatorId), nameof(NodeDto.UserId)); - DefineMap(nameof(Member.Name), nameof(NodeDto.Text)); - DefineMap(nameof(Member.Trashed), nameof(NodeDto.Trashed)); - DefineMap(nameof(Member.Key), nameof(NodeDto.UniqueId)); - DefineMap(nameof(Member.ContentTypeId), nameof(ContentDto.ContentTypeId)); - DefineMap(nameof(Member.ContentTypeAlias), nameof(ContentTypeDto.Alias)); - DefineMap(nameof(Member.UpdateDate), nameof(ContentVersionDto.VersionDate)); + protected override void DefineMaps() + { + DefineMap(nameof(Member.Id), nameof(NodeDto.NodeId)); + DefineMap(nameof(Member.CreateDate), nameof(NodeDto.CreateDate)); + DefineMap(nameof(Member.Level), nameof(NodeDto.Level)); + DefineMap(nameof(Member.ParentId), nameof(NodeDto.ParentId)); + DefineMap(nameof(Member.Path), nameof(NodeDto.Path)); + DefineMap(nameof(Member.SortOrder), nameof(NodeDto.SortOrder)); + DefineMap(nameof(Member.CreatorId), nameof(NodeDto.UserId)); + DefineMap(nameof(Member.Name), nameof(NodeDto.Text)); + DefineMap(nameof(Member.Trashed), nameof(NodeDto.Trashed)); + DefineMap(nameof(Member.Key), nameof(NodeDto.UniqueId)); + DefineMap(nameof(Member.ContentTypeId), nameof(ContentDto.ContentTypeId)); + DefineMap(nameof(Member.ContentTypeAlias), nameof(ContentTypeDto.Alias)); + DefineMap(nameof(Member.UpdateDate), nameof(ContentVersionDto.VersionDate)); - DefineMap(nameof(Member.Email), nameof(MemberDto.Email)); - DefineMap(nameof(Member.Username), nameof(MemberDto.LoginName)); - DefineMap(nameof(Member.RawPasswordValue), nameof(MemberDto.Password)); - DefineMap(nameof(Member.IsApproved), nameof(MemberDto.IsApproved)); - DefineMap(nameof(Member.IsLockedOut), nameof(MemberDto.IsLockedOut)); - DefineMap(nameof(Member.FailedPasswordAttempts), nameof(MemberDto.FailedPasswordAttempts)); - DefineMap(nameof(Member.LastLockoutDate), nameof(MemberDto.LastLockoutDate)); - DefineMap(nameof(Member.LastLoginDate), nameof(MemberDto.LastLoginDate)); - DefineMap(nameof(Member.LastPasswordChangeDate), nameof(MemberDto.LastPasswordChangeDate)); + DefineMap(nameof(Member.Email), nameof(MemberDto.Email)); + DefineMap(nameof(Member.Username), nameof(MemberDto.LoginName)); + DefineMap(nameof(Member.RawPasswordValue), nameof(MemberDto.Password)); + DefineMap(nameof(Member.IsApproved), nameof(MemberDto.IsApproved)); + DefineMap(nameof(Member.IsLockedOut), nameof(MemberDto.IsLockedOut)); + DefineMap(nameof(Member.FailedPasswordAttempts), nameof(MemberDto.FailedPasswordAttempts)); + DefineMap(nameof(Member.LastLockoutDate), nameof(MemberDto.LastLockoutDate)); + DefineMap(nameof(Member.LastLoginDate), nameof(MemberDto.LastLoginDate)); + DefineMap(nameof(Member.LastPasswordChangeDate), nameof(MemberDto.LastPasswordChangeDate)); - DefineMap(nameof(Member.Comments), nameof(PropertyDataDto.TextValue)); + DefineMap(nameof(Member.Comments), nameof(PropertyDataDto.TextValue)); - /* Internal experiment */ - DefineMap(nameof(Member.DateTimePropertyValue), nameof(PropertyDataDto.DateValue)); - DefineMap(nameof(Member.IntegerPropertyValue), nameof(PropertyDataDto.IntegerValue)); - DefineMap(nameof(Member.BoolPropertyValue), nameof(PropertyDataDto.IntegerValue)); - DefineMap(nameof(Member.LongStringPropertyValue), nameof(PropertyDataDto.TextValue)); - DefineMap(nameof(Member.ShortStringPropertyValue), nameof(PropertyDataDto.VarcharValue)); - DefineMap(nameof(Member.PropertyTypeAlias), nameof(PropertyTypeDto.Alias)); - } + /* Internal experiment */ + DefineMap(nameof(Member.DateTimePropertyValue), nameof(PropertyDataDto.DateValue)); + DefineMap(nameof(Member.IntegerPropertyValue), nameof(PropertyDataDto.IntegerValue)); + DefineMap(nameof(Member.BoolPropertyValue), nameof(PropertyDataDto.IntegerValue)); + DefineMap(nameof(Member.LongStringPropertyValue), nameof(PropertyDataDto.TextValue)); + DefineMap(nameof(Member.ShortStringPropertyValue), + nameof(PropertyDataDto.VarcharValue)); + DefineMap(nameof(Member.PropertyTypeAlias), nameof(PropertyTypeDto.Alias)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/MemberTypeMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/MemberTypeMapper.cs index d23e219e2446..4ee71186ea67 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/MemberTypeMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/MemberTypeMapper.cs @@ -1,40 +1,39 @@ -using System; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(MemberType))] +[MapperFor(typeof(IMemberType))] +public sealed class MemberTypeMapper : BaseMapper { - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof (MemberType))] - [MapperFor(typeof (IMemberType))] - public sealed class MemberTypeMapper : BaseMapper + public MemberTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public MemberTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(MemberType.Id), nameof(NodeDto.NodeId)); - DefineMap(nameof(MemberType.CreateDate), nameof(NodeDto.CreateDate)); - DefineMap(nameof(MemberType.Level), nameof(NodeDto.Level)); - DefineMap(nameof(MemberType.ParentId), nameof(NodeDto.ParentId)); - DefineMap(nameof(MemberType.Path), nameof(NodeDto.Path)); - DefineMap(nameof(MemberType.SortOrder), nameof(NodeDto.SortOrder)); - DefineMap(nameof(MemberType.Name), nameof(NodeDto.Text)); - DefineMap(nameof(MemberType.Trashed), nameof(NodeDto.Trashed)); - DefineMap(nameof(MemberType.Key), nameof(NodeDto.UniqueId)); - DefineMap(nameof(MemberType.CreatorId), nameof(NodeDto.UserId)); - DefineMap(nameof(MemberType.Alias), nameof(ContentTypeDto.Alias)); - DefineMap(nameof(MemberType.AllowedAsRoot), nameof(ContentTypeDto.AllowAtRoot)); - DefineMap(nameof(MemberType.Description), nameof(ContentTypeDto.Description)); - DefineMap(nameof(MemberType.Icon), nameof(ContentTypeDto.Icon)); - DefineMap(nameof(MemberType.IsContainer), nameof(ContentTypeDto.IsContainer)); - DefineMap(nameof(MemberType.IsElement), nameof(ContentTypeDto.IsElement)); - DefineMap(nameof(MemberType.Thumbnail), nameof(ContentTypeDto.Thumbnail)); - } + protected override void DefineMaps() + { + DefineMap(nameof(MemberType.Id), nameof(NodeDto.NodeId)); + DefineMap(nameof(MemberType.CreateDate), nameof(NodeDto.CreateDate)); + DefineMap(nameof(MemberType.Level), nameof(NodeDto.Level)); + DefineMap(nameof(MemberType.ParentId), nameof(NodeDto.ParentId)); + DefineMap(nameof(MemberType.Path), nameof(NodeDto.Path)); + DefineMap(nameof(MemberType.SortOrder), nameof(NodeDto.SortOrder)); + DefineMap(nameof(MemberType.Name), nameof(NodeDto.Text)); + DefineMap(nameof(MemberType.Trashed), nameof(NodeDto.Trashed)); + DefineMap(nameof(MemberType.Key), nameof(NodeDto.UniqueId)); + DefineMap(nameof(MemberType.CreatorId), nameof(NodeDto.UserId)); + DefineMap(nameof(MemberType.Alias), nameof(ContentTypeDto.Alias)); + DefineMap(nameof(MemberType.AllowedAsRoot), nameof(ContentTypeDto.AllowAtRoot)); + DefineMap(nameof(MemberType.Description), nameof(ContentTypeDto.Description)); + DefineMap(nameof(MemberType.Icon), nameof(ContentTypeDto.Icon)); + DefineMap(nameof(MemberType.IsContainer), nameof(ContentTypeDto.IsContainer)); + DefineMap(nameof(MemberType.IsElement), nameof(ContentTypeDto.IsElement)); + DefineMap(nameof(MemberType.Thumbnail), nameof(ContentTypeDto.Thumbnail)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/NullableDateMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/NullableDateMapper.cs index 86f5401e52c8..96e502242fb5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/NullableDateMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/NullableDateMapper.cs @@ -1,33 +1,31 @@ -using System; using System.Reflection; using NPoco; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +/// +/// Extends NPoco default mapper and ensures that nullable dates are not saved to the database. +/// +public class NullableDateMapper : DefaultMapper { - /// - /// Extends NPoco default mapper and ensures that nullable dates are not saved to the database. - /// - public class NullableDateMapper : DefaultMapper + public override Func? GetToDbConverter(Type destType, MemberInfo sourceMemberInfo) { - public override Func? GetToDbConverter(Type destType, MemberInfo sourceMemberInfo) + // ensures that NPoco does not try to insert an invalid date + // from a nullable DateTime property + if (sourceMemberInfo.GetMemberInfoType() == typeof(DateTime)) { - // ensures that NPoco does not try to insert an invalid date - // from a nullable DateTime property - if (sourceMemberInfo.GetMemberInfoType() == typeof(DateTime)) + return datetimeVal => { - return datetimeVal => + var datetime = datetimeVal as DateTime?; + if (datetime.HasValue && datetime.Value > DateTime.MinValue) { - var datetime = datetimeVal as DateTime?; - if (datetime.HasValue && datetime.Value > DateTime.MinValue) - { - return datetime.Value; - } - - return null; - }; - } + return datetime.Value; + } - return null; + return null; + }; } + + return null; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyGroupMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyGroupMapper.cs index 47c7df3a8ea0..c7fce999d48a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyGroupMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyGroupMapper.cs @@ -1,28 +1,29 @@ -using System; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(PropertyGroup))] +public sealed class PropertyGroupMapper : BaseMapper { - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(PropertyGroup))] - public sealed class PropertyGroupMapper : BaseMapper + public PropertyGroupMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public PropertyGroupMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(PropertyGroup.Id), nameof(PropertyTypeGroupDto.Id)); - DefineMap(nameof(PropertyGroup.Key), nameof(PropertyTypeGroupDto.UniqueId)); - DefineMap(nameof(PropertyGroup.Type), nameof(PropertyTypeGroupDto.Type)); - DefineMap(nameof(PropertyGroup.Name), nameof(PropertyTypeGroupDto.Text)); - DefineMap(nameof(PropertyGroup.Alias), nameof(PropertyTypeGroupDto.Alias)); - DefineMap(nameof(PropertyGroup.SortOrder), nameof(PropertyTypeGroupDto.SortOrder)); - } + protected override void DefineMaps() + { + DefineMap(nameof(PropertyGroup.Id), nameof(PropertyTypeGroupDto.Id)); + DefineMap(nameof(PropertyGroup.Key), + nameof(PropertyTypeGroupDto.UniqueId)); + DefineMap(nameof(PropertyGroup.Type), nameof(PropertyTypeGroupDto.Type)); + DefineMap(nameof(PropertyGroup.Name), nameof(PropertyTypeGroupDto.Text)); + DefineMap(nameof(PropertyGroup.Alias), nameof(PropertyTypeGroupDto.Alias)); + DefineMap(nameof(PropertyGroup.SortOrder), + nameof(PropertyTypeGroupDto.SortOrder)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyMapper.cs index 08ca8d6a1337..44418c0663b1 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyMapper.cs @@ -1,20 +1,19 @@ -using System; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +[MapperFor(typeof(Property))] +public sealed class PropertyMapper : BaseMapper { - [MapperFor(typeof(Property))] - public sealed class PropertyMapper : BaseMapper + public PropertyMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public PropertyMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(Property.Id), nameof(PropertyDataDto.Id)); - DefineMap(nameof(Property.PropertyTypeId), nameof(PropertyDataDto.PropertyTypeId)); - } + protected override void DefineMaps() + { + DefineMap(nameof(Property.Id), nameof(PropertyDataDto.Id)); + DefineMap(nameof(Property.PropertyTypeId), nameof(PropertyDataDto.PropertyTypeId)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyTypeMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyTypeMapper.cs index 3da3d16fcbee..eac819f87d2a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyTypeMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyTypeMapper.cs @@ -1,36 +1,38 @@ -using System; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(PropertyType))] +public sealed class PropertyTypeMapper : BaseMapper { - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(PropertyType))] - public sealed class PropertyTypeMapper : BaseMapper + public PropertyTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public PropertyTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(PropertyType.Key), nameof(PropertyTypeDto.UniqueId)); - DefineMap(nameof(PropertyType.Id), nameof(PropertyTypeDto.Id)); - DefineMap(nameof(PropertyType.Alias), nameof(PropertyTypeDto.Alias)); - DefineMap(nameof(PropertyType.DataTypeId), nameof(PropertyTypeDto.DataTypeId)); - DefineMap(nameof(PropertyType.Description), nameof(PropertyTypeDto.Description)); - DefineMap(nameof(PropertyType.Mandatory), nameof(PropertyTypeDto.Mandatory)); - DefineMap(nameof(PropertyType.MandatoryMessage), nameof(PropertyTypeDto.MandatoryMessage)); - DefineMap(nameof(PropertyType.Name), nameof(PropertyTypeDto.Name)); - DefineMap(nameof(PropertyType.SortOrder), nameof(PropertyTypeDto.SortOrder)); - DefineMap(nameof(PropertyType.ValidationRegExp), nameof(PropertyTypeDto.ValidationRegExp)); - DefineMap(nameof(PropertyType.ValidationRegExpMessage), nameof(PropertyTypeDto.ValidationRegExpMessage)); - DefineMap(nameof(PropertyType.LabelOnTop), nameof(PropertyTypeDto.LabelOnTop)); - DefineMap(nameof(PropertyType.PropertyEditorAlias), nameof(DataTypeDto.EditorAlias)); - DefineMap(nameof(PropertyType.ValueStorageType), nameof(DataTypeDto.DbType)); - } + protected override void DefineMaps() + { + DefineMap(nameof(PropertyType.Key), nameof(PropertyTypeDto.UniqueId)); + DefineMap(nameof(PropertyType.Id), nameof(PropertyTypeDto.Id)); + DefineMap(nameof(PropertyType.Alias), nameof(PropertyTypeDto.Alias)); + DefineMap(nameof(PropertyType.DataTypeId), nameof(PropertyTypeDto.DataTypeId)); + DefineMap(nameof(PropertyType.Description), nameof(PropertyTypeDto.Description)); + DefineMap(nameof(PropertyType.Mandatory), nameof(PropertyTypeDto.Mandatory)); + DefineMap(nameof(PropertyType.MandatoryMessage), + nameof(PropertyTypeDto.MandatoryMessage)); + DefineMap(nameof(PropertyType.Name), nameof(PropertyTypeDto.Name)); + DefineMap(nameof(PropertyType.SortOrder), nameof(PropertyTypeDto.SortOrder)); + DefineMap(nameof(PropertyType.ValidationRegExp), + nameof(PropertyTypeDto.ValidationRegExp)); + DefineMap(nameof(PropertyType.ValidationRegExpMessage), + nameof(PropertyTypeDto.ValidationRegExpMessage)); + DefineMap(nameof(PropertyType.LabelOnTop), nameof(PropertyTypeDto.LabelOnTop)); + DefineMap(nameof(PropertyType.PropertyEditorAlias), nameof(DataTypeDto.EditorAlias)); + DefineMap(nameof(PropertyType.ValueStorageType), nameof(DataTypeDto.DbType)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/RelationMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/RelationMapper.cs index e75492be1850..6dcbd27877cd 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/RelationMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/RelationMapper.cs @@ -1,29 +1,28 @@ -using System; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(IRelation))] +[MapperFor(typeof(Relation))] +public sealed class RelationMapper : BaseMapper { - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(IRelation))] - [MapperFor(typeof(Relation))] - public sealed class RelationMapper : BaseMapper + public RelationMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public RelationMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(Relation.Id), nameof(RelationDto.Id)); - DefineMap(nameof(Relation.ChildId), nameof(RelationDto.ChildId)); - DefineMap(nameof(Relation.Comment), nameof(RelationDto.Comment)); - DefineMap(nameof(Relation.CreateDate), nameof(RelationDto.Datetime)); - DefineMap(nameof(Relation.ParentId), nameof(RelationDto.ParentId)); - DefineMap(nameof(Relation.RelationTypeId), nameof(RelationDto.RelationType)); - } + protected override void DefineMaps() + { + DefineMap(nameof(Relation.Id), nameof(RelationDto.Id)); + DefineMap(nameof(Relation.ChildId), nameof(RelationDto.ChildId)); + DefineMap(nameof(Relation.Comment), nameof(RelationDto.Comment)); + DefineMap(nameof(Relation.CreateDate), nameof(RelationDto.Datetime)); + DefineMap(nameof(Relation.ParentId), nameof(RelationDto.ParentId)); + DefineMap(nameof(Relation.RelationTypeId), nameof(RelationDto.RelationType)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/RelationTypeMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/RelationTypeMapper.cs index 732563fef7ca..10b32b704f04 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/RelationTypeMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/RelationTypeMapper.cs @@ -1,30 +1,32 @@ -using System; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(RelationType))] +[MapperFor(typeof(IRelationType))] +public sealed class RelationTypeMapper : BaseMapper { - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(RelationType))] - [MapperFor(typeof(IRelationType))] - public sealed class RelationTypeMapper : BaseMapper + public RelationTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public RelationTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(RelationType.Id), nameof(RelationTypeDto.Id)); - DefineMap(nameof(RelationType.Alias), nameof(RelationTypeDto.Alias)); - DefineMap(nameof(RelationType.ChildObjectType), nameof(RelationTypeDto.ChildObjectType)); - DefineMap(nameof(RelationType.IsBidirectional), nameof(RelationTypeDto.Dual)); - DefineMap(nameof(RelationType.IsDependency), nameof(RelationTypeDto.IsDependency)); - DefineMap(nameof(RelationType.Name), nameof(RelationTypeDto.Name)); - DefineMap(nameof(RelationType.ParentObjectType), nameof(RelationTypeDto.ParentObjectType)); - } + protected override void DefineMaps() + { + DefineMap(nameof(RelationType.Id), nameof(RelationTypeDto.Id)); + DefineMap(nameof(RelationType.Alias), nameof(RelationTypeDto.Alias)); + DefineMap(nameof(RelationType.ChildObjectType), + nameof(RelationTypeDto.ChildObjectType)); + DefineMap(nameof(RelationType.IsBidirectional), nameof(RelationTypeDto.Dual)); + DefineMap(nameof(RelationType.IsDependency), + nameof(RelationTypeDto.IsDependency)); + DefineMap(nameof(RelationType.Name), nameof(RelationTypeDto.Name)); + DefineMap(nameof(RelationType.ParentObjectType), + nameof(RelationTypeDto.ParentObjectType)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/ServerRegistrationMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/ServerRegistrationMapper.cs index 61b597be46a9..ae6ee25b585b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/ServerRegistrationMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/ServerRegistrationMapper.cs @@ -1,26 +1,32 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +[MapperFor(typeof(ServerRegistration))] +[MapperFor(typeof(IServerRegistration))] +internal sealed class ServerRegistrationMapper : BaseMapper { - [MapperFor(typeof(ServerRegistration))] - [MapperFor(typeof(IServerRegistration))] - internal sealed class ServerRegistrationMapper : BaseMapper + public ServerRegistrationMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public ServerRegistrationMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(ServerRegistration.Id), nameof(ServerRegistrationDto.Id)); - DefineMap(nameof(ServerRegistration.IsActive), nameof(ServerRegistrationDto.IsActive)); - DefineMap(nameof(ServerRegistration.IsSchedulingPublisher), nameof(ServerRegistrationDto.IsSchedulingPublisher)); - DefineMap(nameof(ServerRegistration.ServerAddress), nameof(ServerRegistrationDto.ServerAddress)); - DefineMap(nameof(ServerRegistration.CreateDate), nameof(ServerRegistrationDto.DateRegistered)); - DefineMap(nameof(ServerRegistration.UpdateDate), nameof(ServerRegistrationDto.DateAccessed)); - DefineMap(nameof(ServerRegistration.ServerIdentity), nameof(ServerRegistrationDto.ServerIdentity)); - } + protected override void DefineMaps() + { + DefineMap(nameof(ServerRegistration.Id), + nameof(ServerRegistrationDto.Id)); + DefineMap(nameof(ServerRegistration.IsActive), + nameof(ServerRegistrationDto.IsActive)); + DefineMap(nameof(ServerRegistration.IsSchedulingPublisher), + nameof(ServerRegistrationDto.IsSchedulingPublisher)); + DefineMap(nameof(ServerRegistration.ServerAddress), + nameof(ServerRegistrationDto.ServerAddress)); + DefineMap(nameof(ServerRegistration.CreateDate), + nameof(ServerRegistrationDto.DateRegistered)); + DefineMap(nameof(ServerRegistration.UpdateDate), + nameof(ServerRegistrationDto.DateAccessed)); + DefineMap(nameof(ServerRegistration.ServerIdentity), + nameof(ServerRegistrationDto.ServerIdentity)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/SimpleContentTypeMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/SimpleContentTypeMapper.cs index b2bcc6098ae0..57a25f3af4b8 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/SimpleContentTypeMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/SimpleContentTypeMapper.cs @@ -1,36 +1,34 @@ -using System; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - // TODO: This mapper is actually very useless because the only time it would ever be used is when trying to generate a strongly typed query - // on an IContentBase object which is what exposes ISimpleContentType, however the queries that we execute in the content repositories don't actually - // join on the content type table. The content type data is resolved outside of the query so the end result of the query that is generated by using - // this mapper will either fail or the syntax will target umbracoNode which will be filtering on the actual content NOT the content type. - // I'm leaving this here purely because the ExpressionTests rely on this which is fine for testing that the expressions work, but note that this - // resulting query will not. +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; +// TODO: This mapper is actually very useless because the only time it would ever be used is when trying to generate a strongly typed query +// on an IContentBase object which is what exposes ISimpleContentType, however the queries that we execute in the content repositories don't actually +// join on the content type table. The content type data is resolved outside of the query so the end result of the query that is generated by using +// this mapper will either fail or the syntax will target umbracoNode which will be filtering on the actual content NOT the content type. +// I'm leaving this here purely because the ExpressionTests rely on this which is fine for testing that the expressions work, but note that this +// resulting query will not. - [MapperFor(typeof(ISimpleContentType))] - [MapperFor(typeof(SimpleContentType))] - public sealed class SimpleContentTypeMapper : BaseMapper +[MapperFor(typeof(ISimpleContentType))] +[MapperFor(typeof(SimpleContentType))] +public sealed class SimpleContentTypeMapper : BaseMapper +{ + public SimpleContentTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public SimpleContentTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - // There is no reason for using ContentType here instead of SimpleContentType, in fact the underlying DefineMap call does nothing with the first type parameter + protected override void DefineMaps() + { + // There is no reason for using ContentType here instead of SimpleContentType, in fact the underlying DefineMap call does nothing with the first type parameter - DefineMap(nameof(ContentType.Id), nameof(NodeDto.NodeId)); - DefineMap(nameof(ContentType.Key), nameof(NodeDto.UniqueId)); - DefineMap(nameof(ContentType.Name), nameof(NodeDto.Text)); - DefineMap(nameof(ContentType.Alias), nameof(ContentTypeDto.Alias)); - DefineMap(nameof(ContentType.Icon), nameof(ContentTypeDto.Icon)); - DefineMap(nameof(ContentType.IsContainer), nameof(ContentTypeDto.IsContainer)); - DefineMap(nameof(ContentType.AllowedAsRoot), nameof(ContentTypeDto.AllowAtRoot)); - DefineMap(nameof(ContentType.IsElement), nameof(ContentTypeDto.IsElement)); - } + DefineMap(nameof(ContentType.Id), nameof(NodeDto.NodeId)); + DefineMap(nameof(ContentType.Key), nameof(NodeDto.UniqueId)); + DefineMap(nameof(ContentType.Name), nameof(NodeDto.Text)); + DefineMap(nameof(ContentType.Alias), nameof(ContentTypeDto.Alias)); + DefineMap(nameof(ContentType.Icon), nameof(ContentTypeDto.Icon)); + DefineMap(nameof(ContentType.IsContainer), nameof(ContentTypeDto.IsContainer)); + DefineMap(nameof(ContentType.AllowedAsRoot), nameof(ContentTypeDto.AllowAtRoot)); + DefineMap(nameof(ContentType.IsElement), nameof(ContentTypeDto.IsElement)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/TagMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/TagMapper.cs index 8c6df88a4d75..8dd5c51049c8 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/TagMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/TagMapper.cs @@ -1,27 +1,26 @@ -using System; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(Tag))] +[MapperFor(typeof(ITag))] +public sealed class TagMapper : BaseMapper { - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(Tag))] - [MapperFor(typeof(ITag))] - public sealed class TagMapper : BaseMapper + public TagMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public TagMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(Tag.Id), nameof(TagDto.Id)); - DefineMap(nameof(Tag.Text), nameof(TagDto.Text)); - DefineMap(nameof(Tag.Group), nameof(TagDto.Group)); - DefineMap(nameof(Tag.LanguageId), nameof(TagDto.LanguageId)); - } + protected override void DefineMaps() + { + DefineMap(nameof(Tag.Id), nameof(TagDto.Id)); + DefineMap(nameof(Tag.Text), nameof(TagDto.Text)); + DefineMap(nameof(Tag.Group), nameof(TagDto.Group)); + DefineMap(nameof(Tag.LanguageId), nameof(TagDto.LanguageId)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/TemplateMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/TemplateMapper.cs index f2c8627cc96f..716ccba02e8f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/TemplateMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/TemplateMapper.cs @@ -1,27 +1,26 @@ -using System; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(Template))] +[MapperFor(typeof(ITemplate))] +public sealed class TemplateMapper : BaseMapper { - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(Template))] - [MapperFor(typeof(ITemplate))] - public sealed class TemplateMapper : BaseMapper + public TemplateMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public TemplateMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(Template.Id), nameof(TemplateDto.NodeId)); - DefineMap(nameof(Template.MasterTemplateId), nameof(NodeDto.ParentId)); - DefineMap(nameof(Template.Key), nameof(NodeDto.UniqueId)); - DefineMap(nameof(Template.Alias), nameof(TemplateDto.Alias)); - } + protected override void DefineMaps() + { + DefineMap(nameof(Template.Id), nameof(TemplateDto.NodeId)); + DefineMap(nameof(Template.MasterTemplateId), nameof(NodeDto.ParentId)); + DefineMap(nameof(Template.Key), nameof(NodeDto.UniqueId)); + DefineMap(nameof(Template.Alias), nameof(TemplateDto.Alias)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/UmbracoEntityMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/UmbracoEntityMapper.cs index 5f608616675a..31b08bedf5a0 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/UmbracoEntityMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/UmbracoEntityMapper.cs @@ -1,28 +1,27 @@ -using System; -using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +[MapperFor(typeof(IUmbracoEntity))] +public sealed class UmbracoEntityMapper : BaseMapper { - [MapperFor(typeof (IUmbracoEntity))] - public sealed class UmbracoEntityMapper : BaseMapper + public UmbracoEntityMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public UmbracoEntityMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(IUmbracoEntity.Id), nameof(NodeDto.NodeId)); - DefineMap(nameof(IUmbracoEntity.CreateDate), nameof(NodeDto.CreateDate)); - DefineMap(nameof(IUmbracoEntity.Level), nameof(NodeDto.Level)); - DefineMap(nameof(IUmbracoEntity.ParentId), nameof(NodeDto.ParentId)); - DefineMap(nameof(IUmbracoEntity.Path), nameof(NodeDto.Path)); - DefineMap(nameof(IUmbracoEntity.SortOrder), nameof(NodeDto.SortOrder)); - DefineMap(nameof(IUmbracoEntity.Name), nameof(NodeDto.Text)); - DefineMap(nameof(IUmbracoEntity.Trashed), nameof(NodeDto.Trashed)); - DefineMap(nameof(IUmbracoEntity.Key), nameof(NodeDto.UniqueId)); - DefineMap(nameof(IUmbracoEntity.CreatorId), nameof(NodeDto.UserId)); - } + protected override void DefineMaps() + { + DefineMap(nameof(IUmbracoEntity.Id), nameof(NodeDto.NodeId)); + DefineMap(nameof(IUmbracoEntity.CreateDate), nameof(NodeDto.CreateDate)); + DefineMap(nameof(IUmbracoEntity.Level), nameof(NodeDto.Level)); + DefineMap(nameof(IUmbracoEntity.ParentId), nameof(NodeDto.ParentId)); + DefineMap(nameof(IUmbracoEntity.Path), nameof(NodeDto.Path)); + DefineMap(nameof(IUmbracoEntity.SortOrder), nameof(NodeDto.SortOrder)); + DefineMap(nameof(IUmbracoEntity.Name), nameof(NodeDto.Text)); + DefineMap(nameof(IUmbracoEntity.Trashed), nameof(NodeDto.Trashed)); + DefineMap(nameof(IUmbracoEntity.Key), nameof(NodeDto.UniqueId)); + DefineMap(nameof(IUmbracoEntity.CreatorId), nameof(NodeDto.UserId)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/UserGroupMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/UserGroupMapper.cs index 51f4d0bbcc29..3fe0f6bfdbe8 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/UserGroupMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/UserGroupMapper.cs @@ -1,29 +1,28 @@ -using System; -using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(IUserGroup))] +[MapperFor(typeof(UserGroup))] +public sealed class UserGroupMapper : BaseMapper { - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(IUserGroup))] - [MapperFor(typeof(UserGroup))] - public sealed class UserGroupMapper : BaseMapper + public UserGroupMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public UserGroupMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(UserGroup.Id), nameof(UserGroupDto.Id)); - DefineMap(nameof(UserGroup.Alias), nameof(UserGroupDto.Alias)); - DefineMap(nameof(UserGroup.Name), nameof(UserGroupDto.Name)); - DefineMap(nameof(UserGroup.Icon), nameof(UserGroupDto.Icon)); - DefineMap(nameof(UserGroup.StartContentId), nameof(UserGroupDto.StartContentId)); - DefineMap(nameof(UserGroup.StartMediaId), nameof(UserGroupDto.StartMediaId)); - } + protected override void DefineMaps() + { + DefineMap(nameof(UserGroup.Id), nameof(UserGroupDto.Id)); + DefineMap(nameof(UserGroup.Alias), nameof(UserGroupDto.Alias)); + DefineMap(nameof(UserGroup.Name), nameof(UserGroupDto.Name)); + DefineMap(nameof(UserGroup.Icon), nameof(UserGroupDto.Icon)); + DefineMap(nameof(UserGroup.StartContentId), nameof(UserGroupDto.StartContentId)); + DefineMap(nameof(UserGroup.StartMediaId), nameof(UserGroupDto.StartMediaId)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/UserMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/UserMapper.cs index 53c229c93506..d8d4a2d49241 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/UserMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/UserMapper.cs @@ -1,35 +1,34 @@ -using System; -using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +[MapperFor(typeof(IUser))] +[MapperFor(typeof(User))] +public sealed class UserMapper : BaseMapper { - [MapperFor(typeof(IUser))] - [MapperFor(typeof(User))] - public sealed class UserMapper : BaseMapper + public UserMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public UserMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(User.Id), nameof(UserDto.Id)); - DefineMap(nameof(User.Email), nameof(UserDto.Email)); - DefineMap(nameof(User.Username), nameof(UserDto.Login)); - DefineMap(nameof(User.RawPasswordValue), nameof(UserDto.Password)); - DefineMap(nameof(User.Name), nameof(UserDto.UserName)); - //NOTE: This column in the db is *not* used! - //DefineMap(nameof(User.DefaultPermissions), nameof(UserDto.DefaultPermissions)); - DefineMap(nameof(User.IsApproved), nameof(UserDto.Disabled)); - DefineMap(nameof(User.IsLockedOut), nameof(UserDto.NoConsole)); - DefineMap(nameof(User.Language), nameof(UserDto.UserLanguage)); - DefineMap(nameof(User.CreateDate), nameof(UserDto.CreateDate)); - DefineMap(nameof(User.UpdateDate), nameof(UserDto.UpdateDate)); - DefineMap(nameof(User.LastLockoutDate), nameof(UserDto.LastLockoutDate)); - DefineMap(nameof(User.LastLoginDate), nameof(UserDto.LastLoginDate)); - DefineMap(nameof(User.LastPasswordChangeDate), nameof(UserDto.LastPasswordChangeDate)); - DefineMap(nameof(User.SecurityStamp), nameof(UserDto.SecurityStampToken)); - } + protected override void DefineMaps() + { + DefineMap(nameof(User.Id), nameof(UserDto.Id)); + DefineMap(nameof(User.Email), nameof(UserDto.Email)); + DefineMap(nameof(User.Username), nameof(UserDto.Login)); + DefineMap(nameof(User.RawPasswordValue), nameof(UserDto.Password)); + DefineMap(nameof(User.Name), nameof(UserDto.UserName)); + //NOTE: This column in the db is *not* used! + //DefineMap(nameof(User.DefaultPermissions), nameof(UserDto.DefaultPermissions)); + DefineMap(nameof(User.IsApproved), nameof(UserDto.Disabled)); + DefineMap(nameof(User.IsLockedOut), nameof(UserDto.NoConsole)); + DefineMap(nameof(User.Language), nameof(UserDto.UserLanguage)); + DefineMap(nameof(User.CreateDate), nameof(UserDto.CreateDate)); + DefineMap(nameof(User.UpdateDate), nameof(UserDto.UpdateDate)); + DefineMap(nameof(User.LastLockoutDate), nameof(UserDto.LastLockoutDate)); + DefineMap(nameof(User.LastLoginDate), nameof(UserDto.LastLoginDate)); + DefineMap(nameof(User.LastPasswordChangeDate), nameof(UserDto.LastPasswordChangeDate)); + DefineMap(nameof(User.SecurityStamp), nameof(UserDto.SecurityStampToken)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/UserSectionMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/UserSectionMapper.cs index 86aeed4b7eb9..d7b04004fbbb 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/UserSectionMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/UserSectionMapper.cs @@ -1,30 +1,30 @@ -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - //[MapperFor(typeof(UserSection))] - //public sealed class UserSectionMapper : BaseMapper - //{ - // private static readonly ConcurrentDictionary PropertyInfoCacheInstance = new ConcurrentDictionary(); +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - // //NOTE: its an internal class but the ctor must be public since we're using Activator.CreateInstance to create it - // // otherwise that would fail because there is no public constructor. - // public UserSectionMapper() - // { - // BuildMap(); - // } - // #region Overrides of BaseMapper +//[MapperFor(typeof(UserSection))] +//public sealed class UserSectionMapper : BaseMapper +//{ +// private static readonly ConcurrentDictionary PropertyInfoCacheInstance = new ConcurrentDictionary(); - // internal override ConcurrentDictionary PropertyInfoCache - // { - // get { return PropertyInfoCacheInstance; } - // } +// //NOTE: its an internal class but the ctor must be public since we're using Activator.CreateInstance to create it +// // otherwise that would fail because there is no public constructor. +// public UserSectionMapper() +// { +// BuildMap(); +// } - // internal override void BuildMap() - // { - // CacheMap(src => src.UserId, dto => dto.UserId); - // CacheMap(src => src.SectionAlias, dto => dto.AppAlias); - // } +// #region Overrides of BaseMapper - // #endregion - //} -} +// internal override ConcurrentDictionary PropertyInfoCache +// { +// get { return PropertyInfoCacheInstance; } +// } + +// internal override void BuildMap() +// { +// CacheMap(src => src.UserId, dto => dto.UserId); +// CacheMap(src => src.SectionAlias, dto => dto.AppAlias); +// } + +// #endregion +//} diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions-Bulk.cs b/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions-Bulk.cs index c53076ff18e4..72bc0155517a 100644 --- a/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions-Bulk.cs +++ b/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions-Bulk.cs @@ -1,121 +1,117 @@ -using System; -using System.Collections.Generic; -using System.Data; +using System.Data; using System.Data.Common; -using System.Linq; using Microsoft.Data.SqlClient; using NPoco; using NPoco.SqlServer; using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods to NPoco Database class. +/// +public static partial class NPocoDatabaseExtensions { /// - /// Provides extension methods to NPoco Database class. + /// Configures NPoco's SqlBulkCopyHelper to use the correct SqlConnection and SqlTransaction instances from the + /// underlying RetryDbConnection and ProfiledDbTransaction /// - public static partial class NPocoDatabaseExtensions + /// + /// This is required to use NPoco's own method because we use + /// wrapped DbConnection and DbTransaction instances. + /// NPoco's InsertBulk method only caters for efficient bulk inserting records for Sql Server, it does not cater for + /// bulk inserting of records for + /// any other database type and in which case will just insert records one at a time. + /// NPoco's InsertBulk method also deals with updating the passed in entity's PK/ID once it's inserted whereas our own + /// BulkInsertRecords methods + /// do not handle this scenario. + /// + public static void ConfigureNPocoBulkExtensions() { - /// - /// Configures NPoco's SqlBulkCopyHelper to use the correct SqlConnection and SqlTransaction instances from the - /// underlying RetryDbConnection and ProfiledDbTransaction - /// - /// - /// This is required to use NPoco's own method because we use - /// wrapped DbConnection and DbTransaction instances. - /// NPoco's InsertBulk method only caters for efficient bulk inserting records for Sql Server, it does not cater for - /// bulk inserting of records for - /// any other database type and in which case will just insert records one at a time. - /// NPoco's InsertBulk method also deals with updating the passed in entity's PK/ID once it's inserted whereas our own - /// BulkInsertRecords methods - /// do not handle this scenario. - /// - public static void ConfigureNPocoBulkExtensions() - { - SqlBulkCopyHelper.SqlConnectionResolver = dbConn => GetTypedConnection(dbConn); - SqlBulkCopyHelper.SqlTransactionResolver = dbTran => GetTypedTransaction(dbTran); - } + SqlBulkCopyHelper.SqlConnectionResolver = dbConn => GetTypedConnection(dbConn); + SqlBulkCopyHelper.SqlTransactionResolver = dbTran => GetTypedTransaction(dbTran); + } - /// - /// Creates bulk-insert commands. - /// - /// The type of the records. - /// The database. - /// The records. - /// The sql commands to execute. - internal static IDbCommand[] GenerateBulkInsertCommands(this IUmbracoDatabase database, T[] records) + /// + /// Creates bulk-insert commands. + /// + /// The type of the records. + /// The database. + /// The records. + /// The sql commands to execute. + internal static IDbCommand[] GenerateBulkInsertCommands(this IUmbracoDatabase database, T[] records) + { + if (database?.Connection == null) { - if (database?.Connection == null) - { - throw new ArgumentException("Null database?.connection.", nameof(database)); - } + throw new ArgumentException("Null database?.connection.", nameof(database)); + } - PocoData pocoData = database.PocoDataFactory.ForType(typeof(T)); + PocoData pocoData = database.PocoDataFactory.ForType(typeof(T)); - // get columns to include, = number of parameters per row - KeyValuePair[] columns = - pocoData.Columns.Where(c => IncludeColumn(pocoData, c)).ToArray(); - var paramsPerRecord = columns.Length; + // get columns to include, = number of parameters per row + KeyValuePair[] columns = + pocoData.Columns.Where(c => IncludeColumn(pocoData, c)).ToArray(); + var paramsPerRecord = columns.Length; - // format columns to sql - var tableName = database.DatabaseType.EscapeTableName(pocoData.TableInfo.TableName); - var columnNames = string.Join(", ", - columns.Select(c => tableName + "." + database.DatabaseType.EscapeSqlIdentifier(c.Key))); + // format columns to sql + var tableName = database.DatabaseType.EscapeTableName(pocoData.TableInfo.TableName); + var columnNames = string.Join(", ", + columns.Select(c => tableName + "." + database.DatabaseType.EscapeSqlIdentifier(c.Key))); - // example: - // assume 4168 records, each record containing 8 fields, ie 8 command parameters - // max 2100 parameter per command - // Math.Floor(2100 / 8) = 262 record per command - // 4168 / 262 = 15.908... = there will be 16 command in total - // (if we have disabled db parameters, then all records will be included, in only one command) - var recordsPerCommand = paramsPerRecord == 0 - ? int.MaxValue - : Convert.ToInt32(Math.Floor((double)Constants.Sql.MaxParameterCount / paramsPerRecord)); - var commandsCount = Convert.ToInt32(Math.Ceiling((double)records.Length / recordsPerCommand)); + // example: + // assume 4168 records, each record containing 8 fields, ie 8 command parameters + // max 2100 parameter per command + // Math.Floor(2100 / 8) = 262 record per command + // 4168 / 262 = 15.908... = there will be 16 command in total + // (if we have disabled db parameters, then all records will be included, in only one command) + var recordsPerCommand = paramsPerRecord == 0 + ? int.MaxValue + : Convert.ToInt32(Math.Floor((double)Constants.Sql.MaxParameterCount / paramsPerRecord)); + var commandsCount = Convert.ToInt32(Math.Ceiling((double)records.Length / recordsPerCommand)); - var commands = new IDbCommand[commandsCount]; - var recordsIndex = 0; - var recordsLeftToInsert = records.Length; - var prefix = database.DatabaseType.GetParameterPrefix(database.ConnectionString); - for (var commandIndex = 0; commandIndex < commandsCount; commandIndex++) + var commands = new IDbCommand[commandsCount]; + var recordsIndex = 0; + var recordsLeftToInsert = records.Length; + var prefix = database.DatabaseType.GetParameterPrefix(database.ConnectionString); + for (var commandIndex = 0; commandIndex < commandsCount; commandIndex++) + { + DbCommand command = database.CreateCommand(database.Connection, CommandType.Text, string.Empty); + var parameterIndex = 0; + var commandRecords = Math.Min(recordsPerCommand, recordsLeftToInsert); + var recordsValues = new string[commandRecords]; + for (var commandRecordIndex = 0; + commandRecordIndex < commandRecords; + commandRecordIndex++, recordsIndex++, recordsLeftToInsert--) { - DbCommand command = database.CreateCommand(database.Connection, CommandType.Text, string.Empty); - var parameterIndex = 0; - var commandRecords = Math.Min(recordsPerCommand, recordsLeftToInsert); - var recordsValues = new string[commandRecords]; - for (var commandRecordIndex = 0; - commandRecordIndex < commandRecords; - commandRecordIndex++, recordsIndex++, recordsLeftToInsert--) + T record = records[recordsIndex]; + var recordValues = new string[columns.Length]; + for (var columnIndex = 0; columnIndex < columns.Length; columnIndex++) { - T record = records[recordsIndex]; - var recordValues = new string[columns.Length]; - for (var columnIndex = 0; columnIndex < columns.Length; columnIndex++) - { - database.AddParameter(command, columns[columnIndex].Value.GetValue(record)); - recordValues[columnIndex] = prefix + parameterIndex++; - } - - recordsValues[commandRecordIndex] = "(" + string.Join(",", recordValues) + ")"; + database.AddParameter(command, columns[columnIndex].Value.GetValue(record)); + recordValues[columnIndex] = prefix + parameterIndex++; } - command.CommandText = - $"INSERT INTO {tableName} ({columnNames}) VALUES {string.Join(", ", recordsValues)}"; - commands[commandIndex] = command; + recordsValues[commandRecordIndex] = "(" + string.Join(",", recordValues) + ")"; } - return commands; + command.CommandText = + $"INSERT INTO {tableName} ({columnNames}) VALUES {string.Join(", ", recordsValues)}"; + commands[commandIndex] = command; } - /// - /// Determines whether a column should be part of a bulk-insert. - /// - /// The PocoData object corresponding to the record's type. - /// The column. - /// A value indicating whether the column should be part of the bulk-insert. - /// Columns that are primary keys and auto-incremental, or result columns, are excluded from bulk-inserts. - public static bool IncludeColumn(PocoData pocoData, KeyValuePair column) => - column.Value.ResultColumn == false - && (pocoData.TableInfo.AutoIncrement == false || column.Key != pocoData.TableInfo.PrimaryKey); + return commands; } + + /// + /// Determines whether a column should be part of a bulk-insert. + /// + /// The PocoData object corresponding to the record's type. + /// The column. + /// A value indicating whether the column should be part of the bulk-insert. + /// Columns that are primary keys and auto-incremental, or result columns, are excluded from bulk-inserts. + public static bool IncludeColumn(PocoData pocoData, KeyValuePair column) => + column.Value.ResultColumn == false + && (pocoData.TableInfo.AutoIncrement == false || column.Key != pocoData.TableInfo.PrimaryKey); } diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions.cs b/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions.cs index 7537ffc48f5f..8b6c1f727e80 100644 --- a/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions.cs @@ -1,6 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Data; +using System.Data; +using System.Data.Common; using System.Text.RegularExpressions; using Microsoft.Data.SqlClient; using NPoco; @@ -9,290 +8,319 @@ using Umbraco.Cms.Infrastructure.Persistence.FaultHandling; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods to NPoco Database class. +/// +public static partial class NPocoDatabaseExtensions { /// - /// Provides extension methods to NPoco Database class. + /// Iterates over the result of a paged data set with a db reader /// - public static partial class NPocoDatabaseExtensions + /// + /// + /// + /// The number of rows to load per page + /// + /// + /// + /// Specify a custom Sql command to get the total count, if null is specified than the + /// auto-generated sql count will be used + /// + /// + /// + /// NPoco's normal Page returns a List{T} but sometimes we don't want all that in memory and instead want to + /// iterate over each row with a reader using Query vs Fetch. + /// + public static IEnumerable QueryPaged(this IDatabase database, long pageSize, Sql sql, Sql? sqlCount) { - /// - /// Iterates over the result of a paged data set with a db reader - /// - /// - /// - /// - /// The number of rows to load per page - /// - /// - /// Specify a custom Sql command to get the total count, if null is specified than the auto-generated sql count will be used - /// - /// - /// NPoco's normal Page returns a List{T} but sometimes we don't want all that in memory and instead want to - /// iterate over each row with a reader using Query vs Fetch. - /// - public static IEnumerable QueryPaged(this IDatabase database, long pageSize, Sql sql, Sql? sqlCount) + var sqlString = sql.SQL; + var sqlArgs = sql.Arguments; + + int? itemCount = null; + long pageIndex = 0; + do { - var sqlString = sql.SQL; - var sqlArgs = sql.Arguments; + // Get the paged queries + database.BuildPageQueries(pageIndex * pageSize, pageSize, sqlString, ref sqlArgs, + out var generatedSqlCount, out var sqlPage); - int? itemCount = null; - long pageIndex = 0; - do + // get the item count once + if (itemCount == null) { - // Get the paged queries - database.BuildPageQueries(pageIndex * pageSize, pageSize, sqlString, ref sqlArgs, out var generatedSqlCount, out var sqlPage); + itemCount = database.ExecuteScalar(sqlCount?.SQL ?? generatedSqlCount, + sqlCount?.Arguments ?? sqlArgs); + } - // get the item count once - if (itemCount == null) - { - itemCount = database.ExecuteScalar(sqlCount?.SQL ?? generatedSqlCount, sqlCount?.Arguments ?? sqlArgs); - } - pageIndex++; + pageIndex++; - // iterate over rows without allocating all items to memory (Query vs Fetch) - foreach (var row in database.Query(sqlPage, sqlArgs)) - { - yield return row; - } + // iterate over rows without allocating all items to memory (Query vs Fetch) + foreach (T row in database.Query(sqlPage, sqlArgs)) + { + yield return row; + } + } while (pageIndex * pageSize < itemCount); + } - } while ((pageIndex * pageSize) < itemCount); - } + /// + /// Iterates over the result of a paged data set with a db reader + /// + /// + /// + /// + /// The number of rows to load per page + /// + /// + /// + /// + /// NPoco's normal Page returns a List{T} but sometimes we don't want all that in memory and instead want to + /// iterate over each row with a reader using Query vs Fetch. + /// + public static IEnumerable QueryPaged(this IDatabase database, long pageSize, Sql sql) => + database.QueryPaged(pageSize, sql, null); - /// - /// Iterates over the result of a paged data set with a db reader - /// - /// - /// - /// - /// The number of rows to load per page - /// - /// - /// - /// - /// NPoco's normal Page returns a List{T} but sometimes we don't want all that in memory and instead want to - /// iterate over each row with a reader using Query vs Fetch. - /// - public static IEnumerable QueryPaged(this IDatabase database, long pageSize, Sql sql) => database.QueryPaged(pageSize, sql, null); + // NOTE + // + // proper way to do it with TSQL and SQLCE + // IF EXISTS (SELECT ... FROM table WITH (UPDLOCK,HOLDLOCK)) WHERE ...) + // BEGIN + // UPDATE table SET ... WHERE ... + // END + // ELSE + // BEGIN + // INSERT INTO table (...) VALUES (...) + // END + // + // works in READ COMMITED, TSQL & SQLCE lock the constraint even if it does not exist, so INSERT is OK + // + // TODO: use the proper database syntax, not this kludge - // NOTE - // - // proper way to do it with TSQL and SQLCE - // IF EXISTS (SELECT ... FROM table WITH (UPDLOCK,HOLDLOCK)) WHERE ...) - // BEGIN - // UPDATE table SET ... WHERE ... - // END - // ELSE - // BEGIN - // INSERT INTO table (...) VALUES (...) - // END - // - // works in READ COMMITED, TSQL & SQLCE lock the constraint even if it does not exist, so INSERT is OK - // - // TODO: use the proper database syntax, not this kludge + /// + /// Safely inserts a record, or updates if it exists, based on a unique constraint. + /// + /// + /// + /// + /// The action that executed, either an insert or an update. If an insert occurred and a PK value got generated, the + /// poco object + /// passed in will contain the updated value. + /// + /// + /// + /// We cannot rely on database-specific options because SQLCE + /// does not support any of them. Ideally this should be achieved with proper transaction isolation levels but that + /// would mean revisiting + /// isolation levels globally. We want to keep it simple for the time being and manage it manually. + /// + /// We handle it by trying to update, then insert, etc. until something works, or we get bored. + /// + /// Note that with proper transactions, if T2 begins after T1 then we are sure that the database will contain T2's + /// value + /// once T1 and T2 have completed. Whereas here, it could contain T1's value. + /// + /// + public static RecordPersistenceType InsertOrUpdate(this IUmbracoDatabase db, T poco) + where T : class => + db.InsertOrUpdate(poco, null, null); - /// - /// Safely inserts a record, or updates if it exists, based on a unique constraint. - /// - /// - /// - /// The action that executed, either an insert or an update. If an insert occurred and a PK value got generated, the poco object - /// passed in will contain the updated value. - /// - /// We cannot rely on database-specific options because SQLCE - /// does not support any of them. Ideally this should be achieved with proper transaction isolation levels but that would mean revisiting - /// isolation levels globally. We want to keep it simple for the time being and manage it manually. - /// We handle it by trying to update, then insert, etc. until something works, or we get bored. - /// Note that with proper transactions, if T2 begins after T1 then we are sure that the database will contain T2's value - /// once T1 and T2 have completed. Whereas here, it could contain T1's value. - /// - public static RecordPersistenceType InsertOrUpdate(this IUmbracoDatabase db, T poco) - where T : class + /// + /// Safely inserts a record, or updates if it exists, based on a unique constraint. + /// + /// + /// + /// + /// If the entity has a composite key they you need to specify the update command explicitly + /// + /// The action that executed, either an insert or an update. If an insert occurred and a PK value got generated, the + /// poco object + /// passed in will contain the updated value. + /// + /// + /// + /// We cannot rely on database-specific options because SQLCE + /// does not support any of them. Ideally this should be achieved with proper transaction isolation levels but that + /// would mean revisiting + /// isolation levels globally. We want to keep it simple for the time being and manage it manually. + /// + /// We handle it by trying to update, then insert, etc. until something works, or we get bored. + /// + /// Note that with proper transactions, if T2 begins after T1 then we are sure that the database will contain T2's + /// value + /// once T1 and T2 have completed. Whereas here, it could contain T1's value. + /// + /// + public static RecordPersistenceType InsertOrUpdate(this IUmbracoDatabase db, + T poco, + string? updateCommand, + object? updateArgs) + where T : class + { + if (poco == null) { - return db.InsertOrUpdate(poco, null, null); + throw new ArgumentNullException(nameof(poco)); } - /// - /// Safely inserts a record, or updates if it exists, based on a unique constraint. - /// - /// - /// - /// - /// If the entity has a composite key they you need to specify the update command explicitly - /// The action that executed, either an insert or an update. If an insert occurred and a PK value got generated, the poco object - /// passed in will contain the updated value. - /// - /// We cannot rely on database-specific options because SQLCE - /// does not support any of them. Ideally this should be achieved with proper transaction isolation levels but that would mean revisiting - /// isolation levels globally. We want to keep it simple for the time being and manage it manually. - /// We handle it by trying to update, then insert, etc. until something works, or we get bored. - /// Note that with proper transactions, if T2 begins after T1 then we are sure that the database will contain T2's value - /// once T1 and T2 have completed. Whereas here, it could contain T1's value. - /// - public static RecordPersistenceType InsertOrUpdate(this IUmbracoDatabase db, - T poco, - string? updateCommand, - object? updateArgs) - where T : class + // TODO: NPoco has a Save method that works with the primary key + // in any case, no point trying to update if there's no primary key! + + // try to update + var rowCount = updateCommand.IsNullOrWhiteSpace() || updateArgs is null + ? db.Update(poco) + : db.Update(updateCommand!, updateArgs); + if (rowCount > 0) { - if (poco == null) - throw new ArgumentNullException(nameof(poco)); + return RecordPersistenceType.Update; + } - // TODO: NPoco has a Save method that works with the primary key - // in any case, no point trying to update if there's no primary key! + // failed: does not exist, need to insert + // RC1 race cond here: another thread may insert a record with the same constraint - // try to update - var rowCount = updateCommand.IsNullOrWhiteSpace() || updateArgs is null + var i = 0; + while (i++ < 4) + { + try + { + // try to insert + db.Insert(poco); + return RecordPersistenceType.Insert; + } + catch (SqlException) // assuming all db engines will throw that exception + { + // failed: exists (due to race cond RC1) + // RC2 race cond here: another thread may remove the record + + // try to update + rowCount = updateCommand.IsNullOrWhiteSpace() || updateArgs is null ? db.Update(poco) : db.Update(updateCommand!, updateArgs); - if (rowCount > 0) - return RecordPersistenceType.Update; - - // failed: does not exist, need to insert - // RC1 race cond here: another thread may insert a record with the same constraint - - var i = 0; - while (i++ < 4) - { - try + if (rowCount > 0) { - // try to insert - db.Insert(poco); - return RecordPersistenceType.Insert; + return RecordPersistenceType.Update; } - catch (SqlException) // assuming all db engines will throw that exception - { - // failed: exists (due to race cond RC1) - // RC2 race cond here: another thread may remove the record - // try to update - rowCount = updateCommand.IsNullOrWhiteSpace() || updateArgs is null - ? db.Update(poco) - : db.Update(updateCommand!, updateArgs); - if (rowCount > 0) - return RecordPersistenceType.Update; - - // failed: does not exist (due to race cond RC2), need to insert - // loop - } + // failed: does not exist (due to race cond RC2), need to insert + // loop } - - // this can go on forever... have to break at some point and report an error. - throw new DataException("Record could not be inserted or updated."); } - /// - /// This will escape single @ symbols for npoco values so it doesn't think it's a parameter - /// - /// - /// - public static string EscapeAtSymbols(string value) - { - if (value.Contains("@") == false) return value; + // this can go on forever... have to break at some point and report an error. + throw new DataException("Record could not be inserted or updated."); + } - //this fancy regex will only match a single @ not a double, etc... - var regex = new Regex("(? + /// This will escape single @ symbols for npoco values so it doesn't think it's a parameter + /// + /// + /// + public static string EscapeAtSymbols(string value) + { + if (value.Contains("@") == false) + { + return value; } - /// - /// Returns the underlying connection as a typed connection - this is used to unwrap the profiled mini profiler stuff - /// - /// - /// - /// - public static TConnection GetTypedConnection(IDbConnection connection) - where TConnection : class, IDbConnection + //this fancy regex will only match a single @ not a double, etc... + var regex = new Regex("(? + /// Returns the underlying connection as a typed connection - this is used to unwrap the profiled mini profiler stuff + /// + /// + /// + /// + public static TConnection GetTypedConnection(IDbConnection connection) + where TConnection : class, IDbConnection + { + IDbConnection? c = connection; + for (;;) { - var c = connection; - for (;;) + switch (c) { - switch (c) - { - case TConnection ofType: - return ofType; - case RetryDbConnection retry: - c = retry.Inner; - break; - case ProfiledDbConnection profiled: - c = profiled.WrappedConnection; - break; - default: - throw new NotSupportedException(connection.GetType().FullName); - } + case TConnection ofType: + return ofType; + case RetryDbConnection retry: + c = retry.Inner; + break; + case ProfiledDbConnection profiled: + c = profiled.WrappedConnection; + break; + default: + throw new NotSupportedException(connection.GetType().FullName); } } + } - /// - /// Returns the underlying transaction as a typed transaction - this is used to unwrap the profiled mini profiler stuff - /// - /// - /// - /// - public static TTransaction GetTypedTransaction(IDbTransaction? transaction) - where TTransaction : class, IDbTransaction + /// + /// Returns the underlying transaction as a typed transaction - this is used to unwrap the profiled mini profiler stuff + /// + /// + /// + /// + public static TTransaction GetTypedTransaction(IDbTransaction? transaction) + where TTransaction : class, IDbTransaction + { + IDbTransaction? t = transaction; + for (;;) { - var t = transaction; - for (;;) + switch (t) { - switch (t) - { - case TTransaction ofType: - return ofType; - case ProfiledDbTransaction profiled: - t = profiled.WrappedTransaction; - break; - default: - throw new NotSupportedException(transaction?.GetType().FullName); - } + case TTransaction ofType: + return ofType; + case ProfiledDbTransaction profiled: + t = profiled.WrappedTransaction; + break; + default: + throw new NotSupportedException(transaction?.GetType().FullName); } } + } - /// - /// Returns the underlying command as a typed command - this is used to unwrap the profiled mini profiler stuff - /// - /// - /// - /// - public static TCommand GetTypedCommand(IDbCommand command) - where TCommand : class, IDbCommand + /// + /// Returns the underlying command as a typed command - this is used to unwrap the profiled mini profiler stuff + /// + /// + /// + /// + public static TCommand GetTypedCommand(IDbCommand command) + where TCommand : class, IDbCommand + { + IDbCommand? c = command; + for (;;) { - var c = command; - for (;;) + switch (c) { - switch (c) - { - case TCommand ofType: - return ofType; - case FaultHandlingDbCommand faultHandling: - c = faultHandling.Inner; - break; - case ProfiledDbCommand profiled: - c = profiled.InternalCommand; - break; - default: - throw new NotSupportedException(command.GetType().FullName); - } + case TCommand ofType: + return ofType; + case FaultHandlingDbCommand faultHandling: + c = faultHandling.Inner; + break; + case ProfiledDbCommand profiled: + c = profiled.InternalCommand; + break; + default: + throw new NotSupportedException(command.GetType().FullName); } } + } - public static void TruncateTable(this IDatabase db, ISqlSyntaxProvider sqlSyntax, string tableName) - { - var sql = new Sql(string.Format( - sqlSyntax.TruncateTable, - sqlSyntax.GetQuotedTableName(tableName))); - db.Execute(sql); - } - - public static IsolationLevel GetCurrentTransactionIsolationLevel(this IDatabase database) - { - var transaction = database.Transaction; - return transaction?.IsolationLevel ?? IsolationLevel.Unspecified; - } + public static void TruncateTable(this IDatabase db, ISqlSyntaxProvider sqlSyntax, string tableName) + { + var sql = new Sql(string.Format( + sqlSyntax.TruncateTable, + sqlSyntax.GetQuotedTableName(tableName))); + db.Execute(sql); + } - public static IEnumerable FetchByGroups(this IDatabase db, IEnumerable source, int groupSize, Func, Sql> sqlFactory) - { - return source.SelectByGroups(x => db.Fetch(sqlFactory(x)), groupSize); - } + public static IsolationLevel GetCurrentTransactionIsolationLevel(this IDatabase database) + { + DbTransaction? transaction = database.Transaction; + return transaction?.IsolationLevel ?? IsolationLevel.Unspecified; } + + public static IEnumerable FetchByGroups(this IDatabase db, IEnumerable source, + int groupSize, Func, Sql> sqlFactory) => + source.SelectByGroups(x => db.Fetch(sqlFactory(x)), groupSize); } diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseTypeExtensions.cs b/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseTypeExtensions.cs index b349824591d1..2b0cb199afe9 100644 --- a/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseTypeExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseTypeExtensions.cs @@ -1,19 +1,18 @@ -using System; using NPoco; +using NPoco.DatabaseTypes; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +internal static class NPocoDatabaseTypeExtensions { - internal static class NPocoDatabaseTypeExtensions - { - [Obsolete("Usage of this method indicates a code smell.")] - public static bool IsSqlServer(this DatabaseType databaseType) => - // note that because SqlServerDatabaseType is the base class for - // all Sql Server types eg SqlServer2012DatabaseType, this will - // test *any* version of Sql Server. - databaseType is NPoco.DatabaseTypes.SqlServerDatabaseType; + [Obsolete("Usage of this method indicates a code smell.")] + public static bool IsSqlServer(this DatabaseType databaseType) => + // note that because SqlServerDatabaseType is the base class for + // all Sql Server types eg SqlServer2012DatabaseType, this will + // test *any* version of Sql Server. + databaseType is SqlServerDatabaseType; - [Obsolete("Usage of this method indicates a code smell.")] - public static bool IsSqlite(this DatabaseType databaseType) - => databaseType is NPoco.DatabaseTypes.SQLiteDatabaseType; - } + [Obsolete("Usage of this method indicates a code smell.")] + public static bool IsSqlite(this DatabaseType databaseType) + => databaseType is SQLiteDatabaseType; } diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoMapperCollection.cs b/src/Umbraco.Infrastructure/Persistence/NPocoMapperCollection.cs index 3d71c0225e41..f19d3d274442 100644 --- a/src/Umbraco.Infrastructure/Persistence/NPocoMapperCollection.cs +++ b/src/Umbraco.Infrastructure/Persistence/NPocoMapperCollection.cs @@ -1,14 +1,11 @@ -using System; -using System.Collections.Generic; using NPoco; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +public sealed class NPocoMapperCollection : BuilderCollectionBase { - public sealed class NPocoMapperCollection : BuilderCollectionBase + public NPocoMapperCollection(Func> items) : base(items) { - public NPocoMapperCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoMapperCollectionBuilder.cs b/src/Umbraco.Infrastructure/Persistence/NPocoMapperCollectionBuilder.cs index 4840ceafe89c..71d8e6b7b1d7 100644 --- a/src/Umbraco.Infrastructure/Persistence/NPocoMapperCollectionBuilder.cs +++ b/src/Umbraco.Infrastructure/Persistence/NPocoMapperCollectionBuilder.cs @@ -1,10 +1,11 @@ using NPoco; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +public sealed class + NPocoMapperCollectionBuilder : SetCollectionBuilderBase { - public sealed class NPocoMapperCollectionBuilder : SetCollectionBuilderBase - { - protected override NPocoMapperCollectionBuilder This => this; - } + protected override NPocoMapperCollectionBuilder This => this; } diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs b/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs index 5a0088c72787..4cdb7207c2b3 100644 --- a/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs @@ -1,7 +1,4 @@ -using System; using System.Collections; -using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Text; @@ -10,1203 +7,1346 @@ using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Querying; +using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class NPocoSqlExtensions { - public static partial class NPocoSqlExtensions + #region From + + /// + /// Appends a FROM clause to the Sql statement. + /// + /// The type of the Dto. + /// The Sql statement. + /// An optional table alias + /// The Sql statement. + public static Sql From(this Sql sql, string? alias = null) { - #region Where + Type type = typeof(TDto); + var tableName = type.GetTableName(); - /// - /// Appends a WHERE clause to the Sql statement. - /// - /// The type of the Dto. - /// The Sql statement. - /// A predicate to transform and append to the Sql statement. - /// An optional alias for the table. - /// The Sql statement. - public static Sql Where(this Sql sql, Expression> predicate, string? alias = null) + var from = sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName); + if (!string.IsNullOrWhiteSpace(alias)) { - var (s, a) = sql.SqlContext.VisitDto(predicate, alias); - return sql.Where(s, a); + from += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias); } - /// - /// Appends a WHERE clause to the Sql statement. - /// - /// The type of Dto 1. - /// The type of Dto 2. - /// The Sql statement. - /// A predicate to transform and append to the Sql statement. - /// An optional alias for Dto 1 table. - /// An optional alias for Dto 2 table. - /// The Sql statement. - public static Sql Where(this Sql sql, Expression> predicate, string? alias1 = null, string? alias2 = null) - { - var (s, a) = sql.SqlContext.VisitDto(predicate, alias1, alias2); - return sql.Where(s, a); - } + sql.From(from); - /// - /// Appends a WHERE IN clause to the Sql statement. - /// - /// The type of the Dto. - /// The Sql statement. - /// An expression specifying the field. - /// The values. - /// The Sql statement. - public static Sql WhereIn(this Sql sql, Expression> field, IEnumerable? values) - { - var fieldName = sql.SqlContext.SqlSyntax.GetFieldName(field); - sql.Where(fieldName + " IN (@values)", new { values }); - return sql; - } + return sql; + } - /// - /// Appends a WHERE IN clause to the Sql statement. - /// - /// The type of the Dto. - /// The Sql statement. - /// An expression specifying the field. - /// A subquery returning the value. - /// The Sql statement. - public static Sql WhereIn(this Sql sql, Expression> field, Sql? values) - { - return WhereIn(sql, field, values, false, null); - } + #endregion - public static Sql WhereIn(this Sql sql, Expression> field, Sql? values, string tableAlias) - { - return sql.WhereIn(field, values, false, tableAlias); - } + #region Aliasing + internal static string GetAliasedField(this Sql sql, string field) + { + // get alias, if aliased + // + // regex looks for pattern "([\w+].[\w+]) AS ([\w+])" ie "(field) AS (alias)" + // and, if found & a group's field matches the field name, returns the alias + // + // so... if query contains "[umbracoNode].[nodeId] AS [umbracoNode__nodeId]" + // then GetAliased for "[umbracoNode].[nodeId]" returns "[umbracoNode__nodeId]" + + MatchCollection matches = sql.SqlContext.SqlSyntax.AliasRegex.Matches(sql.SQL); + Match? match = matches.FirstOrDefault(m => m.Groups[1].Value.InvariantEquals(field)); + return match == null ? field : match.Groups[2].Value; + } - public static Sql WhereLike(this Sql sql, Expression> fieldSelector, Sql? valuesSql) - { - var fieldName = sql.SqlContext.SqlSyntax.GetFieldName(fieldSelector); - sql.Where(fieldName + " LIKE (" + valuesSql?.SQL + ")", valuesSql?.Arguments); - return sql; - } + #endregion - public static Sql WhereLike(this Sql sql, Expression> fieldSelector, string likeValue) - { - var fieldName = sql.SqlContext.SqlSyntax.GetFieldName(fieldSelector); - sql.Where(fieldName + " LIKE ('" + likeValue + "')"); - return sql; - } + #region Where - /// - /// Appends a WHERE NOT IN clause to the Sql statement. - /// - /// The type of the Dto. - /// The Sql statement. - /// An expression specifying the field. - /// The values. - /// The Sql statement. - public static Sql WhereNotIn(this Sql sql, Expression> field, IEnumerable values) - { - var fieldName = sql.SqlContext.SqlSyntax.GetFieldName(field); - sql.Where(fieldName + " NOT IN (@values)", new { values }); - return sql; - } + /// + /// Appends a WHERE clause to the Sql statement. + /// + /// The type of the Dto. + /// The Sql statement. + /// A predicate to transform and append to the Sql statement. + /// An optional alias for the table. + /// The Sql statement. + public static Sql Where(this Sql sql, Expression> predicate, + string? alias = null) + { + var (s, a) = sql.SqlContext.VisitDto(predicate, alias); + return sql.Where(s, a); + } - /// - /// Appends a WHERE NOT IN clause to the Sql statement. - /// - /// The type of the Dto. - /// The Sql statement. - /// An expression specifying the field. - /// A subquery returning the value. - /// The Sql statement. - public static Sql WhereNotIn(this Sql sql, Expression> field, Sql values) - { - return sql.WhereIn(field, values, true); - } + /// + /// Appends a WHERE clause to the Sql statement. + /// + /// The type of Dto 1. + /// The type of Dto 2. + /// The Sql statement. + /// A predicate to transform and append to the Sql statement. + /// An optional alias for Dto 1 table. + /// An optional alias for Dto 2 table. + /// The Sql statement. + public static Sql Where(this Sql sql, + Expression> predicate, string? alias1 = null, string? alias2 = null) + { + var (s, a) = sql.SqlContext.VisitDto(predicate, alias1, alias2); + return sql.Where(s, a); + } - /// - /// Appends multiple OR WHERE IN clauses to the Sql statement. - /// - /// The type of the Dto. - /// The Sql statement. - /// Expressions specifying the fields. - /// The values. - /// The Sql statement. - public static Sql WhereAnyIn(this Sql sql, Expression>[] fields, IEnumerable values) + /// + /// Appends a WHERE IN clause to the Sql statement. + /// + /// The type of the Dto. + /// The Sql statement. + /// An expression specifying the field. + /// The values. + /// The Sql statement. + public static Sql WhereIn(this Sql sql, Expression> field, + IEnumerable? values) + { + var fieldName = sql.SqlContext.SqlSyntax.GetFieldName(field); + sql.Where(fieldName + " IN (@values)", new {values}); + return sql; + } + + /// + /// Appends a WHERE IN clause to the Sql statement. + /// + /// The type of the Dto. + /// The Sql statement. + /// An expression specifying the field. + /// A subquery returning the value. + /// The Sql statement. + public static Sql WhereIn(this Sql sql, Expression> field, + Sql? values) => WhereIn(sql, field, values, false, null); + + public static Sql WhereIn(this Sql sql, Expression> field, + Sql? values, string tableAlias) => sql.WhereIn(field, values, false, tableAlias); + + + public static Sql WhereLike(this Sql sql, + Expression> fieldSelector, Sql? valuesSql) + { + var fieldName = sql.SqlContext.SqlSyntax.GetFieldName(fieldSelector); + sql.Where(fieldName + " LIKE (" + valuesSql?.SQL + ")", valuesSql?.Arguments); + return sql; + } + + public static Sql WhereLike(this Sql sql, + Expression> fieldSelector, string likeValue) + { + var fieldName = sql.SqlContext.SqlSyntax.GetFieldName(fieldSelector); + sql.Where(fieldName + " LIKE ('" + likeValue + "')"); + return sql; + } + + /// + /// Appends a WHERE NOT IN clause to the Sql statement. + /// + /// The type of the Dto. + /// The Sql statement. + /// An expression specifying the field. + /// The values. + /// The Sql statement. + public static Sql WhereNotIn(this Sql sql, Expression> field, + IEnumerable values) + { + var fieldName = sql.SqlContext.SqlSyntax.GetFieldName(field); + sql.Where(fieldName + " NOT IN (@values)", new {values}); + return sql; + } + + /// + /// Appends a WHERE NOT IN clause to the Sql statement. + /// + /// The type of the Dto. + /// The Sql statement. + /// An expression specifying the field. + /// A subquery returning the value. + /// The Sql statement. + public static Sql WhereNotIn(this Sql sql, Expression> field, + Sql values) => sql.WhereIn(field, values, true); + + /// + /// Appends multiple OR WHERE IN clauses to the Sql statement. + /// + /// The type of the Dto. + /// The Sql statement. + /// Expressions specifying the fields. + /// The values. + /// The Sql statement. + public static Sql WhereAnyIn(this Sql sql, Expression>[] fields, + IEnumerable values) + { + ISqlSyntaxProvider sqlSyntax = sql.SqlContext.SqlSyntax; + var fieldNames = fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); + var sb = new StringBuilder(); + sb.Append("("); + for (var i = 0; i < fieldNames.Length; i++) { - var sqlSyntax = sql.SqlContext.SqlSyntax; - var fieldNames = fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); - var sb = new StringBuilder(); - sb.Append("("); - for (var i = 0; i < fieldNames.Length; i++) + if (i > 0) { - if (i > 0) sb.Append(" OR "); - sb.Append(fieldNames[i]); - sql.Append(" IN (@values)"); + sb.Append(" OR "); } - sb.Append(")"); - sql.Where(sb.ToString(), new { values }); - return sql; - } - private static Sql WhereIn(this Sql sql, Expression> fieldSelector, Sql? valuesSql, bool not) - { - return WhereIn(sql, fieldSelector, valuesSql, not, null); + sb.Append(fieldNames[i]); + sql.Append(" IN (@values)"); } - private static Sql WhereIn(this Sql sql, Expression> fieldSelector, Sql? valuesSql, bool not, string? tableAlias) - { - var fieldName = sql.SqlContext.SqlSyntax.GetFieldName(fieldSelector, tableAlias); - sql.Where(fieldName + (not ? " NOT" : "") +" IN (" + valuesSql?.SQL + ")", valuesSql?.Arguments); - return sql; - } + sb.Append(")"); + sql.Where(sb.ToString(), new {values}); + return sql; + } - /// - /// Appends multiple OR WHERE clauses to the Sql statement. - /// - /// The Sql statement. - /// The WHERE predicates. - /// The Sql statement. - public static Sql WhereAny(this Sql sql, params Func, Sql>[] predicates) - { - var wsql = new Sql(sql.SqlContext); + private static Sql WhereIn(this Sql sql, Expression> fieldSelector, + Sql? valuesSql, bool not) => WhereIn(sql, fieldSelector, valuesSql, not, null); + + private static Sql WhereIn(this Sql sql, Expression> fieldSelector, + Sql? valuesSql, bool not, string? tableAlias) + { + var fieldName = sql.SqlContext.SqlSyntax.GetFieldName(fieldSelector, tableAlias); + sql.Where(fieldName + (not ? " NOT" : "") + " IN (" + valuesSql?.SQL + ")", valuesSql?.Arguments); + return sql; + } + + /// + /// Appends multiple OR WHERE clauses to the Sql statement. + /// + /// The Sql statement. + /// The WHERE predicates. + /// The Sql statement. + public static Sql WhereAny(this Sql sql, + params Func, Sql>[] predicates) + { + var wsql = new Sql(sql.SqlContext); - wsql.Append("("); - for (var i = 0; i < predicates.Length; i++) + wsql.Append("("); + for (var i = 0; i < predicates.Length; i++) + { + if (i > 0) { - if (i > 0) - wsql.Append(") OR ("); - var temp = new Sql(sql.SqlContext); - temp = predicates[i](temp); - wsql.Append(temp.SQL.TrimStart("WHERE "), temp.Arguments); + wsql.Append(") OR ("); } - wsql.Append(")"); - return sql.Where(wsql.SQL, wsql.Arguments); + var temp = new Sql(sql.SqlContext); + temp = predicates[i](temp); + wsql.Append(temp.SQL.TrimStart("WHERE "), temp.Arguments); } - /// - /// Appends a WHERE NOT NULL clause to the Sql statement. - /// - /// The type of the Dto. - /// The Sql statement. - /// Expression specifying the field. - /// An optional alias for the table. - /// The Sql statement. - public static Sql WhereNotNull(this Sql sql, Expression> field, string? tableAlias = null) - { - return sql.WhereNull(field, tableAlias, true); - } + wsql.Append(")"); - /// - /// Appends a WHERE [NOT] NULL clause to the Sql statement. - /// - /// The type of the Dto. - /// The Sql statement. - /// Expression specifying the field. - /// An optional alias for the table. - /// A value indicating whether to NOT NULL. - /// The Sql statement. - public static Sql WhereNull(this Sql sql, Expression> field, string? tableAlias = null, bool not = false) - { - var column = sql.GetColumns(columnExpressions: new[] { field }, tableAlias: tableAlias, withAlias: false).First(); - return sql.Where("(" + column + " IS " + (not ? "NOT " : "") + "NULL)"); - } + return sql.Where(wsql.SQL, wsql.Arguments); + } - #endregion + /// + /// Appends a WHERE NOT NULL clause to the Sql statement. + /// + /// The type of the Dto. + /// The Sql statement. + /// Expression specifying the field. + /// An optional alias for the table. + /// The Sql statement. + public static Sql WhereNotNull(this Sql sql, Expression> field, + string? tableAlias = null) => sql.WhereNull(field, tableAlias, true); + + /// + /// Appends a WHERE [NOT] NULL clause to the Sql statement. + /// + /// The type of the Dto. + /// The Sql statement. + /// Expression specifying the field. + /// An optional alias for the table. + /// A value indicating whether to NOT NULL. + /// The Sql statement. + public static Sql WhereNull(this Sql sql, Expression> field, + string? tableAlias = null, bool not = false) + { + var column = sql.GetColumns(columnExpressions: new[] {field}, tableAlias: tableAlias, withAlias: false).First(); + return sql.Where("(" + column + " IS " + (not ? "NOT " : "") + "NULL)"); + } - #region From + #endregion + + #region OrderBy, GroupBy + + /// + /// Appends an ORDER BY clause to the Sql statement. + /// + /// The type of the Dto. + /// The Sql statement. + /// An expression specifying the field. + /// The Sql statement. + public static Sql OrderBy(this Sql sql, Expression> field) => + sql.OrderBy("(" + sql.SqlContext.SqlSyntax.GetFieldName(field) + ")"); + + /// + /// Appends an ORDER BY clause to the Sql statement. + /// + /// The type of the Dto. + /// The Sql statement. + /// Expression specifying the fields. + /// The Sql statement. + public static Sql OrderBy(this Sql sql, + params Expression>[] fields) + { + ISqlSyntaxProvider sqlSyntax = sql.SqlContext.SqlSyntax; + var columns = fields.Length == 0 + ? sql.GetColumns(withAlias: false) + : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); + return sql.OrderBy(columns); + } - /// - /// Appends a FROM clause to the Sql statement. - /// - /// The type of the Dto. - /// The Sql statement. - /// An optional table alias - /// The Sql statement. - public static Sql From(this Sql sql, string? alias = null) - { - var type = typeof (TDto); - var tableName = type.GetTableName(); + /// + /// Appends an ORDER BY DESC clause to the Sql statement. + /// + /// The type of the Dto. + /// The Sql statement. + /// An expression specifying the field. + /// The Sql statement. + public static Sql OrderByDescending(this Sql sql, + Expression> field) => sql.OrderByDescending(sql.SqlContext.SqlSyntax.GetFieldName(field)); + + /// + /// Appends an ORDER BY DESC clause to the Sql statement. + /// + /// The type of the Dto. + /// The Sql statement. + /// Expression specifying the fields. + /// The Sql statement. + public static Sql OrderByDescending(this Sql sql, + params Expression>[] fields) + { + ISqlSyntaxProvider sqlSyntax = sql.SqlContext.SqlSyntax; + var columns = fields.Length == 0 + ? sql.GetColumns(withAlias: false) + : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); + return sql.OrderByDescending(columns); + } - var from = sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName); - if (!string.IsNullOrWhiteSpace(alias)) - from += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias); - sql.From(from); + /// + /// Appends an ORDER BY DESC clause to the Sql statement. + /// + /// The Sql statement. + /// Fields. + /// The Sql statement. + public static Sql OrderByDescending(this Sql sql, params string?[] fields) => + sql.Append("ORDER BY " + string.Join(", ", fields.Select(x => x + " DESC"))); + + /// + /// Appends a GROUP BY clause to the Sql statement. + /// + /// The type of the Dto. + /// The Sql statement. + /// An expression specifying the field. + /// The Sql statement. + public static Sql GroupBy(this Sql sql, Expression> field) => + sql.GroupBy(sql.SqlContext.SqlSyntax.GetFieldName(field)); + + /// + /// Appends a GROUP BY clause to the Sql statement. + /// + /// The type of the Dto. + /// The Sql statement. + /// Expression specifying the fields. + /// The Sql statement. + public static Sql GroupBy(this Sql sql, + params Expression>[] fields) + { + ISqlSyntaxProvider sqlSyntax = sql.SqlContext.SqlSyntax; + var columns = fields.Length == 0 + ? sql.GetColumns(withAlias: false) + : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); + return sql.GroupBy(columns); + } - return sql; - } + /// + /// Appends more ORDER BY or GROUP BY fields to the Sql statement. + /// + /// The type of the Dto. + /// The Sql statement. + /// Expressions specifying the fields. + /// The Sql statement. + public static Sql AndBy(this Sql sql, + params Expression>[] fields) + { + ISqlSyntaxProvider sqlSyntax = sql.SqlContext.SqlSyntax; + var columns = fields.Length == 0 + ? sql.GetColumns(withAlias: false) + : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); + return sql.Append(", " + string.Join(", ", columns)); + } - #endregion + /// + /// Appends more ORDER BY DESC fields to the Sql statement. + /// + /// The type of the Dto. + /// The Sql statement. + /// Expressions specifying the fields. + /// The Sql statement. + public static Sql AndByDescending(this Sql sql, + params Expression>[] fields) + { + ISqlSyntaxProvider sqlSyntax = sql.SqlContext.SqlSyntax; + var columns = fields.Length == 0 + ? sql.GetColumns(withAlias: false) + : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); + return sql.Append(", " + string.Join(", ", columns.Select(x => x + " DESC"))); + } - #region OrderBy, GroupBy + #endregion - /// - /// Appends an ORDER BY clause to the Sql statement. - /// - /// The type of the Dto. - /// The Sql statement. - /// An expression specifying the field. - /// The Sql statement. - public static Sql OrderBy(this Sql sql, Expression> field) - { - return sql.OrderBy("(" + sql.SqlContext.SqlSyntax.GetFieldName(field) + ")"); - } + #region Joins - /// - /// Appends an ORDER BY clause to the Sql statement. - /// - /// The type of the Dto. - /// The Sql statement. - /// Expression specifying the fields. - /// The Sql statement. - public static Sql OrderBy(this Sql sql, params Expression>[] fields) + /// + /// Appends a CROSS JOIN clause to the Sql statement. + /// + /// The type of the Dto. + /// The Sql statement. + /// An optional alias for the joined table. + /// The Sql statement. + public static Sql CrossJoin(this Sql sql, string? alias = null) + { + Type type = typeof(TDto); + var tableName = type.GetTableName(); + var join = sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName); + if (alias != null) { - var sqlSyntax = sql.SqlContext.SqlSyntax; - var columns = fields.Length == 0 - ? sql.GetColumns(withAlias: false) - : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); - return sql.OrderBy(columns); + join += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias); } - /// - /// Appends an ORDER BY DESC clause to the Sql statement. - /// - /// The type of the Dto. - /// The Sql statement. - /// An expression specifying the field. - /// The Sql statement. - public static Sql OrderByDescending(this Sql sql, Expression> field) - { - return sql.OrderByDescending(sql.SqlContext.SqlSyntax.GetFieldName(field)); - } + return sql.Append("CROSS JOIN " + join); + } - /// - /// Appends an ORDER BY DESC clause to the Sql statement. - /// - /// The type of the Dto. - /// The Sql statement. - /// Expression specifying the fields. - /// The Sql statement. - public static Sql OrderByDescending(this Sql sql, params Expression>[] fields) + /// + /// Appends an INNER JOIN clause to the Sql statement. + /// + /// The type of the Dto. + /// The Sql statement. + /// An optional alias for the joined table. + /// A SqlJoin statement. + public static Sql.SqlJoinClause InnerJoin(this Sql sql, + string? alias = null) + { + Type type = typeof(TDto); + var tableName = type.GetTableName(); + var join = sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName); + if (alias != null) { - var sqlSyntax = sql.SqlContext.SqlSyntax; - var columns = fields.Length == 0 - ? sql.GetColumns(withAlias: false) - : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); - return sql.OrderByDescending(columns); + join += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias); } - /// - /// Appends an ORDER BY DESC clause to the Sql statement. - /// - /// The Sql statement. - /// Fields. - /// The Sql statement. - public static Sql OrderByDescending(this Sql sql, params string?[] fields) - { - return sql.Append("ORDER BY " + string.Join(", ", fields.Select(x => x + " DESC"))); - } + return sql.InnerJoin(join); + } - /// - /// Appends a GROUP BY clause to the Sql statement. - /// - /// The type of the Dto. - /// The Sql statement. - /// An expression specifying the field. - /// The Sql statement. - public static Sql GroupBy(this Sql sql, Expression> field) + /// + /// Appends an INNER JOIN clause using a nested query. + /// + /// The SQL statement. + /// The nested sql query. + /// An optional alias for the joined table. + /// A SqlJoin statement. + public static Sql.SqlJoinClause InnerJoin(this Sql sql, + Sql nestedSelect, string? alias = null) + { + var join = $"({nestedSelect.SQL})"; + if (alias is not null) { - return sql.GroupBy(sql.SqlContext.SqlSyntax.GetFieldName(field)); + join += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias); } - /// - /// Appends a GROUP BY clause to the Sql statement. - /// - /// The type of the Dto. - /// The Sql statement. - /// Expression specifying the fields. - /// The Sql statement. - public static Sql GroupBy(this Sql sql, params Expression>[] fields) + return sql.InnerJoin(join); + } + + /// + /// Appends a LEFT JOIN clause to the Sql statement. + /// + /// The type of the Dto. + /// The Sql statement. + /// An optional alias for the joined table. + /// A SqlJoin statement. + public static Sql.SqlJoinClause LeftJoin(this Sql sql, + string? alias = null) + { + Type type = typeof(TDto); + var tableName = type.GetTableName(); + var join = sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName); + if (alias != null) { - var sqlSyntax = sql.SqlContext.SqlSyntax; - var columns = fields.Length == 0 - ? sql.GetColumns(withAlias: false) - : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); - return sql.GroupBy(columns); + join += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias); } - /// - /// Appends more ORDER BY or GROUP BY fields to the Sql statement. - /// - /// The type of the Dto. - /// The Sql statement. - /// Expressions specifying the fields. - /// The Sql statement. - public static Sql AndBy(this Sql sql, params Expression>[] fields) + return sql.LeftJoin(join); + } + + /// + /// Appends a LEFT JOIN clause to the Sql statement. + /// + /// The type of the Dto. + /// The Sql statement. + /// A nested join statement. + /// An optional alias for the joined table. + /// A SqlJoin statement. + /// Nested statement produces LEFT JOIN xxx JOIN yyy ON ... ON ... + public static Sql.SqlJoinClause LeftJoin( + this Sql sql, + Func, Sql> nestedJoin, + string? alias = null) => + sql.SqlContext.SqlSyntax.LeftJoinWithNestedJoin(sql, nestedJoin, alias); + + /// + /// Appends an LEFT JOIN clause using a nested query. + /// + /// The SQL statement. + /// The nested sql query. + /// An optional alias for the joined table. + /// A SqlJoin statement. + public static Sql.SqlJoinClause LeftJoin(this Sql sql, + Sql nestedSelect, string? alias = null) + { + var join = $"({nestedSelect.SQL})"; + if (alias is not null) { - var sqlSyntax = sql.SqlContext.SqlSyntax; - var columns = fields.Length == 0 - ? sql.GetColumns(withAlias: false) - : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); - return sql.Append(", " + string.Join(", ", columns)); + join += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias); } - /// - /// Appends more ORDER BY DESC fields to the Sql statement. - /// - /// The type of the Dto. - /// The Sql statement. - /// Expressions specifying the fields. - /// The Sql statement. - public static Sql AndByDescending(this Sql sql, params Expression>[] fields) + return sql.LeftJoin(join); + } + + /// + /// Appends a RIGHT JOIN clause to the Sql statement. + /// + /// The type of the Dto. + /// The Sql statement. + /// An optional alias for the joined table. + /// A SqlJoin statement. + public static Sql.SqlJoinClause RightJoin(this Sql sql, + string? alias = null) + { + Type type = typeof(TDto); + var tableName = type.GetTableName(); + var join = sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName); + if (alias != null) { - var sqlSyntax = sql.SqlContext.SqlSyntax; - var columns = fields.Length == 0 - ? sql.GetColumns(withAlias: false) - : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); - return sql.Append(", " + string.Join(", ", columns.Select(x => x + " DESC"))); + join += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias); } - #endregion + return sql.RightJoin(join); + } - #region Joins + /// + /// Appends an ON clause to a SqlJoin statement. + /// + /// The type of the left Dto. + /// The type of the right Dto. + /// The Sql join statement. + /// An expression specifying the left field. + /// An expression specifying the right field. + /// The Sql statement. + public static Sql On(this Sql.SqlJoinClause sqlJoin, + Expression> leftField, Expression> rightField) + { + // TODO: ugly - should define on SqlContext! - /// - /// Appends a CROSS JOIN clause to the Sql statement. - /// - /// The type of the Dto. - /// The Sql statement. - /// An optional alias for the joined table. - /// The Sql statement. - public static Sql CrossJoin(this Sql sql, string? alias = null) - { - var type = typeof(TDto); - var tableName = type.GetTableName(); - var join = sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName); - if (alias != null) join += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias); + var xLeft = new Sql(sqlJoin.SqlContext).Columns(leftField); + var xRight = new Sql(sqlJoin.SqlContext).Columns(rightField); + return sqlJoin.On(xLeft + " = " + xRight); - return sql.Append("CROSS JOIN " + join); - } + //var sqlSyntax = clause.SqlContext.SqlSyntax; - /// - /// Appends an INNER JOIN clause to the Sql statement. - /// - /// The type of the Dto. - /// The Sql statement. - /// An optional alias for the joined table. - /// A SqlJoin statement. - public static Sql.SqlJoinClause InnerJoin(this Sql sql, string? alias = null) - { - var type = typeof(TDto); - var tableName = type.GetTableName(); - var join = sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName); - if (alias != null) join += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias); + //var leftType = typeof (TLeft); + //var rightType = typeof (TRight); + //var leftTableName = leftType.GetTableName(); + //var rightTableName = rightType.GetTableName(); - return sql.InnerJoin(join); - } + //var leftColumn = ExpressionHelper.FindProperty(leftMember) as PropertyInfo; + //var rightColumn = ExpressionHelper.FindProperty(rightMember) as PropertyInfo; - /// - /// Appends an INNER JOIN clause using a nested query. - /// - /// The SQL statement. - /// The nested sql query. - /// An optional alias for the joined table. - /// A SqlJoin statement. - public static Sql.SqlJoinClause InnerJoin(this Sql sql, Sql nestedSelect, string? alias = null) - { - var join = $"({nestedSelect.SQL})"; - if (alias is not null) - { - join += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias); - } + //var leftColumnName = leftColumn.GetColumnName(); + //var rightColumnName = rightColumn.GetColumnName(); - return sql.InnerJoin(join); - } + //string onClause = $"{sqlSyntax.GetQuotedTableName(leftTableName)}.{sqlSyntax.GetQuotedColumnName(leftColumnName)} = {sqlSyntax.GetQuotedTableName(rightTableName)}.{sqlSyntax.GetQuotedColumnName(rightColumnName)}"; + //return clause.On(onClause); + } - /// - /// Appends a LEFT JOIN clause to the Sql statement. - /// - /// The type of the Dto. - /// The Sql statement. - /// An optional alias for the joined table. - /// A SqlJoin statement. - public static Sql.SqlJoinClause LeftJoin(this Sql sql, string? alias = null) - { - var type = typeof(TDto); - var tableName = type.GetTableName(); - var join = sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName); - if (alias != null) join += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias); + /// + /// Appends an ON clause to a SqlJoin statement. + /// + /// The Sql join statement. + /// A Sql fragment to use as the ON clause body. + /// The Sql statement. + public static Sql On(this Sql.SqlJoinClause sqlJoin, + Func, Sql> on) + { + var sql = new Sql(sqlJoin.SqlContext); + sql = on(sql); + var text = sql.SQL.Trim().TrimStart("WHERE").Trim(); + return sqlJoin.On(text, sql.Arguments); + } - return sql.LeftJoin(join); - } + /// + /// Appends an ON clause to a SqlJoin statement. + /// + /// The type of Dto 1. + /// The type of Dto 2. + /// The SqlJoin statement. + /// A predicate to transform and use as the ON clause body. + /// An optional alias for Dto 1 table. + /// An optional alias for Dto 2 table. + /// The Sql statement. + public static Sql On(this Sql.SqlJoinClause sqlJoin, + Expression> predicate, string? aliasLeft = null, string? aliasRight = null) + { + var expresionist = new PocoToSqlExpressionVisitor(sqlJoin.SqlContext, aliasLeft, aliasRight); + var onExpression = expresionist.Visit(predicate); + return sqlJoin.On(onExpression, expresionist.GetSqlParameters()); + } - /// - /// Appends a LEFT JOIN clause to the Sql statement. - /// - /// The type of the Dto. - /// The Sql statement. - /// A nested join statement. - /// An optional alias for the joined table. - /// A SqlJoin statement. - /// Nested statement produces LEFT JOIN xxx JOIN yyy ON ... ON ... - public static Sql.SqlJoinClause LeftJoin( - this Sql sql, - Func, Sql> nestedJoin, - string? alias = null) => - sql.SqlContext.SqlSyntax.LeftJoinWithNestedJoin(sql, nestedJoin, alias); + /// + /// Appends an ON clause to a SqlJoin statement. + /// + /// The type of Dto 1. + /// The type of Dto 2. + /// The type of Dto 3. + /// The SqlJoin statement. + /// A predicate to transform and use as the ON clause body. + /// An optional alias for Dto 1 table. + /// An optional alias for Dto 2 table. + /// An optional alias for Dto 3 table. + /// The Sql statement. + public static Sql On(this Sql.SqlJoinClause sqlJoin, + Expression> predicate, string? aliasLeft = null, string? aliasRight = null, + string? aliasOther = null) + { + var expresionist = + new PocoToSqlExpressionVisitor(sqlJoin.SqlContext, aliasLeft, aliasRight, aliasOther); + var onExpression = expresionist.Visit(predicate); + return sqlJoin.On(onExpression, expresionist.GetSqlParameters()); + } - /// - /// Appends an LEFT JOIN clause using a nested query. - /// - /// The SQL statement. - /// The nested sql query. - /// An optional alias for the joined table. - /// A SqlJoin statement. - public static Sql.SqlJoinClause LeftJoin(this Sql sql, Sql nestedSelect, string? alias = null) - { - var join = $"({nestedSelect.SQL})"; - if (alias is not null) - { - join += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias); - } + #endregion - return sql.LeftJoin(join); - } + #region Select - /// - /// Appends a RIGHT JOIN clause to the Sql statement. - /// - /// The type of the Dto. - /// The Sql statement. - /// An optional alias for the joined table. - /// A SqlJoin statement. - public static Sql.SqlJoinClause RightJoin(this Sql sql, string? alias = null) + /// + /// Alters a Sql statement to return a maximum amount of rows. + /// + /// The Sql statement. + /// The maximum number of rows to return. + /// The Sql statement. + public static Sql SelectTop(this Sql sql, int count) + { + if (sql == null) { - var type = typeof(TDto); - var tableName = type.GetTableName(); - var join = sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName); - if (alias != null) join += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias); - - return sql.RightJoin(join); + throw new ArgumentNullException(nameof(sql)); } - /// - /// Appends an ON clause to a SqlJoin statement. - /// - /// The type of the left Dto. - /// The type of the right Dto. - /// The Sql join statement. - /// An expression specifying the left field. - /// An expression specifying the right field. - /// The Sql statement. - public static Sql On(this Sql.SqlJoinClause sqlJoin, - Expression> leftField, Expression> rightField) + return sql.SqlContext.SqlSyntax.SelectTop(sql, count); + } + + /// + /// Creates a SELECT COUNT(*) Sql statement. + /// + /// The origin sql. + /// An optional alias. + /// The Sql statement. + public static Sql SelectCount(this Sql sql, string? alias = null) + { + if (sql == null) { - // TODO: ugly - should define on SqlContext! + throw new ArgumentNullException(nameof(sql)); + } - var xLeft = new Sql(sqlJoin.SqlContext).Columns(leftField); - var xRight = new Sql(sqlJoin.SqlContext).Columns(rightField); - return sqlJoin.On(xLeft + " = " + xRight); + var text = "COUNT(*)"; + if (alias != null) + { + text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias); + } - //var sqlSyntax = clause.SqlContext.SqlSyntax; + return sql.Select(text); + } - //var leftType = typeof (TLeft); - //var rightType = typeof (TRight); - //var leftTableName = leftType.GetTableName(); - //var rightTableName = rightType.GetTableName(); + /// + /// Creates a SELECT COUNT Sql statement. + /// + /// The type of the DTO to count. + /// The origin sql. + /// Expressions indicating the columns to count. + /// The Sql statement. + /// + /// If is empty, all columns are counted. + /// + public static Sql SelectCount(this Sql sql, + params Expression>[] fields) + => sql.SelectCount(null, fields); + + /// + /// Creates a SELECT COUNT Sql statement. + /// + /// The type of the DTO to count. + /// The origin sql. + /// An alias. + /// Expressions indicating the columns to count. + /// The Sql statement. + /// + /// If is empty, all columns are counted. + /// + public static Sql SelectCount(this Sql sql, string? alias, + params Expression>[] fields) + { + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } - //var leftColumn = ExpressionHelper.FindProperty(leftMember) as PropertyInfo; - //var rightColumn = ExpressionHelper.FindProperty(rightMember) as PropertyInfo; + ISqlSyntaxProvider sqlSyntax = sql.SqlContext.SqlSyntax; + var columns = fields.Length == 0 + ? sql.GetColumns(withAlias: false) + : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); + var text = "COUNT (" + string.Join(", ", columns) + ")"; + if (alias != null) + { + text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias); + } - //var leftColumnName = leftColumn.GetColumnName(); - //var rightColumnName = rightColumn.GetColumnName(); + return sql.Select(text); + } - //string onClause = $"{sqlSyntax.GetQuotedTableName(leftTableName)}.{sqlSyntax.GetQuotedColumnName(leftColumnName)} = {sqlSyntax.GetQuotedTableName(rightTableName)}.{sqlSyntax.GetQuotedColumnName(rightColumnName)}"; - //return clause.On(onClause); + /// + /// Creates a SELECT * Sql statement. + /// + /// The origin sql. + /// The Sql statement. + public static Sql SelectAll(this Sql sql) + { + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); } - /// - /// Appends an ON clause to a SqlJoin statement. - /// - /// The Sql join statement. - /// A Sql fragment to use as the ON clause body. - /// The Sql statement. - public static Sql On(this Sql.SqlJoinClause sqlJoin, Func, Sql> on) + return sql.Select("*"); + } + + /// + /// Creates a SELECT Sql statement. + /// + /// The type of the DTO to select. + /// The origin sql. + /// Expressions indicating the columns to select. + /// The Sql statement. + /// + /// If is empty, all columns are selected. + /// + public static Sql Select(this Sql sql, + params Expression>[] fields) + { + if (sql == null) { - var sql = new Sql(sqlJoin.SqlContext); - sql = on(sql); - var text = sql.SQL.Trim().TrimStart("WHERE").Trim(); - return sqlJoin.On(text, sql.Arguments); + throw new ArgumentNullException(nameof(sql)); } - /// - /// Appends an ON clause to a SqlJoin statement. - /// - /// The type of Dto 1. - /// The type of Dto 2. - /// The SqlJoin statement. - /// A predicate to transform and use as the ON clause body. - /// An optional alias for Dto 1 table. - /// An optional alias for Dto 2 table. - /// The Sql statement. - public static Sql On(this Sql.SqlJoinClause sqlJoin, Expression> predicate, string? aliasLeft = null, string? aliasRight = null) + return sql.Select(sql.GetColumns(columnExpressions: fields)); + } + + /// + /// Creates a SELECT DISTINCT Sql statement. + /// + /// The type of the DTO to select. + /// The origin sql. + /// Expressions indicating the columns to select. + /// The Sql statement. + /// + /// If is empty, all columns are selected. + /// + public static Sql SelectDistinct(this Sql sql, + params Expression>[] fields) + { + if (sql == null) { - var expresionist = new PocoToSqlExpressionVisitor(sqlJoin.SqlContext, aliasLeft, aliasRight); - var onExpression = expresionist.Visit(predicate); - return sqlJoin.On(onExpression, expresionist.GetSqlParameters()); + throw new ArgumentNullException(nameof(sql)); } - /// - /// Appends an ON clause to a SqlJoin statement. - /// - /// The type of Dto 1. - /// The type of Dto 2. - /// The type of Dto 3. - /// The SqlJoin statement. - /// A predicate to transform and use as the ON clause body. - /// An optional alias for Dto 1 table. - /// An optional alias for Dto 2 table. - /// An optional alias for Dto 3 table. - /// The Sql statement. - public static Sql On(this Sql.SqlJoinClause sqlJoin, Expression> predicate, string? aliasLeft = null, string? aliasRight = null, string? aliasOther = null) + var columns = sql.GetColumns(columnExpressions: fields); + sql.Append("SELECT DISTINCT " + string.Join(", ", columns)); + return sql; + } + + public static Sql SelectDistinct(this Sql sql, params object[] columns) + { + sql.Append("SELECT DISTINCT " + string.Join(", ", columns)); + return sql; + } + + //this.Append("SELECT " + string.Join(", ", columns), new object[0]); + + /// + /// Creates a SELECT Sql statement. + /// + /// The type of the DTO to select. + /// The origin sql. + /// A table alias. + /// Expressions indicating the columns to select. + /// The Sql statement. + /// + /// If is empty, all columns are selected. + /// + public static Sql Select(this Sql sql, string tableAlias, + params Expression>[] fields) + { + if (sql == null) { - var expresionist = new PocoToSqlExpressionVisitor(sqlJoin.SqlContext, aliasLeft, aliasRight, aliasOther); - var onExpression = expresionist.Visit(predicate); - return sqlJoin.On(onExpression, expresionist.GetSqlParameters()); + throw new ArgumentNullException(nameof(sql)); } - #endregion - - #region Select + return sql.Select(sql.GetColumns(tableAlias, columnExpressions: fields)); + } - /// - /// Alters a Sql statement to return a maximum amount of rows. - /// - /// The Sql statement. - /// The maximum number of rows to return. - /// The Sql statement. - public static Sql SelectTop(this Sql sql, int count) + /// + /// Adds columns to a SELECT Sql statement. + /// + /// The origin sql. + /// Columns to select. + /// The Sql statement. + public static Sql AndSelect(this Sql sql, params string[] fields) + { + if (sql == null) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); - return sql.SqlContext.SqlSyntax.SelectTop(sql, count); + throw new ArgumentNullException(nameof(sql)); } - /// - /// Creates a SELECT COUNT(*) Sql statement. - /// - /// The origin sql. - /// An optional alias. - /// The Sql statement. - public static Sql SelectCount(this Sql sql, string? alias = null) + return sql.Append(", " + string.Join(", ", fields)); + } + + /// + /// Adds columns to a SELECT Sql statement. + /// + /// The type of the DTO to select. + /// The origin sql. + /// Expressions indicating the columns to select. + /// The Sql statement. + /// + /// If is empty, all columns are selected. + /// + public static Sql AndSelect(this Sql sql, + params Expression>[] fields) + { + if (sql == null) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); - var text = "COUNT(*)"; - if (alias != null) text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias); - return sql.Select(text); + throw new ArgumentNullException(nameof(sql)); } - /// - /// Creates a SELECT COUNT Sql statement. - /// - /// The type of the DTO to count. - /// The origin sql. - /// Expressions indicating the columns to count. - /// The Sql statement. - /// - /// If is empty, all columns are counted. - /// - public static Sql SelectCount(this Sql sql, params Expression>[] fields) - => sql.SelectCount(null, fields); + return sql.Append(", " + string.Join(", ", sql.GetColumns(columnExpressions: fields))); + } - /// - /// Creates a SELECT COUNT Sql statement. - /// - /// The type of the DTO to count. - /// The origin sql. - /// An alias. - /// Expressions indicating the columns to count. - /// The Sql statement. - /// - /// If is empty, all columns are counted. - /// - public static Sql SelectCount(this Sql sql, string? alias, params Expression>[] fields) + /// + /// Adds columns to a SELECT Sql statement. + /// + /// The type of the DTO to select. + /// The origin sql. + /// A table alias. + /// Expressions indicating the columns to select. + /// The Sql statement. + /// + /// If is empty, all columns are selected. + /// + public static Sql AndSelect(this Sql sql, string tableAlias, + params Expression>[] fields) + { + if (sql == null) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); - var sqlSyntax = sql.SqlContext.SqlSyntax; - var columns = fields.Length == 0 - ? sql.GetColumns(withAlias: false) - : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); - var text = "COUNT (" + string.Join(", ", columns) + ")"; - if (alias != null) text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias); - return sql.Select(text); + throw new ArgumentNullException(nameof(sql)); } - /// - /// Creates a SELECT * Sql statement. - /// - /// The origin sql. - /// The Sql statement. - public static Sql SelectAll(this Sql sql) + return sql.Append(", " + string.Join(", ", sql.GetColumns(tableAlias, columnExpressions: fields))); + } + + /// + /// Adds a COUNT(*) to a SELECT Sql statement. + /// + /// The origin sql. + /// An optional alias. + /// The Sql statement. + public static Sql AndSelectCount(this Sql sql, string? alias = null) + { + if (sql == null) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); - return sql.Select("*"); + throw new ArgumentNullException(nameof(sql)); } - /// - /// Creates a SELECT Sql statement. - /// - /// The type of the DTO to select. - /// The origin sql. - /// Expressions indicating the columns to select. - /// The Sql statement. - /// - /// If is empty, all columns are selected. - /// - public static Sql Select(this Sql sql, params Expression>[] fields) + var text = ", COUNT(*)"; + if (alias != null) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); - return sql.Select(sql.GetColumns(columnExpressions: fields)); + text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias); } - /// - /// Creates a SELECT DISTINCT Sql statement. - /// - /// The type of the DTO to select. - /// The origin sql. - /// Expressions indicating the columns to select. - /// The Sql statement. - /// - /// If is empty, all columns are selected. - /// - public static Sql SelectDistinct(this Sql sql, params Expression>[] fields) + return sql.Append(text); + } + + /// + /// Adds a COUNT to a SELECT Sql statement. + /// + /// The type of the DTO to count. + /// The origin sql. + /// Expressions indicating the columns to count. + /// The Sql statement. + /// + /// If is empty, all columns are counted. + /// + public static Sql AndSelectCount(this Sql sql, + params Expression>[] fields) + => sql.AndSelectCount(null, fields); + + /// + /// Adds a COUNT to a SELECT Sql statement. + /// + /// The type of the DTO to count. + /// The origin sql. + /// An alias. + /// Expressions indicating the columns to count. + /// The Sql statement. + /// + /// If is empty, all columns are counted. + /// + public static Sql AndSelectCount(this Sql sql, string? alias = null, + params Expression>[] fields) + { + if (sql == null) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); - var columns = sql.GetColumns(columnExpressions: fields); - sql.Append("SELECT DISTINCT " + string.Join(", ", columns)); - return sql; + throw new ArgumentNullException(nameof(sql)); } - public static Sql SelectDistinct(this Sql sql, params object[] columns) + ISqlSyntaxProvider sqlSyntax = sql.SqlContext.SqlSyntax; + var columns = fields.Length == 0 + ? sql.GetColumns(withAlias: false) + : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); + var text = ", COUNT (" + string.Join(", ", columns) + ")"; + if (alias != null) { - sql.Append("SELECT DISTINCT " + string.Join(", ", columns)); - return sql; + text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias); } - //this.Append("SELECT " + string.Join(", ", columns), new object[0]); + return sql.Append(text); + } - /// - /// Creates a SELECT Sql statement. - /// - /// The type of the DTO to select. - /// The origin sql. - /// A table alias. - /// Expressions indicating the columns to select. - /// The Sql statement. - /// - /// If is empty, all columns are selected. - /// - public static Sql Select(this Sql sql, string tableAlias, params Expression>[] fields) + /// + /// Creates a SELECT Sql statement with a referenced Dto. + /// + /// The type of the Dto to select. + /// The origin Sql. + /// An expression specifying the reference. + /// The Sql statement. + public static Sql Select(this Sql sql, Func, SqlRef> reference) + { + if (sql == null) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); - return sql.Select(sql.GetColumns(tableAlias: tableAlias, columnExpressions: fields)); + throw new ArgumentNullException(nameof(sql)); } - /// - /// Adds columns to a SELECT Sql statement. - /// - /// The origin sql. - /// Columns to select. - /// The Sql statement. - public static Sql AndSelect(this Sql sql, params string[] fields) + sql.Select(sql.GetColumns()); + + reference?.Invoke(new SqlRef(sql, null)); + return sql; + } + + /// + /// Creates a SELECT Sql statement with a referenced Dto. + /// + /// The type of the Dto to select. + /// The origin Sql. + /// An expression specifying the reference. + /// An expression to apply to the Sql statement before adding the reference selection. + /// The Sql statement. + /// + /// The expression applies to the Sql statement before the reference selection + /// is added, so that it is possible to add (e.g. calculated) columns to the referencing Dto. + /// + public static Sql Select(this Sql sql, Func, SqlRef> reference, + Func, Sql> sqlexpr) + { + if (sql == null) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); - return sql.Append(", " + string.Join(", ", fields)); + throw new ArgumentNullException(nameof(sql)); } + sql.Select(sql.GetColumns()); + + sql = sqlexpr(sql); + + reference(new SqlRef(sql, null)); + return sql; + } + + /// + /// Represents a Dto reference expression. + /// + /// The type of the referencing Dto. + public class SqlRef + { /// - /// Adds columns to a SELECT Sql statement. + /// Initializes a new Dto reference expression. /// - /// The type of the DTO to select. - /// The origin sql. - /// Expressions indicating the columns to select. - /// The Sql statement. - /// - /// If is empty, all columns are selected. - /// - public static Sql AndSelect(this Sql sql, params Expression>[] fields) + /// The original Sql expression. + /// The current Dtos prefix. + public SqlRef(Sql sql, string? prefix) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); - return sql.Append(", " + string.Join(", ", sql.GetColumns(columnExpressions: fields))); + Sql = sql; + Prefix = prefix; } /// - /// Adds columns to a SELECT Sql statement. + /// Gets the original Sql expression. /// - /// The type of the DTO to select. - /// The origin sql. - /// A table alias. - /// Expressions indicating the columns to select. - /// The Sql statement. - /// - /// If is empty, all columns are selected. - /// - public static Sql AndSelect(this Sql sql, string tableAlias, params Expression>[] fields) - { - if (sql == null) throw new ArgumentNullException(nameof(sql)); - return sql.Append(", " + string.Join(", ", sql.GetColumns(tableAlias: tableAlias, columnExpressions: fields))); - } + public Sql Sql { get; } /// - /// Adds a COUNT(*) to a SELECT Sql statement. + /// Gets the current Dtos prefix. /// - /// The origin sql. - /// An optional alias. - /// The Sql statement. - public static Sql AndSelectCount(this Sql sql, string? alias = null) - { - if (sql == null) throw new ArgumentNullException(nameof(sql)); - var text = ", COUNT(*)"; - if (alias != null) text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias); - return sql.Append(text); - } + public string? Prefix { get; } /// - /// Adds a COUNT to a SELECT Sql statement. + /// Appends fields for a referenced Dto. /// - /// The type of the DTO to count. - /// The origin sql. - /// Expressions indicating the columns to count. - /// The Sql statement. - /// - /// If is empty, all columns are counted. - /// - public static Sql AndSelectCount(this Sql sql, params Expression>[] fields) - => sql.AndSelectCount(null, fields); + /// The type of the referenced Dto. + /// An expression specifying the referencing field. + /// An optional expression representing a nested reference selection. + /// A SqlRef statement. + public SqlRef Select(Expression> field, + Func, SqlRef>? reference = null) + => Select(field, null, reference); /// - /// Adds a COUNT to a SELECT Sql statement. + /// Appends fields for a referenced Dto. /// - /// The type of the DTO to count. - /// The origin sql. - /// An alias. - /// Expressions indicating the columns to count. - /// The Sql statement. - /// - /// If is empty, all columns are counted. - /// - public static Sql AndSelectCount(this Sql sql, string? alias = null, params Expression>[] fields) + /// The type of the referenced Dto. + /// An expression specifying the referencing field. + /// The referenced Dto table alias. + /// An optional expression representing a nested reference selection. + /// A SqlRef statement. + public SqlRef Select(Expression> field, string? tableAlias, + Func, SqlRef>? reference = null) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); - var sqlSyntax = sql.SqlContext.SqlSyntax; - var columns = fields.Length == 0 - ? sql.GetColumns(withAlias: false) - : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); - var text = ", COUNT (" + string.Join(", ", columns) + ")"; - if (alias != null) text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias); - return sql.Append(text); + PropertyInfo? property = field == null ? null : ExpressionHelper.FindProperty(field).Item1 as PropertyInfo; + return Select(property, tableAlias, reference); } /// - /// Creates a SELECT Sql statement with a referenced Dto. + /// Selects referenced DTOs. /// - /// The type of the Dto to select. - /// The origin Sql. - /// An expression specifying the reference. - /// The Sql statement. - public static Sql Select(this Sql sql, Func, SqlRef> reference) - { - if (sql == null) throw new ArgumentNullException(nameof(sql)); - - sql.Select(sql.GetColumns()); - - reference?.Invoke(new SqlRef(sql, null)); - return sql; - } + /// The type of the referenced DTOs. + /// An expression specifying the referencing field. + /// An optional expression representing a nested reference selection. + /// A referenced DTO expression. + /// + /// The referencing property has to be a List{}. + /// + public SqlRef Select(Expression>> field, + Func, SqlRef>? reference = null) + => Select(field, null, reference); /// - /// Creates a SELECT Sql statement with a referenced Dto. + /// Selects referenced DTOs. /// - /// The type of the Dto to select. - /// The origin Sql. - /// An expression specifying the reference. - /// An expression to apply to the Sql statement before adding the reference selection. - /// The Sql statement. - /// The expression applies to the Sql statement before the reference selection - /// is added, so that it is possible to add (e.g. calculated) columns to the referencing Dto. - public static Sql Select(this Sql sql, Func, SqlRef> reference, Func, Sql> sqlexpr) + /// The type of the referenced DTOs. + /// An expression specifying the referencing field. + /// The DTO table alias. + /// An optional expression representing a nested reference selection. + /// A referenced DTO expression. + /// + /// The referencing property has to be a List{}. + /// + public SqlRef Select(Expression>> field, string? tableAlias, + Func, SqlRef>? reference = null) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); - - sql.Select(sql.GetColumns()); - - sql = sqlexpr(sql); - - reference(new SqlRef(sql, null)); - return sql; + PropertyInfo? property = field == null ? null : ExpressionHelper.FindProperty(field).Item1 as PropertyInfo; + return Select(property, tableAlias, reference); } - /// - /// Represents a Dto reference expression. - /// - /// The type of the referencing Dto. - public class SqlRef + private SqlRef Select(PropertyInfo? propertyInfo, string? tableAlias, + Func, SqlRef>? nested = null) { - /// - /// Initializes a new Dto reference expression. - /// - /// The original Sql expression. - /// The current Dtos prefix. - public SqlRef(Sql sql, string? prefix) - { - Sql = sql; - Prefix = prefix; - } - - /// - /// Gets the original Sql expression. - /// - public Sql Sql { get; } - - /// - /// Gets the current Dtos prefix. - /// - public string? Prefix { get; } - - /// - /// Appends fields for a referenced Dto. - /// - /// The type of the referenced Dto. - /// An expression specifying the referencing field. - /// An optional expression representing a nested reference selection. - /// A SqlRef statement. - public SqlRef Select(Expression> field, Func, SqlRef>? reference = null) - => Select(field, null, reference); - - /// - /// Appends fields for a referenced Dto. - /// - /// The type of the referenced Dto. - /// An expression specifying the referencing field. - /// The referenced Dto table alias. - /// An optional expression representing a nested reference selection. - /// A SqlRef statement. - public SqlRef Select(Expression> field, string? tableAlias, Func, SqlRef>? reference = null) - { - var property = field == null ? null : ExpressionHelper.FindProperty(field).Item1 as PropertyInfo; - return Select(property, tableAlias, reference); - } - - /// - /// Selects referenced DTOs. - /// - /// The type of the referenced DTOs. - /// An expression specifying the referencing field. - /// An optional expression representing a nested reference selection. - /// A referenced DTO expression. - /// - /// The referencing property has to be a List{}. - /// - public SqlRef Select(Expression>> field, Func, SqlRef>? reference = null) - => Select(field, null, reference); - - /// - /// Selects referenced DTOs. - /// - /// The type of the referenced DTOs. - /// An expression specifying the referencing field. - /// The DTO table alias. - /// An optional expression representing a nested reference selection. - /// A referenced DTO expression. - /// - /// The referencing property has to be a List{}. - /// - public SqlRef Select(Expression>> field, string? tableAlias, Func, SqlRef>? reference = null) + var referenceName = propertyInfo?.Name ?? typeof(TDto).Name; + if (Prefix != null) { - var property = field == null ? null : ExpressionHelper.FindProperty(field).Item1 as PropertyInfo; - return Select(property, tableAlias, reference); + referenceName = Prefix + PocoData.Separator + referenceName; } - private SqlRef Select(PropertyInfo? propertyInfo, string? tableAlias, Func, SqlRef>? nested = null) - { - var referenceName = propertyInfo?.Name ?? typeof (TDto).Name; - if (Prefix != null) referenceName = Prefix + PocoData.Separator + referenceName; - - var columns = Sql.GetColumns(tableAlias, referenceName); - Sql.Append(", " + string.Join(", ", columns)); + var columns = Sql.GetColumns(tableAlias, referenceName); + Sql.Append(", " + string.Join(", ", columns)); - nested?.Invoke(new SqlRef(Sql, referenceName)); - return this; - } + nested?.Invoke(new SqlRef(Sql, referenceName)); + return this; } + } - /// - /// Gets fields for a Dto. - /// - /// The type of the Dto. - /// The origin sql. - /// Expressions specifying the fields. - /// The comma-separated list of fields. - /// - /// If is empty, all fields are selected. - /// - public static string Columns(this Sql sql, params Expression>[] fields) + /// + /// Gets fields for a Dto. + /// + /// The type of the Dto. + /// The origin sql. + /// Expressions specifying the fields. + /// The comma-separated list of fields. + /// + /// If is empty, all fields are selected. + /// + public static string Columns(this Sql sql, params Expression>[] fields) + { + if (sql == null) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); - return string.Join(", ", sql.GetColumns(columnExpressions: fields, withAlias: false)); + throw new ArgumentNullException(nameof(sql)); } - /// - /// Gets fields for a Dto. - /// - public static string ColumnsForInsert(this Sql sql, params Expression>[]? fields) - { - if (sql == null) throw new ArgumentNullException(nameof(sql)); - return string.Join(", ", sql.GetColumns(columnExpressions: fields, withAlias: false, forInsert: true)); - } + return string.Join(", ", sql.GetColumns(columnExpressions: fields, withAlias: false)); + } - /// - /// Gets fields for a Dto. - /// - /// The type of the Dto. - /// The origin sql. - /// The Dto table alias. - /// Expressions specifying the fields. - /// The comma-separated list of fields. - /// - /// If is empty, all fields are selected. - /// - public static string Columns(this Sql sql, string alias, params Expression>[] fields) + /// + /// Gets fields for a Dto. + /// + public static string ColumnsForInsert(this Sql sql, + params Expression>[]? fields) + { + if (sql == null) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); - return string.Join(", ", sql.GetColumns(columnExpressions: fields, withAlias: false, tableAlias: alias)); + throw new ArgumentNullException(nameof(sql)); } - #endregion - - #region Delete + return string.Join(", ", sql.GetColumns(columnExpressions: fields, withAlias: false, forInsert: true)); + } - public static Sql Delete(this Sql sql) + /// + /// Gets fields for a Dto. + /// + /// The type of the Dto. + /// The origin sql. + /// The Dto table alias. + /// Expressions specifying the fields. + /// The comma-separated list of fields. + /// + /// If is empty, all fields are selected. + /// + public static string Columns(this Sql sql, string alias, + params Expression>[] fields) + { + if (sql == null) { - sql.Append("DELETE"); - return sql; + throw new ArgumentNullException(nameof(sql)); } - public static Sql Delete(this Sql sql) - { - var type = typeof(TDto); - var tableName = type.GetTableName(); + return string.Join(", ", sql.GetColumns(columnExpressions: fields, withAlias: false, tableAlias: alias)); + } - // FROM optional SQL server, but not elsewhere. - sql.Append($"DELETE FROM {sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName)}"); - return sql; - } + #endregion - #endregion + #region Delete - #region Update + public static Sql Delete(this Sql sql) + { + sql.Append("DELETE"); + return sql; + } - public static Sql Update(this Sql sql) - { - sql.Append("UPDATE"); - return sql; - } + public static Sql Delete(this Sql sql) + { + Type type = typeof(TDto); + var tableName = type.GetTableName(); - public static Sql Update(this Sql sql) - { - var type = typeof(TDto); - var tableName = type.GetTableName(); + // FROM optional SQL server, but not elsewhere. + sql.Append($"DELETE FROM {sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName)}"); + return sql; + } - sql.Append($"UPDATE {sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName)}"); - return sql; - } + #endregion - public static Sql Update(this Sql sql, Func, SqlUpd> updates) - { - var type = typeof(TDto); - var tableName = type.GetTableName(); + #region Update - sql.Append($"UPDATE {sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName)} SET"); + public static Sql Update(this Sql sql) + { + sql.Append("UPDATE"); + return sql; + } - var u = new SqlUpd(sql.SqlContext); - u = updates(u); - var first = true; - foreach (var setExpression in u.SetExpressions) - { - switch (setExpression.Item2) - { - case null: - sql.Append((first ? "" : ",") + " " + setExpression.Item1 + "=NULL"); - break; - case string s when s == string.Empty: - sql.Append((first ? "" : ",") + " " + setExpression.Item1 + "=''"); - break; - default: - sql.Append((first ? "" : ",") + " " + setExpression.Item1 + "=@0", setExpression.Item2); - break; - } + public static Sql Update(this Sql sql) + { + Type type = typeof(TDto); + var tableName = type.GetTableName(); - first = false; - } + sql.Append($"UPDATE {sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName)}"); + return sql; + } - if (!first) - sql.Append(" "); + public static Sql Update(this Sql sql, Func, SqlUpd> updates) + { + Type type = typeof(TDto); + var tableName = type.GetTableName(); - return sql; - } + sql.Append($"UPDATE {sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName)} SET"); - public class SqlUpd + var u = new SqlUpd(sql.SqlContext); + u = updates(u); + var first = true; + foreach (Tuple setExpression in u.SetExpressions) { - private readonly ISqlContext _sqlContext; - private readonly List> _setExpressions = new List>(); - - public SqlUpd(ISqlContext sqlContext) - { - _sqlContext = sqlContext; - } - - public SqlUpd Set(Expression> fieldSelector, object? value) + switch (setExpression.Item2) { - var fieldName = _sqlContext.SqlSyntax.GetFieldNameForUpdate(fieldSelector); - _setExpressions.Add(new Tuple(fieldName, value)); - return this; + case null: + sql.Append((first ? "" : ",") + " " + setExpression.Item1 + "=NULL"); + break; + case string s when s == string.Empty: + sql.Append((first ? "" : ",") + " " + setExpression.Item1 + "=''"); + break; + default: + sql.Append((first ? "" : ",") + " " + setExpression.Item1 + "=@0", setExpression.Item2); + break; } - public List> SetExpressions => _setExpressions; + first = false; } - #endregion - - #region Hints + if (!first) + { + sql.Append(" "); + } - /// - /// Appends the relevant ForUpdate hint. - /// - /// The Sql statement. - /// The Sql statement. - /// - /// NOTE: This method will not work for all queries, only simple ones! - /// - public static Sql ForUpdate(this Sql sql) - => sql.SqlContext.SqlSyntax.InsertForUpdateHint(sql); + return sql; + } - public static Sql AppendForUpdateHint(this Sql sql) - => sql.SqlContext.SqlSyntax.AppendForUpdateHint(sql); + public class SqlUpd + { + private readonly ISqlContext _sqlContext; - #endregion + public SqlUpd(ISqlContext sqlContext) => _sqlContext = sqlContext; - #region Aliasing + public List> SetExpressions { get; } = new(); - internal static string GetAliasedField(this Sql sql, string field) + public SqlUpd Set(Expression> fieldSelector, object? value) { - // get alias, if aliased - // - // regex looks for pattern "([\w+].[\w+]) AS ([\w+])" ie "(field) AS (alias)" - // and, if found & a group's field matches the field name, returns the alias - // - // so... if query contains "[umbracoNode].[nodeId] AS [umbracoNode__nodeId]" - // then GetAliased for "[umbracoNode].[nodeId]" returns "[umbracoNode__nodeId]" - - var matches = sql.SqlContext.SqlSyntax.AliasRegex.Matches(sql.SQL); - var match = matches.Cast().FirstOrDefault(m => m.Groups[1].Value.InvariantEquals(field)); - return match == null ? field : match.Groups[2].Value; + var fieldName = _sqlContext.SqlSyntax.GetFieldNameForUpdate(fieldSelector); + SetExpressions.Add(new Tuple(fieldName, value)); + return this; } + } - #endregion + #endregion - #region Utilities + #region Hints - private static string[] GetColumns(this Sql sql, string? tableAlias = null, string? referenceName = null, Expression>[]? columnExpressions = null, bool withAlias = true, bool forInsert = false) - { - var pd = sql.SqlContext.PocoDataFactory.ForType(typeof (TDto)); - var tableName = tableAlias ?? pd.TableInfo.TableName; - var queryColumns = pd.QueryColumns.ToList(); + /// + /// Appends the relevant ForUpdate hint. + /// + /// The Sql statement. + /// The Sql statement. + /// + /// NOTE: This method will not work for all queries, only simple ones! + /// + public static Sql ForUpdate(this Sql sql) + => sql.SqlContext.SqlSyntax.InsertForUpdateHint(sql); - Dictionary? aliases = null; + public static Sql AppendForUpdateHint(this Sql sql) + => sql.SqlContext.SqlSyntax.AppendForUpdateHint(sql); - if (columnExpressions != null && columnExpressions.Length > 0) + #endregion + + #region Utilities + + private static string[] GetColumns(this Sql sql, string? tableAlias = null, + string? referenceName = null, Expression>[]? columnExpressions = null, + bool withAlias = true, bool forInsert = false) + { + PocoData? pd = sql.SqlContext.PocoDataFactory.ForType(typeof(TDto)); + var tableName = tableAlias ?? pd.TableInfo.TableName; + var queryColumns = pd.QueryColumns.ToList(); + + Dictionary? aliases = null; + + if (columnExpressions != null && columnExpressions.Length > 0) + { + var names = columnExpressions.Select(x => { - var names = columnExpressions.Select(x => + (MemberInfo member, var alias) = ExpressionHelper.FindProperty(x); + var field = member as PropertyInfo; + var fieldName = field?.GetColumnName(); + if (alias != null && fieldName is not null) { - (var member, var alias) = ExpressionHelper.FindProperty(x); - var field = member as PropertyInfo; - var fieldName = field?.GetColumnName(); - if (alias != null && fieldName is not null) + if (aliases == null) { - if (aliases == null) - aliases = new Dictionary(); - aliases[fieldName] = alias; + aliases = new Dictionary(); } - return fieldName; - }).ToArray(); - //only get the columns that exist in the selected names - queryColumns = queryColumns.Where(x => names.Contains(x.Key)).ToList(); - - //ensure the order of the columns in the expressions is the order in the result - queryColumns.Sort((a, b) => names.IndexOf(a.Key).CompareTo(names.IndexOf(b.Key))); - } + aliases[fieldName] = alias; + } - string? GetAlias(PocoColumn column) - { - if (aliases != null && aliases.TryGetValue(column.ColumnName, out var alias)) - return alias; + return fieldName; + }).ToArray(); - return withAlias ? (string.IsNullOrEmpty(column.ColumnAlias) ? column.MemberInfoKey : column.ColumnAlias) : null; - } + //only get the columns that exist in the selected names + queryColumns = queryColumns.Where(x => names.Contains(x.Key)).ToList(); - return queryColumns - .Select(x => sql.SqlContext.SqlSyntax.GetColumn(sql.SqlContext.DatabaseType, tableName, x.Value.ColumnName, GetAlias(x.Value)!, referenceName, forInsert: forInsert)) - .ToArray(); + //ensure the order of the columns in the expressions is the order in the result + queryColumns.Sort((a, b) => names.IndexOf(a.Key).CompareTo(names.IndexOf(b.Key))); } - public static string GetTableName(this Type type) + string? GetAlias(PocoColumn column) { - // TODO: returning string.Empty for now - // BUT the code bits that calls this method cannot deal with string.Empty so we - // should either throw, or fix these code bits... - var attr = type.FirstAttribute(); - return string.IsNullOrWhiteSpace(attr?.Value) ? string.Empty : attr.Value; - } + if (aliases != null && aliases.TryGetValue(column.ColumnName, out var alias)) + { + return alias; + } - private static string GetColumnName(this PropertyInfo column) - { - var attr = column.FirstAttribute(); - return string.IsNullOrWhiteSpace(attr?.Name) ? column.Name : attr.Name; + return withAlias + ? string.IsNullOrEmpty(column.ColumnAlias) ? column.MemberInfoKey : column.ColumnAlias + : null; } - public static string ToText(this Sql sql) - { - var text = new StringBuilder(); - sql.ToText(text); - return text.ToString(); - } + return queryColumns + .Select(x => sql.SqlContext.SqlSyntax.GetColumn(sql.SqlContext.DatabaseType, tableName, x.Value.ColumnName, + GetAlias(x.Value)!, referenceName, forInsert)) + .ToArray(); + } - public static void ToText(this Sql sql, StringBuilder text) - { - ToText(sql.SQL, sql.Arguments, text); - } + public static string GetTableName(this Type type) + { + // TODO: returning string.Empty for now + // BUT the code bits that calls this method cannot deal with string.Empty so we + // should either throw, or fix these code bits... + TableNameAttribute? attr = type.FirstAttribute(); + return string.IsNullOrWhiteSpace(attr?.Value) ? string.Empty : attr.Value; + } - public static void ToText(string? sql, object[]? arguments, StringBuilder text) - { - text.AppendLine(sql); + private static string GetColumnName(this PropertyInfo column) + { + ColumnAttribute? attr = column.FirstAttribute(); + return string.IsNullOrWhiteSpace(attr?.Name) ? column.Name : attr.Name; + } - if (arguments == null || arguments.Length == 0) - return; + public static string ToText(this Sql sql) + { + var text = new StringBuilder(); + sql.ToText(text); + return text.ToString(); + } - text.Append(" --"); + public static void ToText(this Sql sql, StringBuilder text) => ToText(sql.SQL, sql.Arguments, text); - var i = 0; - foreach (var arg in arguments) - { - text.Append(" @"); - text.Append(i++); - text.Append(":"); - text.Append(arg); - } + public static void ToText(string? sql, object[]? arguments, StringBuilder text) + { + text.AppendLine(sql); - text.AppendLine(); + if (arguments == null || arguments.Length == 0) + { + return; + } + + text.Append(" --"); + + var i = 0; + foreach (var arg in arguments) + { + text.Append(" @"); + text.Append(i++); + text.Append(":"); + text.Append(arg); } - #endregion + text.AppendLine(); } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Querying/CachedExpression.cs b/src/Umbraco.Infrastructure/Persistence/Querying/CachedExpression.cs index ae812193c9ad..66897b0ae4ef 100644 --- a/src/Umbraco.Infrastructure/Persistence/Querying/CachedExpression.cs +++ b/src/Umbraco.Infrastructure/Persistence/Querying/CachedExpression.cs @@ -1,48 +1,46 @@ -using System; -using System.Linq.Expressions; +using System.Linq.Expressions; -namespace Umbraco.Cms.Infrastructure.Persistence.Querying +namespace Umbraco.Cms.Infrastructure.Persistence.Querying; + +/// +/// Represents an expression which caches the visitor's result. +/// +internal class CachedExpression : Expression { + private string _visitResult = null!; + /// - /// Represents an expression which caches the visitor's result. + /// Gets or sets the inner Expression. /// - internal class CachedExpression : Expression - { - private string _visitResult = null!; - - /// - /// Gets or sets the inner Expression. - /// - public Expression InnerExpression { get; private set; } = null!; + public Expression InnerExpression { get; private set; } = null!; - /// - /// Gets or sets the compiled SQL statement output. - /// - public string VisitResult + /// + /// Gets or sets the compiled SQL statement output. + /// + public string VisitResult + { + get => _visitResult; + set { - get => _visitResult; - set + if (Visited) { - if (Visited) - throw new InvalidOperationException("Cached expression has already been visited."); - _visitResult = value; - Visited = true; + throw new InvalidOperationException("Cached expression has already been visited."); } - } - /// - /// Gets or sets a value indicating whether the cache Expression has been compiled already. - /// - public bool Visited { get; private set; } - - /// - /// Replaces the inner expression. - /// - /// expression. - /// The new expression is assumed to have different parameter but produce the same SQL statement. - public void Wrap(Expression expression) - { - InnerExpression = expression; + _visitResult = value; + Visited = true; } } + + /// + /// Gets or sets a value indicating whether the cache Expression has been compiled already. + /// + public bool Visited { get; private set; } + + /// + /// Replaces the inner expression. + /// + /// expression. + /// The new expression is assumed to have different parameter but produce the same SQL statement. + public void Wrap(Expression expression) => InnerExpression = expression; } diff --git a/src/Umbraco.Infrastructure/Persistence/Querying/ExpressionVisitorBase.cs b/src/Umbraco.Infrastructure/Persistence/Querying/ExpressionVisitorBase.cs index f399f66aca5c..bdf630291701 100644 --- a/src/Umbraco.Infrastructure/Persistence/Querying/ExpressionVisitorBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Querying/ExpressionVisitorBase.cs @@ -1,8 +1,5 @@ -using System; using System.Collections; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Linq; using System.Linq.Expressions; using System.Text; using Umbraco.Cms.Core.Composing; @@ -10,769 +7,843 @@ using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Querying +namespace Umbraco.Cms.Infrastructure.Persistence.Querying; +// TODO: are we basically duplicating entire parts of NPoco just because of SqlSyntax ?! +// try to use NPoco's version ! + +/// +/// An expression tree parser to create SQL statements and SQL parameters based on a strongly typed expression. +/// +/// This object is stateful and cannot be re-used to parse an expression. +internal abstract class ExpressionVisitorBase { - // TODO: are we basically duplicating entire parts of NPoco just because of SqlSyntax ?! - // try to use NPoco's version ! + /// + /// Gets the list of SQL parameters. + /// + protected readonly List SqlParameters = new(); + + protected ExpressionVisitorBase(ISqlSyntaxProvider sqlSyntax) => SqlSyntax = sqlSyntax; + + /// + /// Gets or sets a value indicating whether the visited expression has been visited already, + /// in which case visiting will just populate the SQL parameters. + /// + protected bool Visited { get; set; } + + /// + /// Gets or sets the SQL syntax provider for the current database. + /// + protected ISqlSyntaxProvider SqlSyntax { get; } + + /// + /// Gets the SQL parameters. + /// + /// + public object[] GetSqlParameters() => SqlParameters.ToArray(); /// - /// An expression tree parser to create SQL statements and SQL parameters based on a strongly typed expression. + /// Visits the expression and produces the corresponding SQL statement. /// - /// This object is stateful and cannot be re-used to parse an expression. - internal abstract class ExpressionVisitorBase + /// The expression + /// The SQL statement corresponding to the expression. + /// Also populates the SQL parameters. + public virtual string Visit(Expression? expression) { - protected ExpressionVisitorBase(ISqlSyntaxProvider sqlSyntax) + if (expression == null) { - SqlSyntax = sqlSyntax; + return string.Empty; } - /// - /// Gets or sets a value indicating whether the visited expression has been visited already, - /// in which case visiting will just populate the SQL parameters. - /// - protected bool Visited { get; set; } - - /// - /// Gets or sets the SQL syntax provider for the current database. - /// - protected ISqlSyntaxProvider SqlSyntax { get; } - - /// - /// Gets the list of SQL parameters. - /// - protected readonly List SqlParameters = new List(); - - /// - /// Gets the SQL parameters. - /// - /// - public object[] GetSqlParameters() + // if the expression is a CachedExpression, + // visit the inner expression if not already visited + var cachedExpression = expression as CachedExpression; + if (cachedExpression != null) { - return SqlParameters.ToArray(); + Visited = cachedExpression.Visited; + expression = cachedExpression.InnerExpression; } - /// - /// Visits the expression and produces the corresponding SQL statement. - /// - /// The expression - /// The SQL statement corresponding to the expression. - /// Also populates the SQL parameters. - public virtual string Visit(Expression? expression) - { - if (expression == null) return string.Empty; - - // if the expression is a CachedExpression, - // visit the inner expression if not already visited - var cachedExpression = expression as CachedExpression; - if (cachedExpression != null) - { - Visited = cachedExpression.Visited; - expression = cachedExpression.InnerExpression; - } + string result; - string result; + switch (expression.NodeType) + { + case ExpressionType.Lambda: + result = VisitLambda(expression as LambdaExpression); + break; + case ExpressionType.MemberAccess: + result = VisitMemberAccess(expression as MemberExpression); + break; + case ExpressionType.Constant: + result = VisitConstant(expression as ConstantExpression); + break; + case ExpressionType.Add: + case ExpressionType.AddChecked: + case ExpressionType.Subtract: + case ExpressionType.SubtractChecked: + case ExpressionType.Multiply: + case ExpressionType.MultiplyChecked: + case ExpressionType.Divide: + case ExpressionType.Modulo: + case ExpressionType.And: + case ExpressionType.AndAlso: + case ExpressionType.Or: + case ExpressionType.OrElse: + case ExpressionType.LessThan: + case ExpressionType.LessThanOrEqual: + case ExpressionType.GreaterThan: + case ExpressionType.GreaterThanOrEqual: + case ExpressionType.Equal: + case ExpressionType.NotEqual: + case ExpressionType.Coalesce: + case ExpressionType.ArrayIndex: + case ExpressionType.RightShift: + case ExpressionType.LeftShift: + case ExpressionType.ExclusiveOr: + result = VisitBinary(expression as BinaryExpression); + break; + case ExpressionType.Negate: + case ExpressionType.NegateChecked: + case ExpressionType.Not: + case ExpressionType.Convert: + case ExpressionType.ConvertChecked: + case ExpressionType.ArrayLength: + case ExpressionType.Quote: + case ExpressionType.TypeAs: + result = VisitUnary(expression as UnaryExpression); + break; + case ExpressionType.Parameter: + result = VisitParameter(expression as ParameterExpression); + break; + case ExpressionType.Call: + result = VisitMethodCall(expression as MethodCallExpression); + break; + case ExpressionType.New: + result = VisitNew(expression as NewExpression); + break; + case ExpressionType.NewArrayInit: + case ExpressionType.NewArrayBounds: + result = VisitNewArray(expression as NewArrayExpression); + break; + default: + result = expression.ToString(); + break; + } - switch (expression.NodeType) - { - case ExpressionType.Lambda: - result = VisitLambda(expression as LambdaExpression); - break; - case ExpressionType.MemberAccess: - result = VisitMemberAccess(expression as MemberExpression); - break; - case ExpressionType.Constant: - result = VisitConstant(expression as ConstantExpression); - break; - case ExpressionType.Add: - case ExpressionType.AddChecked: - case ExpressionType.Subtract: - case ExpressionType.SubtractChecked: - case ExpressionType.Multiply: - case ExpressionType.MultiplyChecked: - case ExpressionType.Divide: - case ExpressionType.Modulo: - case ExpressionType.And: - case ExpressionType.AndAlso: - case ExpressionType.Or: - case ExpressionType.OrElse: - case ExpressionType.LessThan: - case ExpressionType.LessThanOrEqual: - case ExpressionType.GreaterThan: - case ExpressionType.GreaterThanOrEqual: - case ExpressionType.Equal: - case ExpressionType.NotEqual: - case ExpressionType.Coalesce: - case ExpressionType.ArrayIndex: - case ExpressionType.RightShift: - case ExpressionType.LeftShift: - case ExpressionType.ExclusiveOr: - result = VisitBinary(expression as BinaryExpression); - break; - case ExpressionType.Negate: - case ExpressionType.NegateChecked: - case ExpressionType.Not: - case ExpressionType.Convert: - case ExpressionType.ConvertChecked: - case ExpressionType.ArrayLength: - case ExpressionType.Quote: - case ExpressionType.TypeAs: - result = VisitUnary(expression as UnaryExpression); - break; - case ExpressionType.Parameter: - result = VisitParameter(expression as ParameterExpression); - break; - case ExpressionType.Call: - result = VisitMethodCall(expression as MethodCallExpression); - break; - case ExpressionType.New: - result = VisitNew(expression as NewExpression); - break; - case ExpressionType.NewArrayInit: - case ExpressionType.NewArrayBounds: - result = VisitNewArray(expression as NewArrayExpression); - break; - default: - result = expression.ToString(); - break; - } + // if the expression is a CachedExpression, + // and is not already compiled, assign the result + if (cachedExpression == null) + { + return result; + } - // if the expression is a CachedExpression, - // and is not already compiled, assign the result - if (cachedExpression == null) - return result; - if (!cachedExpression.Visited) - cachedExpression.VisitResult = result; - return cachedExpression.VisitResult; + if (!cachedExpression.Visited) + { + cachedExpression.VisitResult = result; } - protected abstract string VisitMemberAccess(MemberExpression? m); + return cachedExpression.VisitResult; + } - protected virtual string VisitLambda(LambdaExpression? lambda) - { - if (lambda?.Body.NodeType == ExpressionType.MemberAccess && - lambda.Body is MemberExpression memberExpression && memberExpression.Expression != null) - { - //This deals with members that are boolean (i.e. x => IsTrashed ) - var result = VisitMemberAccess(memberExpression); + protected abstract string VisitMemberAccess(MemberExpression? m); - SqlParameters.Add(true); + protected virtual string VisitLambda(LambdaExpression? lambda) + { + if (lambda?.Body.NodeType == ExpressionType.MemberAccess && + lambda.Body is MemberExpression memberExpression && memberExpression.Expression != null) + { + //This deals with members that are boolean (i.e. x => IsTrashed ) + var result = VisitMemberAccess(memberExpression); - return Visited ? string.Empty : $"{result} = @{SqlParameters.Count - 1}"; - } + SqlParameters.Add(true); - return Visit(lambda?.Body); + return Visited ? string.Empty : $"{result} = @{SqlParameters.Count - 1}"; } - protected virtual string VisitBinary(BinaryExpression? b) - { - if (b is null) - { - return string.Empty; - } - var left = string.Empty; - var right = string.Empty; + return Visit(lambda?.Body); + } - var operand = BindOperant(b.NodeType); - if (operand == "AND" || operand == "OR") - { - if (b.Left is MemberExpression mLeft && mLeft.Expression != null) - { - var r = VisitMemberAccess(mLeft); + protected virtual string VisitBinary(BinaryExpression? b) + { + if (b is null) + { + return string.Empty; + } - SqlParameters.Add(true); + var left = string.Empty; + var right = string.Empty; - if (Visited == false) - left = $"{r} = @{SqlParameters.Count - 1}"; - } - else - { - left = Visit(b.Left); - } - if (b.Right is MemberExpression mRight && mRight.Expression != null) - { - var r = VisitMemberAccess(mRight); + var operand = BindOperant(b.NodeType); + if (operand == "AND" || operand == "OR") + { + if (b.Left is MemberExpression mLeft && mLeft.Expression != null) + { + var r = VisitMemberAccess(mLeft); - SqlParameters.Add(true); + SqlParameters.Add(true); - if (Visited == false) - right = $"{r} = @{SqlParameters.Count - 1}"; - } - else + if (Visited == false) { - right = Visit(b.Right); + left = $"{r} = @{SqlParameters.Count - 1}"; } } - else if (operand == "=") + else { - // deal with (x == true|false) - most common - if (b.Right is ConstantExpression constRight && constRight.Type == typeof(bool)) - return (bool) constRight.Value! ? VisitNotNot(b.Left) : VisitNot(b.Left); - right = Visit(b.Right); - - // deal with (true|false == x) - why not - if (b.Left is ConstantExpression constLeft && constLeft.Type == typeof(bool)) - return (bool) constLeft.Value! ? VisitNotNot(b.Right) : VisitNot(b.Right); left = Visit(b.Left); } - else if (operand == "<>") + + if (b.Right is MemberExpression mRight && mRight.Expression != null) { - // deal with (x != true|false) - most common - if (b.Right is ConstantExpression constRight && constRight.Type == typeof (bool)) - return (bool) constRight.Value! ? VisitNot(b.Left) : VisitNotNot(b.Left); - right = Visit(b.Right); + var r = VisitMemberAccess(mRight); - // deal with (true|false != x) - why not - if (b.Left is ConstantExpression constLeft && constLeft.Type == typeof (bool)) - return (bool) constLeft.Value! ? VisitNot(b.Right) : VisitNotNot(b.Right); - left = Visit(b.Left); + SqlParameters.Add(true); + + if (Visited == false) + { + right = $"{r} = @{SqlParameters.Count - 1}"; + } } else { - left = Visit(b.Left); right = Visit(b.Right); } - - if (operand == "=" && right == "null") operand = "is"; - else if (operand == "<>" && right == "null") operand = "is not"; - else if (operand == "=" || operand == "<>") + } + else if (operand == "=") + { + // deal with (x == true|false) - most common + if (b.Right is ConstantExpression constRight && constRight.Type == typeof(bool)) { - //if (IsTrueExpression(right)) right = GetQuotedTrueValue(); - //else if (IsFalseExpression(right)) right = GetQuotedFalseValue(); - - //if (IsTrueExpression(left)) left = GetQuotedTrueValue(); - //else if (IsFalseExpression(left)) left = GetQuotedFalseValue(); - + return (bool)constRight.Value! ? VisitNotNot(b.Left) : VisitNot(b.Left); } - switch (operand) - { - case "MOD": - case "COALESCE": - return Visited ? string.Empty : $"{operand}({left},{right})"; + right = Visit(b.Right); - default: - return Visited ? string.Empty : $"({left} {operand} {right})"; + // deal with (true|false == x) - why not + if (b.Left is ConstantExpression constLeft && constLeft.Type == typeof(bool)) + { + return (bool)constLeft.Value! ? VisitNotNot(b.Right) : VisitNot(b.Right); } - } - protected virtual List VisitExpressionList(ReadOnlyCollection? original) + left = Visit(b.Left); + } + else if (operand == "<>") { - var list = new List(); - if (original is null) + // deal with (x != true|false) - most common + if (b.Right is ConstantExpression constRight && constRight.Type == typeof(bool)) { - return list; + return (bool)constRight.Value! ? VisitNot(b.Left) : VisitNotNot(b.Left); } - for (int i = 0, n = original.Count; i < n; i++) + + right = Visit(b.Right); + + // deal with (true|false != x) - why not + if (b.Left is ConstantExpression constLeft && constLeft.Type == typeof(bool)) { - if (original[i].NodeType == ExpressionType.NewArrayInit || - original[i].NodeType == ExpressionType.NewArrayBounds) - { - list.AddRange(VisitNewArrayFromExpressionList(original[i] as NewArrayExpression)); - } - else - { - list.Add(Visit(original[i])); - } + return (bool)constLeft.Value! ? VisitNot(b.Right) : VisitNotNot(b.Right); } - return list; + + left = Visit(b.Left); + } + else + { + left = Visit(b.Left); + right = Visit(b.Right); } - protected virtual string VisitNew(NewExpression? newExpression) + if (operand == "=" && right == "null") { - if (newExpression is null) - { - return string.Empty; - } - // TODO: check ! - var member = Expression.Convert(newExpression, typeof(object)); - var lambda = Expression.Lambda>(member); - try - { - var getter = lambda.Compile(); - var o = getter(); + operand = "is"; + } + else if (operand == "<>" && right == "null") + { + operand = "is not"; + } + else if (operand == "=" || operand == "<>") + { + //if (IsTrueExpression(right)) right = GetQuotedTrueValue(); + //else if (IsFalseExpression(right)) right = GetQuotedFalseValue(); - SqlParameters.Add(o); + //if (IsTrueExpression(left)) left = GetQuotedTrueValue(); + //else if (IsFalseExpression(left)) left = GetQuotedFalseValue(); + } - return Visited ? string.Empty : $"@{SqlParameters.Count - 1}"; + switch (operand) + { + case "MOD": + case "COALESCE": + return Visited ? string.Empty : $"{operand}({left},{right})"; + + default: + return Visited ? string.Empty : $"({left} {operand} {right})"; + } + } + + protected virtual List VisitExpressionList(ReadOnlyCollection? original) + { + var list = new List(); + if (original is null) + { + return list; + } + + for (int i = 0, n = original.Count; i < n; i++) + { + if (original[i].NodeType == ExpressionType.NewArrayInit || + original[i].NodeType == ExpressionType.NewArrayBounds) + { + list.AddRange(VisitNewArrayFromExpressionList(original[i] as NewArrayExpression)); } - catch (InvalidOperationException) + else { - if (Visited) - return string.Empty; - - var exprs = VisitExpressionList(newExpression.Arguments); - return string.Join(",", exprs); + list.Add(Visit(original[i])); } } - protected virtual string VisitParameter(ParameterExpression? p) + return list; + } + + protected virtual string VisitNew(NewExpression? newExpression) + { + if (newExpression is null) { - return p?.Name ?? string.Empty; + return string.Empty; } - protected virtual string VisitConstant(ConstantExpression? c) + // TODO: check ! + UnaryExpression member = Expression.Convert(newExpression, typeof(object)); + var lambda = Expression.Lambda>(member); + try { - if (c?.Value == null) - return "null"; + Func getter = lambda.Compile(); + var o = getter(); - SqlParameters.Add(c.Value); + SqlParameters.Add(o); return Visited ? string.Empty : $"@{SqlParameters.Count - 1}"; } - - protected virtual string VisitUnary(UnaryExpression? u) + catch (InvalidOperationException) { - switch (u?.NodeType) + if (Visited) { - case ExpressionType.Not: - return VisitNot(u.Operand); - default: - return Visit(u?.Operand); + return string.Empty; } + + List exprs = VisitExpressionList(newExpression.Arguments); + return string.Join(",", exprs); } + } - private string VisitNot(Expression exp) + protected virtual string VisitParameter(ParameterExpression? p) => p?.Name ?? string.Empty; + + protected virtual string VisitConstant(ConstantExpression? c) + { + if (c?.Value == null) { - var o = Visit(exp); + return "null"; + } - // use a "NOT (...)" syntax instead of "<>" since we don't know whether "<>" works in all sql servers - // also, x.StartsWith(...) translates to "x LIKE '...%'" which we cannot "<>" and have to "NOT (...") + SqlParameters.Add(c.Value); - switch (exp.NodeType) - { - case ExpressionType.MemberAccess: - // false property , i.e. x => !Trashed - // BUT we don't want to do a NOT SQL statement since this generally results in indexes not being used - // so we want to do an == false - SqlParameters.Add(false); - return Visited ? string.Empty : $"{o} = @{SqlParameters.Count - 1}"; - //return Visited ? string.Empty : $"NOT ({o} = @{SqlParameters.Count - 1})"; - default: - // could be anything else, such as: x => !x.Path.StartsWith("-20") - return Visited ? string.Empty : string.Concat("NOT (", o, ")"); - } + return Visited ? string.Empty : $"@{SqlParameters.Count - 1}"; + } + + protected virtual string VisitUnary(UnaryExpression? u) + { + switch (u?.NodeType) + { + case ExpressionType.Not: + return VisitNot(u.Operand); + default: + return Visit(u?.Operand); } + } + + private string VisitNot(Expression exp) + { + var o = Visit(exp); - private string VisitNotNot(Expression exp) + // use a "NOT (...)" syntax instead of "<>" since we don't know whether "<>" works in all sql servers + // also, x.StartsWith(...) translates to "x LIKE '...%'" which we cannot "<>" and have to "NOT (...") + + switch (exp.NodeType) { - var o = Visit(exp); + case ExpressionType.MemberAccess: + // false property , i.e. x => !Trashed + // BUT we don't want to do a NOT SQL statement since this generally results in indexes not being used + // so we want to do an == false + SqlParameters.Add(false); + return Visited ? string.Empty : $"{o} = @{SqlParameters.Count - 1}"; + //return Visited ? string.Empty : $"NOT ({o} = @{SqlParameters.Count - 1})"; + default: + // could be anything else, such as: x => !x.Path.StartsWith("-20") + return Visited ? string.Empty : string.Concat("NOT (", o, ")"); + } + } - switch (exp.NodeType) - { - case ExpressionType.MemberAccess: - // true property, i.e. x => Trashed - SqlParameters.Add(true); - return Visited ? string.Empty : $"({o} = @{SqlParameters.Count - 1})"; - default: - // could be anything else, such as: x => x.Path.StartsWith("-20") - return Visited ? string.Empty : o; - } + private string VisitNotNot(Expression exp) + { + var o = Visit(exp); + + switch (exp.NodeType) + { + case ExpressionType.MemberAccess: + // true property, i.e. x => Trashed + SqlParameters.Add(true); + return Visited ? string.Empty : $"({o} = @{SqlParameters.Count - 1})"; + default: + // could be anything else, such as: x => x.Path.StartsWith("-20") + return Visited ? string.Empty : o; } + } - protected virtual string VisitNewArray(NewArrayExpression? na) + protected virtual string VisitNewArray(NewArrayExpression? na) + { + if (na is null) { - if (na is null) - { - return string.Empty; - } - var exprs = VisitExpressionList(na.Expressions); - return Visited ? string.Empty : string.Join(",", exprs); + return string.Empty; } - protected virtual List VisitNewArrayFromExpressionList(NewArrayExpression? na) - => VisitExpressionList(na?.Expressions); + List exprs = VisitExpressionList(na.Expressions); + return Visited ? string.Empty : string.Join(",", exprs); + } + + protected virtual List VisitNewArrayFromExpressionList(NewArrayExpression? na) + => VisitExpressionList(na?.Expressions); - protected virtual string BindOperant(ExpressionType e) + protected virtual string BindOperant(ExpressionType e) + { + switch (e) { - switch (e) - { - case ExpressionType.Equal: - return "="; - case ExpressionType.NotEqual: - return "<>"; - case ExpressionType.GreaterThan: - return ">"; - case ExpressionType.GreaterThanOrEqual: - return ">="; - case ExpressionType.LessThan: - return "<"; - case ExpressionType.LessThanOrEqual: - return "<="; - case ExpressionType.AndAlso: - return "AND"; - case ExpressionType.OrElse: - return "OR"; - case ExpressionType.Add: - return "+"; - case ExpressionType.Subtract: - return "-"; - case ExpressionType.Multiply: - return "*"; - case ExpressionType.Divide: - return "/"; - case ExpressionType.Modulo: - return "MOD"; - case ExpressionType.Coalesce: - return "COALESCE"; - default: - return e.ToString(); - } + case ExpressionType.Equal: + return "="; + case ExpressionType.NotEqual: + return "<>"; + case ExpressionType.GreaterThan: + return ">"; + case ExpressionType.GreaterThanOrEqual: + return ">="; + case ExpressionType.LessThan: + return "<"; + case ExpressionType.LessThanOrEqual: + return "<="; + case ExpressionType.AndAlso: + return "AND"; + case ExpressionType.OrElse: + return "OR"; + case ExpressionType.Add: + return "+"; + case ExpressionType.Subtract: + return "-"; + case ExpressionType.Multiply: + return "*"; + case ExpressionType.Divide: + return "/"; + case ExpressionType.Modulo: + return "MOD"; + case ExpressionType.Coalesce: + return "COALESCE"; + default: + return e.ToString(); } + } - protected virtual string VisitMethodCall(MethodCallExpression? m) + protected virtual string VisitMethodCall(MethodCallExpression? m) + { + if (m is null) { - if (m is null) - { - return string.Empty; - } - // m.Object is the expression that represent the instance for instance method class, or null for static method calls - // m.Arguments is the collection of expressions that represent arguments of the called method - // m.MethodInfo is the method info for the method to be called - - // assume that static methods are extension methods (probably not ok) - // and then, the method object is its first argument - get "safe" object - var methodObject = m.Object ?? m.Arguments[0]; - var visitedMethodObject = Visit(methodObject); - // and then, "safe" arguments are what would come after the first arg - var methodArgs = m.Object == null - ? new ReadOnlyCollection(m.Arguments.Skip(1).ToList()) - : m.Arguments; - - switch (m.Method.Name) - { - case "ToString": - SqlParameters.Add(methodObject.ToString()); - return Visited ? string.Empty : $"@{SqlParameters.Count - 1}"; - - case "ToUpper": - return Visited ? string.Empty : $"upper({visitedMethodObject})"; - - case "ToLower": - return Visited ? string.Empty : $"lower({visitedMethodObject})"; - - case "Contains": - // for 'Contains', it can either be the string.Contains(string) method, or a collection Contains - // method, which would then need to become a SQL IN clause - but beware that string is - // an enumerable of char, and string.Contains(char) is an extension method - but NOT an SQL IN - - var isCollectionContains = - ( - m.Object == null && // static (extension?) method - m.Arguments.Count == 2 && // with two args - m.Arguments[0].Type != typeof(string) && // but not for string - TypeHelper.IsTypeAssignableFrom(m.Arguments[0].Type) && // first arg being an enumerable - m.Arguments[1].NodeType == ExpressionType.MemberAccess // second arg being a member access - ) || - ( - m.Object != null && // instance method - TypeHelper.IsTypeAssignableFrom(m.Object.Type) && // of an enumerable - m.Object.Type != typeof(string) && // but not for string - m.Arguments.Count == 1 && // with 1 arg - m.Arguments[0].NodeType == ExpressionType.MemberAccess // arg being a member access - ); - - if (isCollectionContains) - goto case "SqlIn"; - else - goto case "Contains**String"; - - case nameof(SqlExpressionExtensions.SqlWildcard): - case "StartsWith": - case "EndsWith": - case "Contains**String": // see "Contains" above - case "Equals": - case nameof(SqlExpressionExtensions.SqlStartsWith): - case nameof(SqlExpressionExtensions.SqlEndsWith): - case nameof(SqlExpressionExtensions.SqlContains): - case nameof(SqlExpressionExtensions.SqlEquals): - case nameof(StringExtensions.InvariantStartsWith): - case nameof(StringExtensions.InvariantEndsWith): - case nameof(StringExtensions.InvariantContains): - case nameof(StringExtensions.InvariantEquals): - - string compareValue; - - if (methodArgs[0].NodeType != ExpressionType.Constant) - { - // if it's a field accessor, we could Visit(methodArgs[0]) and get [a].[b] - // but then, what if we want more, eg .StartsWith(node.Path + ',') ? => not - - //This occurs when we are getting a value from a non constant such as: x => x.Path.StartsWith(content.Path) - // So we'll go get the value: - var member = Expression.Convert(methodArgs[0], typeof(object)); - var lambda = Expression.Lambda>(member); - var getter = lambda.Compile(); - compareValue = getter().ToString()!; - } - else - { - compareValue = methodArgs[0].ToString(); - } + return string.Empty; + } + // m.Object is the expression that represent the instance for instance method class, or null for static method calls + // m.Arguments is the collection of expressions that represent arguments of the called method + // m.MethodInfo is the method info for the method to be called + + // assume that static methods are extension methods (probably not ok) + // and then, the method object is its first argument - get "safe" object + Expression methodObject = m.Object ?? m.Arguments[0]; + var visitedMethodObject = Visit(methodObject); + // and then, "safe" arguments are what would come after the first arg + ReadOnlyCollection methodArgs = m.Object == null + ? new ReadOnlyCollection(m.Arguments.Skip(1).ToList()) + : m.Arguments; + + switch (m.Method.Name) + { + case "ToString": + SqlParameters.Add(methodObject.ToString()); + return Visited ? string.Empty : $"@{SqlParameters.Count - 1}"; - //default column type - var colType = TextColumnType.NVarchar; + case "ToUpper": + return Visited ? string.Empty : $"upper({visitedMethodObject})"; + + case "ToLower": + return Visited ? string.Empty : $"lower({visitedMethodObject})"; + + case "Contains": + // for 'Contains', it can either be the string.Contains(string) method, or a collection Contains + // method, which would then need to become a SQL IN clause - but beware that string is + // an enumerable of char, and string.Contains(char) is an extension method - but NOT an SQL IN + + var isCollectionContains = + ( + m.Object == null && // static (extension?) method + m.Arguments.Count == 2 && // with two args + m.Arguments[0].Type != typeof(string) && // but not for string + TypeHelper.IsTypeAssignableFrom(m.Arguments[0] + .Type) && // first arg being an enumerable + m.Arguments[1].NodeType == ExpressionType.MemberAccess // second arg being a member access + ) || + ( + m.Object != null && // instance method + TypeHelper.IsTypeAssignableFrom(m.Object.Type) && // of an enumerable + m.Object.Type != typeof(string) && // but not for string + m.Arguments.Count == 1 && // with 1 arg + m.Arguments[0].NodeType == ExpressionType.MemberAccess // arg being a member access + ); + + if (isCollectionContains) + { + goto case "SqlIn"; + } - //then check if the col type argument has been passed to the current method (this will be the case for methods like - // SqlContains and other Sql methods) - if (methodArgs.Count > 1) + goto case "Contains**String"; + + case nameof(SqlExpressionExtensions.SqlWildcard): + case "StartsWith": + case "EndsWith": + case "Contains**String": // see "Contains" above + case "Equals": + case nameof(SqlExpressionExtensions.SqlStartsWith): + case nameof(SqlExpressionExtensions.SqlEndsWith): + case nameof(SqlExpressionExtensions.SqlContains): + case nameof(SqlExpressionExtensions.SqlEquals): + case nameof(StringExtensions.InvariantStartsWith): + case nameof(StringExtensions.InvariantEndsWith): + case nameof(StringExtensions.InvariantContains): + case nameof(StringExtensions.InvariantEquals): + + string compareValue; + + if (methodArgs[0].NodeType != ExpressionType.Constant) + { + // if it's a field accessor, we could Visit(methodArgs[0]) and get [a].[b] + // but then, what if we want more, eg .StartsWith(node.Path + ',') ? => not + + //This occurs when we are getting a value from a non constant such as: x => x.Path.StartsWith(content.Path) + // So we'll go get the value: + UnaryExpression member = Expression.Convert(methodArgs[0], typeof(object)); + var lambda = Expression.Lambda>(member); + Func getter = lambda.Compile(); + compareValue = getter().ToString()!; + } + else + { + compareValue = methodArgs[0].ToString(); + } + + //default column type + TextColumnType colType = TextColumnType.NVarchar; + + //then check if the col type argument has been passed to the current method (this will be the case for methods like + // SqlContains and other Sql methods) + if (methodArgs.Count > 1) + { + Expression? colTypeArg = + methodArgs.FirstOrDefault(x => x is ConstantExpression && x.Type == typeof(TextColumnType)); + if (colTypeArg != null) { - var colTypeArg = methodArgs.FirstOrDefault(x => x is ConstantExpression && x.Type == typeof(TextColumnType)); - if (colTypeArg != null) - { - colType = (TextColumnType)((ConstantExpression)colTypeArg).Value!; - } + colType = (TextColumnType)((ConstantExpression)colTypeArg).Value!; } + } - return HandleStringComparison(visitedMethodObject, compareValue, m.Method.Name, colType); + return HandleStringComparison(visitedMethodObject, compareValue, m.Method.Name, colType); - case "Replace": - string searchValue; + case "Replace": + string searchValue; - if (methodArgs[0].NodeType != ExpressionType.Constant) - { - //This occurs when we are getting a value from a non constant such as: x => x.Path.StartsWith(content.Path) - // So we'll go get the value: - var member = Expression.Convert(methodArgs[0], typeof(object)); - var lambda = Expression.Lambda>(member); - var getter = lambda.Compile(); - searchValue = getter().ToString()!; - } - else - { - searchValue = methodArgs[0].ToString(); - } + if (methodArgs[0].NodeType != ExpressionType.Constant) + { + //This occurs when we are getting a value from a non constant such as: x => x.Path.StartsWith(content.Path) + // So we'll go get the value: + UnaryExpression member = Expression.Convert(methodArgs[0], typeof(object)); + var lambda = Expression.Lambda>(member); + Func getter = lambda.Compile(); + searchValue = getter().ToString()!; + } + else + { + searchValue = methodArgs[0].ToString(); + } - if (methodArgs[0].Type != typeof(string) && TypeHelper.IsTypeAssignableFrom(methodArgs[0].Type)) - { - throw new NotSupportedException("An array Contains method is not supported"); - } + if (methodArgs[0].Type != typeof(string) && + TypeHelper.IsTypeAssignableFrom(methodArgs[0].Type)) + { + throw new NotSupportedException("An array Contains method is not supported"); + } - string replaceValue; + string replaceValue; - if (methodArgs[1].NodeType != ExpressionType.Constant) + if (methodArgs[1].NodeType != ExpressionType.Constant) + { + //This occurs when we are getting a value from a non constant such as: x => x.Path.StartsWith(content.Path) + // So we'll go get the value: + UnaryExpression member = Expression.Convert(methodArgs[1], typeof(object)); + var lambda = Expression.Lambda>(member); + Func getter = lambda.Compile(); + replaceValue = getter().ToString()!; + } + else + { + replaceValue = methodArgs[1].ToString(); + } + + if (methodArgs[1].Type != typeof(string) && + TypeHelper.IsTypeAssignableFrom(methodArgs[1].Type)) + { + throw new NotSupportedException("An array Contains method is not supported"); + } + + SqlParameters.Add(RemoveQuote(searchValue)!); + SqlParameters.Add(RemoveQuote(replaceValue)!); + + //don't execute if compiled + return Visited + ? string.Empty + : $"replace({visitedMethodObject}, @{SqlParameters.Count - 2}, @{SqlParameters.Count - 1})"; + + //case "Substring": + // var startIndex = Int32.Parse(args[0].ToString()) + 1; + // if (args.Count == 2) + // { + // var length = Int32.Parse(args[1].ToString()); + // return string.Format("substring({0} from {1} for {2})", + // r, + // startIndex, + // length); + // } + // else + // return string.Format("substring({0} from {1})", + // r, + // startIndex); + //case "Round": + //case "Floor": + //case "Ceiling": + //case "Coalesce": + //case "Abs": + //case "Sum": + // return string.Format("{0}({1}{2})", + // m.Method.Name, + // r, + // args.Count == 1 ? string.Format(",{0}", args[0]) : ""); + //case "Concat": + // var s = new StringBuilder(); + // foreach (Object e in args) + // { + // s.AppendFormat(" || {0}", e); + // } + // return string.Format("{0}{1}", r, s); + + case "SqlIn": + + if (methodArgs.Count != 1 || methodArgs[0].NodeType != ExpressionType.MemberAccess) + { + throw new NotSupportedException("SqlIn must contain the member being accessed."); + } + + var memberAccess = VisitMemberAccess((MemberExpression)methodArgs[0]); + + UnaryExpression inMember = Expression.Convert(methodObject, typeof(object)); + var inLambda = Expression.Lambda>(inMember); + Func inGetter = inLambda.Compile(); + + var inArgs = (IEnumerable)inGetter(); + + var inBuilder = new StringBuilder(); + var inFirst = true; + + inBuilder.Append(memberAccess); + inBuilder.Append(" IN ("); + + foreach (var e in inArgs) + { + SqlParameters.Add(e); + if (inFirst) { - //This occurs when we are getting a value from a non constant such as: x => x.Path.StartsWith(content.Path) - // So we'll go get the value: - var member = Expression.Convert(methodArgs[1], typeof(object)); - var lambda = Expression.Lambda>(member); - var getter = lambda.Compile(); - replaceValue = getter().ToString()!; + inFirst = false; } else { - replaceValue = methodArgs[1].ToString(); + inBuilder.Append(","); } - if (methodArgs[1].Type != typeof(string) && TypeHelper.IsTypeAssignableFrom(methodArgs[1].Type)) - { - throw new NotSupportedException("An array Contains method is not supported"); - } + inBuilder.Append("@"); + inBuilder.Append(SqlParameters.Count - 1); + } - SqlParameters.Add(RemoveQuote(searchValue)!); - SqlParameters.Add(RemoveQuote(replaceValue)!); - - //don't execute if compiled - return Visited ? string.Empty : $"replace({visitedMethodObject}, @{SqlParameters.Count - 2}, @{SqlParameters.Count - 1})"; - - //case "Substring": - // var startIndex = Int32.Parse(args[0].ToString()) + 1; - // if (args.Count == 2) - // { - // var length = Int32.Parse(args[1].ToString()); - // return string.Format("substring({0} from {1} for {2})", - // r, - // startIndex, - // length); - // } - // else - // return string.Format("substring({0} from {1})", - // r, - // startIndex); - //case "Round": - //case "Floor": - //case "Ceiling": - //case "Coalesce": - //case "Abs": - //case "Sum": - // return string.Format("{0}({1}{2})", - // m.Method.Name, - // r, - // args.Count == 1 ? string.Format(",{0}", args[0]) : ""); - //case "Concat": - // var s = new StringBuilder(); - // foreach (Object e in args) - // { - // s.AppendFormat(" || {0}", e); - // } - // return string.Format("{0}{1}", r, s); - - case "SqlIn": - - if (methodArgs.Count != 1 || methodArgs[0].NodeType != ExpressionType.MemberAccess) - throw new NotSupportedException("SqlIn must contain the member being accessed."); - - var memberAccess = VisitMemberAccess((MemberExpression) methodArgs[0]); - - var inMember = Expression.Convert(methodObject, typeof(object)); - var inLambda = Expression.Lambda>(inMember); - var inGetter = inLambda.Compile(); - - var inArgs = (IEnumerable) inGetter(); - - var inBuilder = new StringBuilder(); - var inFirst = true; - - inBuilder.Append(memberAccess); - inBuilder.Append(" IN ("); - - foreach (var e in inArgs) - { - SqlParameters.Add(e); - if (inFirst) inFirst = false; else inBuilder.Append(","); - inBuilder.Append("@"); - inBuilder.Append(SqlParameters.Count - 1); - } + inBuilder.Append(")"); + return inBuilder.ToString(); - inBuilder.Append(")"); - return inBuilder.ToString(); + //case "Desc": + // return string.Format("{0} DESC", r); + //case "Alias": + //case "As": + // return string.Format("{0} As {1}", r, + // GetQuotedColumnName(RemoveQuoteFromAlias(RemoveQuote(args[0].ToString())))); - //case "Desc": - // return string.Format("{0} DESC", r); - //case "Alias": - //case "As": - // return string.Format("{0} As {1}", r, - // GetQuotedColumnName(RemoveQuoteFromAlias(RemoveQuote(args[0].ToString())))); + case "SqlText": + if (m.Method.DeclaringType != typeof(SqlExtensionsStatics)) + { + goto default; + } - case "SqlText": - if (m.Method.DeclaringType != typeof(SqlExtensionsStatics)) - goto default; - if (m.Arguments.Count == 2) + if (m.Arguments.Count == 2) + { + var n1 = Visit(m.Arguments[0]); + Expression f = m.Arguments[1]; + if (!(f is Expression> fl)) { - var n1 = Visit(m.Arguments[0]); - var f = m.Arguments[1]; - if (!(f is Expression> fl)) - throw new NotSupportedException("Expression is not a proper lambda."); - var ff = fl.Compile(); - return ff(n1); + throw new NotSupportedException("Expression is not a proper lambda."); } - else if (m.Arguments.Count == 3) + + Func ff = fl.Compile(); + return ff(n1); + } + + if (m.Arguments.Count == 3) + { + var n1 = Visit(m.Arguments[0]); + var n2 = Visit(m.Arguments[1]); + Expression f = m.Arguments[2]; + if (!(f is Expression> fl)) { - var n1 = Visit(m.Arguments[0]); - var n2 = Visit(m.Arguments[1]); - var f = m.Arguments[2]; - if (!(f is Expression> fl)) - throw new NotSupportedException("Expression is not a proper lambda."); - var ff = fl.Compile(); - return ff(n1, n2); + throw new NotSupportedException("Expression is not a proper lambda."); } - else if (m.Arguments.Count == 4) + + Func ff = fl.Compile(); + return ff(n1, n2); + } + + if (m.Arguments.Count == 4) + { + var n1 = Visit(m.Arguments[0]); + var n2 = Visit(m.Arguments[1]); + var n3 = Visit(m.Arguments[3]); + Expression f = m.Arguments[3]; + if (!(f is Expression> fl)) { - var n1 = Visit(m.Arguments[0]); - var n2 = Visit(m.Arguments[1]); - var n3 = Visit(m.Arguments[3]); - var f = m.Arguments[3]; - if (!(f is Expression> fl)) - throw new NotSupportedException("Expression is not a proper lambda."); - var ff = fl.Compile(); - return ff(n1, n2, n3); - } - else throw new NotSupportedException("Expression is not a proper lambda."); + } - // c# 'x == null' becomes sql 'x IS NULL' which is fine - // c# 'x == y' becomes sql 'x = @0' which is fine - unless they are nullable types, - // because sql 'x = NULL' is always false and the 'IS NULL' syntax is required, - // so for comparing nullable types, we use x.SqlNullableEquals(y, fb) where fb is a fallback - // value which will be used when values are null - turning the comparison into - // sql 'COALESCE(x,fb) = COALESCE(y,fb)' - of course, fb must be a value outside - // of x and y range - and if that is not possible, then a manual comparison need - // to be written - // TODO: support SqlNullableEquals with 0 parameters, using the full syntax below - case "SqlNullableEquals": - var compareTo = Visit(m.Arguments[1]); - var fallback = Visit(m.Arguments[2]); - // that would work without a fallback value but is more cumbersome - //return Visited ? string.Empty : $"((({compareTo} is null) AND ({visitedMethodObject} is null)) OR (({compareTo} is not null) AND ({visitedMethodObject} = {compareTo})))"; - // use a fallback value - return Visited ? string.Empty : $"(COALESCE({visitedMethodObject},{fallback}) = COALESCE({compareTo},{fallback}))"; - - default: - - throw new ArgumentOutOfRangeException("No logic supported for " + m.Method.Name); - - //var s2 = new StringBuilder(); - //foreach (Object e in args) - //{ - // s2.AppendFormat(",{0}", GetQuotedValue(e, e.GetType())); - //} - //return string.Format("{0}({1}{2})", m.Method.Name, r, s2.ToString()); - } - } + Func ff = fl.Compile(); + return ff(n1, n2, n3); + } - public virtual string GetQuotedTableName(string tableName) - => GetQuotedName(tableName); + throw new NotSupportedException("Expression is not a proper lambda."); + + // c# 'x == null' becomes sql 'x IS NULL' which is fine + // c# 'x == y' becomes sql 'x = @0' which is fine - unless they are nullable types, + // because sql 'x = NULL' is always false and the 'IS NULL' syntax is required, + // so for comparing nullable types, we use x.SqlNullableEquals(y, fb) where fb is a fallback + // value which will be used when values are null - turning the comparison into + // sql 'COALESCE(x,fb) = COALESCE(y,fb)' - of course, fb must be a value outside + // of x and y range - and if that is not possible, then a manual comparison need + // to be written + // TODO: support SqlNullableEquals with 0 parameters, using the full syntax below + case "SqlNullableEquals": + var compareTo = Visit(m.Arguments[1]); + var fallback = Visit(m.Arguments[2]); + // that would work without a fallback value but is more cumbersome + //return Visited ? string.Empty : $"((({compareTo} is null) AND ({visitedMethodObject} is null)) OR (({compareTo} is not null) AND ({visitedMethodObject} = {compareTo})))"; + // use a fallback value + return Visited + ? string.Empty + : $"(COALESCE({visitedMethodObject},{fallback}) = COALESCE({compareTo},{fallback}))"; - public virtual string GetQuotedColumnName(string columnName) - => GetQuotedName(columnName); + default: - public virtual string GetQuotedName(string name) - => Visited ? name : "\"" + name + "\""; + throw new ArgumentOutOfRangeException("No logic supported for " + m.Method.Name); - protected string HandleStringComparison(string col, string val, string verb, TextColumnType columnType) - { - switch (verb) - { - case nameof(SqlExpressionExtensions.SqlWildcard): - SqlParameters.Add(RemoveQuote(val)!); - return Visited ? string.Empty : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); - - case "Equals": - case nameof(StringExtensions.InvariantEquals): - case nameof(SqlExpressionExtensions.SqlEquals): - SqlParameters.Add(RemoveQuote(val)!); - return Visited ? string.Empty : SqlSyntax.GetStringColumnEqualComparison(col, SqlParameters.Count - 1, columnType); - - case "StartsWith": - case nameof(StringExtensions.InvariantStartsWith): - case nameof(SqlExpressionExtensions.SqlStartsWith): - SqlParameters.Add(RemoveQuote(val) + SqlSyntax.GetWildcardPlaceholder()); - return Visited ? string.Empty : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); - - case "EndsWith": - case nameof(StringExtensions.InvariantEndsWith): - case nameof(SqlExpressionExtensions.SqlEndsWith): - SqlParameters.Add(SqlSyntax.GetWildcardPlaceholder() + RemoveQuote(val)); - return Visited ? string.Empty : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); - - case "Contains": - case nameof(StringExtensions.InvariantContains): - case nameof(SqlExpressionExtensions.SqlContains): - var wildcardPlaceholder = SqlSyntax.GetWildcardPlaceholder(); - SqlParameters.Add(wildcardPlaceholder + RemoveQuote(val) + wildcardPlaceholder); - return Visited ? string.Empty : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); - - default: - throw new ArgumentOutOfRangeException(nameof(verb)); - } + //var s2 = new StringBuilder(); + //foreach (Object e in args) + //{ + // s2.AppendFormat(",{0}", GetQuotedValue(e, e.GetType())); + //} + //return string.Format("{0}({1}{2})", m.Method.Name, r, s2.ToString()); } + } - public virtual string EscapeParam(object paramValue, ISqlSyntaxProvider sqlSyntax) - { - return paramValue == null - ? string.Empty - : sqlSyntax.EscapeString(paramValue.ToString()!); - } + public virtual string GetQuotedTableName(string tableName) + => GetQuotedName(tableName); + + public virtual string GetQuotedColumnName(string columnName) + => GetQuotedName(columnName); - protected virtual string? RemoveQuote(string? exp) + public virtual string GetQuotedName(string name) + => Visited ? name : "\"" + name + "\""; + + protected string HandleStringComparison(string col, string val, string verb, TextColumnType columnType) + { + switch (verb) { - if (exp.IsNullOrWhiteSpace()) return exp; + case nameof(SqlExpressionExtensions.SqlWildcard): + SqlParameters.Add(RemoveQuote(val)!); + return Visited + ? string.Empty + : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); + + case "Equals": + case nameof(StringExtensions.InvariantEquals): + case nameof(SqlExpressionExtensions.SqlEquals): + SqlParameters.Add(RemoveQuote(val)!); + return Visited + ? string.Empty + : SqlSyntax.GetStringColumnEqualComparison(col, SqlParameters.Count - 1, columnType); + + case "StartsWith": + case nameof(StringExtensions.InvariantStartsWith): + case nameof(SqlExpressionExtensions.SqlStartsWith): + SqlParameters.Add(RemoveQuote(val) + SqlSyntax.GetWildcardPlaceholder()); + return Visited + ? string.Empty + : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); - var c = exp![0]; - return (c == '"' || c == '`' || c == '\'') && exp[exp.Length - 1] == c - ? exp.Length == 1 + case "EndsWith": + case nameof(StringExtensions.InvariantEndsWith): + case nameof(SqlExpressionExtensions.SqlEndsWith): + SqlParameters.Add(SqlSyntax.GetWildcardPlaceholder() + RemoveQuote(val)); + return Visited + ? string.Empty + : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); + + case "Contains": + case nameof(StringExtensions.InvariantContains): + case nameof(SqlExpressionExtensions.SqlContains): + var wildcardPlaceholder = SqlSyntax.GetWildcardPlaceholder(); + SqlParameters.Add(wildcardPlaceholder + RemoveQuote(val) + wildcardPlaceholder); + return Visited ? string.Empty - : exp.Substring(1, exp.Length - 2) - : exp; + : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); + + default: + throw new ArgumentOutOfRangeException(nameof(verb)); } } + + public virtual string EscapeParam(object paramValue, ISqlSyntaxProvider sqlSyntax) => + paramValue == null + ? string.Empty + : sqlSyntax.EscapeString(paramValue.ToString()!); + + protected virtual string? RemoveQuote(string? exp) + { + if (exp.IsNullOrWhiteSpace()) + { + return exp; + } + + var c = exp![0]; + return (c == '"' || c == '`' || c == '\'') && exp[exp.Length - 1] == c + ? exp.Length == 1 + ? string.Empty + : exp.Substring(1, exp.Length - 2) + : exp; + } } diff --git a/src/Umbraco.Infrastructure/Persistence/Querying/ModelToSqlExpressionVisitor.cs b/src/Umbraco.Infrastructure/Persistence/Querying/ModelToSqlExpressionVisitor.cs index afe00a7fe907..0dfcc4146aea 100644 --- a/src/Umbraco.Infrastructure/Persistence/Querying/ModelToSqlExpressionVisitor.cs +++ b/src/Umbraco.Infrastructure/Persistence/Querying/ModelToSqlExpressionVisitor.cs @@ -1,140 +1,153 @@ -using System; -using System.Linq.Expressions; +using System.Linq.Expressions; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Querying +namespace Umbraco.Cms.Infrastructure.Persistence.Querying; + +/// +/// An expression tree parser to create SQL statements and SQL parameters based on a strongly typed expression, +/// based on Umbraco's business logic models. +/// +/// This object is stateful and cannot be re-used to parse an expression. +internal class ModelToSqlExpressionVisitor : ExpressionVisitorBase { - /// - /// An expression tree parser to create SQL statements and SQL parameters based on a strongly typed expression, - /// based on Umbraco's business logic models. - /// - /// This object is stateful and cannot be re-used to parse an expression. - internal class ModelToSqlExpressionVisitor : ExpressionVisitorBase + private readonly BaseMapper? _mapper; + private readonly IMapperCollection? _mappers; + + public ModelToSqlExpressionVisitor(ISqlSyntaxProvider sqlSyntax, IMapperCollection? mappers) + : base(sqlSyntax) { - private readonly IMapperCollection? _mappers; - private readonly BaseMapper? _mapper; + _mappers = mappers; + _mapper = mappers?[typeof(T)]; // throws if not found + } - public ModelToSqlExpressionVisitor(ISqlSyntaxProvider sqlSyntax, IMapperCollection? mappers) - : base(sqlSyntax) + protected override string VisitMemberAccess(MemberExpression? m) + { + if (m is null) { - _mappers = mappers; - _mapper = mappers?[typeof(T)]; // throws if not found + return string.Empty; } - protected override string VisitMemberAccess(MemberExpression? m) + if (m.Expression != null && + m.Expression.NodeType == ExpressionType.Parameter + && m.Expression.Type == typeof(T)) { - if (m is null) - { - return string.Empty; - } - if (m.Expression != null && - m.Expression.NodeType == ExpressionType.Parameter - && m.Expression.Type == typeof(T)) + //don't execute if compiled + if (Visited == false) { - //don't execute if compiled - if (Visited == false) + var field = _mapper?.Map(m.Member.Name); + if (field.IsNullOrWhiteSpace()) { - var field = _mapper?.Map(m.Member.Name); - if (field.IsNullOrWhiteSpace()) - throw new InvalidOperationException($"The mapper returned an empty field for the member name: {m.Member.Name} for type: {m.Expression.Type}."); - return field!; + throw new InvalidOperationException( + $"The mapper returned an empty field for the member name: {m.Member.Name} for type: {m.Expression.Type}."); } - //already compiled, return - return string.Empty; + return field!; } - if (m.Expression != null && m.Expression.NodeType == ExpressionType.Convert) + //already compiled, return + return string.Empty; + } + + if (m.Expression != null && m.Expression.NodeType == ExpressionType.Convert) + { + //don't execute if compiled + if (Visited == false) { - //don't execute if compiled - if (Visited == false) + var field = _mapper?.Map(m.Member.Name); + if (field.IsNullOrWhiteSpace()) { - var field = _mapper?.Map(m.Member.Name); - if (field.IsNullOrWhiteSpace()) - throw new InvalidOperationException($"The mapper returned an empty field for the member name: {m.Member.Name} for type: {m.Expression.Type}."); - return field!; + throw new InvalidOperationException( + $"The mapper returned an empty field for the member name: {m.Member.Name} for type: {m.Expression.Type}."); } - //already compiled, return - return string.Empty; + return field!; } - if (m.Expression != null - && m.Expression.Type != typeof(T) - && EndsWithConstant(m) == false - && _mappers is not null - && _mappers.TryGetMapper(m.Expression.Type, out var subMapper)) - { - //if this is the case, it means we have a sub expression / nested property access, such as: x.ContentType.Alias == "Test"; - //and since the sub type (x.ContentType) is not the same as x, we need to resolve a mapper for x.ContentType to get it's mapped SQL column - - //don't execute if compiled - if (Visited == false) - { - var field = subMapper.Map(m.Member.Name); - if (field.IsNullOrWhiteSpace()) - throw new InvalidOperationException($"The mapper returned an empty field for the member name: {m.Member.Name} for type: {m.Expression.Type}"); - return field; - } - //already compiled, return - return string.Empty; - } + //already compiled, return + return string.Empty; + } - // TODO: When m.Expression.NodeType == ExpressionType.Constant and it's an expression like: content => aliases.Contains(content.ContentType.Alias); - // then an SQL parameter will be added for aliases as an array, however in SqlIn on the subclass it will manually add these SqlParameters anyways, - // however the query will still execute because the SQL that is written will only contain the correct indexes of SQL parameters, this would be ignored, - // I'm just unsure right now due to time constraints how to make it correct. It won't matter right now and has been working already with this bug but I've - // only just discovered what it is actually doing. - - // TODO - // in most cases we want to convert the value to a plain object, - // but for in some rare cases, we may want to do it differently, - // for instance a Models.AuditType (an enum) may in some cases - // need to be converted to its string value. - // but - we cannot have specific code here, really - and how would - // we configure this? is it even possible? - /* - var toString = typeof(object).GetMethod("ToString"); - var member = Expression.Call(m, toString); - */ - var member = Expression.Convert(m, typeof(object)); - var lambda = Expression.Lambda>(member); - var getter = lambda.Compile(); - var o = getter(); - - SqlParameters.Add(o); + if (m.Expression != null + && m.Expression.Type != typeof(T) + && EndsWithConstant(m) == false + && _mappers is not null + && _mappers.TryGetMapper(m.Expression.Type, out BaseMapper? subMapper)) + { + //if this is the case, it means we have a sub expression / nested property access, such as: x.ContentType.Alias == "Test"; + //and since the sub type (x.ContentType) is not the same as x, we need to resolve a mapper for x.ContentType to get it's mapped SQL column //don't execute if compiled if (Visited == false) - return $"@{SqlParameters.Count - 1}"; + { + var field = subMapper.Map(m.Member.Name); + if (field.IsNullOrWhiteSpace()) + { + throw new InvalidOperationException( + $"The mapper returned an empty field for the member name: {m.Member.Name} for type: {m.Expression.Type}"); + } + + return field; + } //already compiled, return return string.Empty; } - /// - /// Determines if the MemberExpression ends in a Constant value - /// - /// - /// - private static bool EndsWithConstant(MemberExpression m) + // TODO: When m.Expression.NodeType == ExpressionType.Constant and it's an expression like: content => aliases.Contains(content.ContentType.Alias); + // then an SQL parameter will be added for aliases as an array, however in SqlIn on the subclass it will manually add these SqlParameters anyways, + // however the query will still execute because the SQL that is written will only contain the correct indexes of SQL parameters, this would be ignored, + // I'm just unsure right now due to time constraints how to make it correct. It won't matter right now and has been working already with this bug but I've + // only just discovered what it is actually doing. + + // TODO + // in most cases we want to convert the value to a plain object, + // but for in some rare cases, we may want to do it differently, + // for instance a Models.AuditType (an enum) may in some cases + // need to be converted to its string value. + // but - we cannot have specific code here, really - and how would + // we configure this? is it even possible? + /* + var toString = typeof(object).GetMethod("ToString"); + var member = Expression.Call(m, toString); + */ + UnaryExpression member = Expression.Convert(m, typeof(object)); + var lambda = Expression.Lambda>(member); + Func getter = lambda.Compile(); + var o = getter(); + + SqlParameters.Add(o); + + //don't execute if compiled + if (Visited == false) { - Expression? expr = m; + return $"@{SqlParameters.Count - 1}"; + } - while (expr is MemberExpression) - { - var memberExpr = expr as MemberExpression; - if (memberExpr is not null) - { - expr = memberExpr.Expression; - } + //already compiled, return + return string.Empty; + } - } + /// + /// Determines if the MemberExpression ends in a Constant value + /// + /// + /// + private static bool EndsWithConstant(MemberExpression m) + { + Expression? expr = m; - var constExpr = expr as ConstantExpression; - return constExpr != null; + while (expr is MemberExpression) + { + var memberExpr = expr as MemberExpression; + if (memberExpr is not null) + { + expr = memberExpr.Expression; + } } + + var constExpr = expr as ConstantExpression; + return constExpr != null; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Querying/PocoToSqlExpressionVisitor.cs b/src/Umbraco.Infrastructure/Persistence/Querying/PocoToSqlExpressionVisitor.cs index 87d758ebda0d..727dff3015e5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Querying/PocoToSqlExpressionVisitor.cs +++ b/src/Umbraco.Infrastructure/Persistence/Querying/PocoToSqlExpressionVisitor.cs @@ -1,283 +1,310 @@ -using System; -using System.Linq; -using System.Linq.Expressions; +using System.Linq.Expressions; +using System.Reflection; using NPoco; -namespace Umbraco.Cms.Infrastructure.Persistence.Querying +namespace Umbraco.Cms.Infrastructure.Persistence.Querying; + +/// +/// Represents an expression tree parser used to turn strongly typed expressions into SQL statements. +/// +/// The type of the DTO. +/// This visitor is stateful and cannot be reused. +internal class PocoToSqlExpressionVisitor : ExpressionVisitorBase { - /// - /// Represents an expression tree parser used to turn strongly typed expressions into SQL statements. - /// - /// The type of the DTO. - /// This visitor is stateful and cannot be reused. - internal class PocoToSqlExpressionVisitor : ExpressionVisitorBase + private readonly string? _alias; + private readonly PocoData _pd; + + public PocoToSqlExpressionVisitor(ISqlContext sqlContext, string? alias) + : base(sqlContext.SqlSyntax) { - private readonly PocoData _pd; - private readonly string? _alias; + _pd = sqlContext.PocoDataFactory.ForType(typeof(TDto)); + _alias = alias; + } - public PocoToSqlExpressionVisitor(ISqlContext sqlContext, string? alias) - : base(sqlContext.SqlSyntax) + protected override string VisitMethodCall(MethodCallExpression? m) + { + if (m is null) { - _pd = sqlContext.PocoDataFactory.ForType(typeof(TDto)); - _alias = alias; + return string.Empty; } - protected override string VisitMethodCall(MethodCallExpression? m) + Type? declaring = m.Method.DeclaringType; + if (declaring != typeof(SqlTemplate)) { - if (m is null) - { - return string.Empty; - } - var declaring = m.Method.DeclaringType; - if (declaring != typeof (SqlTemplate)) - return base.VisitMethodCall(m); + return base.VisitMethodCall(m); + } - if (m.Method.Name != "Arg" && m.Method.Name != "ArgIn") - throw new NotSupportedException($"Method SqlTemplate.{m.Method.Name} is not supported."); + if (m.Method.Name != "Arg" && m.Method.Name != "ArgIn") + { + throw new NotSupportedException($"Method SqlTemplate.{m.Method.Name} is not supported."); + } - var parameters = m.Method.GetParameters(); - if (parameters.Length != 1 || parameters[0].ParameterType != typeof (string)) - throw new NotSupportedException($"Method SqlTemplate.{m.Method.Name}({string.Join(", ", parameters.Select(x => x.ParameterType))} is not supported."); + ParameterInfo[] parameters = m.Method.GetParameters(); + if (parameters.Length != 1 || parameters[0].ParameterType != typeof(string)) + { + throw new NotSupportedException( + $"Method SqlTemplate.{m.Method.Name}({string.Join(", ", parameters.Select(x => x.ParameterType))} is not supported."); + } - var arg = m.Arguments[0]; - string? name; - if (arg.NodeType == ExpressionType.Constant) - { - name = arg.ToString(); - } - else - { - // though... we probably should avoid doing this - var member = Expression.Convert(arg, typeof (object)); - var lambda = Expression.Lambda>(member); - var getter = lambda.Compile(); - name = getter().ToString(); - } + Expression arg = m.Arguments[0]; + string? name; + if (arg.NodeType == ExpressionType.Constant) + { + name = arg.ToString(); + } + else + { + // though... we probably should avoid doing this + UnaryExpression member = Expression.Convert(arg, typeof(object)); + var lambda = Expression.Lambda>(member); + Func getter = lambda.Compile(); + name = getter().ToString(); + } + + SqlParameters.Add(new SqlTemplate.TemplateArg(RemoveQuote(name))); - SqlParameters.Add(new SqlTemplate.TemplateArg(RemoveQuote(name))); + return Visited + ? string.Empty + : $"@{SqlParameters.Count - 1}"; + } - return Visited - ? string.Empty - : $"@{SqlParameters.Count - 1}"; + protected override string VisitMemberAccess(MemberExpression? m) + { + if (m is null) + { + return string.Empty; } - protected override string VisitMemberAccess(MemberExpression? m) + if (m.Expression != null && m.Expression.NodeType == ExpressionType.Parameter && + m.Expression.Type == typeof(TDto)) { - if (m is null) - { - return string.Empty; - } - if (m.Expression != null && m.Expression.NodeType == ExpressionType.Parameter && m.Expression.Type == typeof(TDto)) - { - return Visited ? string.Empty : GetFieldName(_pd, m.Member.Name, _alias); - } + return Visited ? string.Empty : GetFieldName(_pd, m.Member.Name, _alias); + } - if (m.Expression != null && m.Expression.NodeType == ExpressionType.Convert) - { - return Visited ? string.Empty : GetFieldName(_pd, m.Member.Name, _alias); - } + if (m.Expression != null && m.Expression.NodeType == ExpressionType.Convert) + { + return Visited ? string.Empty : GetFieldName(_pd, m.Member.Name, _alias); + } - var member = Expression.Convert(m, typeof(object)); - var lambda = Expression.Lambda>(member); - var getter = lambda.Compile(); - var o = getter(); + UnaryExpression member = Expression.Convert(m, typeof(object)); + var lambda = Expression.Lambda>(member); + Func getter = lambda.Compile(); + var o = getter(); - SqlParameters.Add(o); + SqlParameters.Add(o); - return Visited ? string.Empty : "@" + (SqlParameters.Count - 1); - } + return Visited ? string.Empty : "@" + (SqlParameters.Count - 1); + } - protected virtual string GetFieldName(PocoData pocoData, string name, string? alias) - { - var column = pocoData.Columns.FirstOrDefault(x => x.Value.MemberInfoData.Name == name); - var tableName = SqlSyntax.GetQuotedTableName(alias ?? pocoData.TableInfo.TableName); - var columnName = SqlSyntax.GetQuotedColumnName(column.Value.ColumnName); + protected virtual string GetFieldName(PocoData pocoData, string name, string? alias) + { + KeyValuePair column = + pocoData.Columns.FirstOrDefault(x => x.Value.MemberInfoData.Name == name); + var tableName = SqlSyntax.GetQuotedTableName(alias ?? pocoData.TableInfo.TableName); + var columnName = SqlSyntax.GetQuotedColumnName(column.Value.ColumnName); - return tableName + "." + columnName; - } + return tableName + "." + columnName; } +} - /// - /// Represents an expression tree parser used to turn strongly typed expressions into SQL statements. - /// - /// The type of DTO 1. - /// The type of DTO 2. - /// This visitor is stateful and cannot be reused. - internal class PocoToSqlExpressionVisitor : ExpressionVisitorBase +/// +/// Represents an expression tree parser used to turn strongly typed expressions into SQL statements. +/// +/// The type of DTO 1. +/// The type of DTO 2. +/// This visitor is stateful and cannot be reused. +internal class PocoToSqlExpressionVisitor : ExpressionVisitorBase +{ + private readonly string? _alias1, _alias2; + private readonly PocoData _pocoData1, _pocoData2; + private string? _parameterName1, _parameterName2; + + public PocoToSqlExpressionVisitor(ISqlContext sqlContext, string? alias1, string? alias2) + : base(sqlContext.SqlSyntax) { - private readonly PocoData _pocoData1, _pocoData2; - private readonly string? _alias1, _alias2; - private string? _parameterName1, _parameterName2; + _pocoData1 = sqlContext.PocoDataFactory.ForType(typeof(TDto1)); + _pocoData2 = sqlContext.PocoDataFactory.ForType(typeof(TDto2)); + _alias1 = alias1; + _alias2 = alias2; + } - public PocoToSqlExpressionVisitor(ISqlContext sqlContext, string? alias1, string? alias2) - : base(sqlContext.SqlSyntax) + protected override string VisitLambda(LambdaExpression? lambda) + { + if (lambda is null) { - _pocoData1 = sqlContext.PocoDataFactory.ForType(typeof (TDto1)); - _pocoData2 = sqlContext.PocoDataFactory.ForType(typeof (TDto2)); - _alias1 = alias1; - _alias2 = alias2; + return string.Empty; } - protected override string VisitLambda(LambdaExpression? lambda) + if (lambda.Parameters.Count == 2) { - if (lambda is null) - { - return string.Empty; - } - if (lambda.Parameters.Count == 2) - { - _parameterName1 = lambda.Parameters[0].Name; - _parameterName2 = lambda.Parameters[1].Name; - } - else - { - _parameterName1 = _parameterName2 = null; - } - return base.VisitLambda(lambda); + _parameterName1 = lambda.Parameters[0].Name; + _parameterName2 = lambda.Parameters[1].Name; } + else + { + _parameterName1 = _parameterName2 = null; + } + + return base.VisitLambda(lambda); + } - protected override string VisitMemberAccess(MemberExpression? m) + protected override string VisitMemberAccess(MemberExpression? m) + { + if (m is null) { - if (m is null) - { - return string.Empty; - } - if (m.Expression != null) - { - if (m.Expression.NodeType == ExpressionType.Parameter) - { - var pex = (ParameterExpression) m.Expression; + return string.Empty; + } - if (pex.Name == _parameterName1) - return Visited ? string.Empty : GetFieldName(_pocoData1, m.Member.Name, _alias1); + if (m.Expression != null) + { + if (m.Expression.NodeType == ExpressionType.Parameter) + { + var pex = (ParameterExpression)m.Expression; - if (pex.Name == _parameterName2) - return Visited ? string.Empty : GetFieldName(_pocoData2, m.Member.Name, _alias2); + if (pex.Name == _parameterName1) + { + return Visited ? string.Empty : GetFieldName(_pocoData1, m.Member.Name, _alias1); } - else if (m.Expression.NodeType == ExpressionType.Convert) + + if (pex.Name == _parameterName2) { - // here: which _pd should we use?! - throw new NotSupportedException(); - //return Visited ? string.Empty : GetFieldName(_pd, m.Member.Name); + return Visited ? string.Empty : GetFieldName(_pocoData2, m.Member.Name, _alias2); } } + else if (m.Expression.NodeType == ExpressionType.Convert) + { + // here: which _pd should we use?! + throw new NotSupportedException(); + //return Visited ? string.Empty : GetFieldName(_pd, m.Member.Name); + } + } - var member = Expression.Convert(m, typeof (object)); - var lambda = Expression.Lambda>(member); - var getter = lambda.Compile(); - var o = getter(); + UnaryExpression member = Expression.Convert(m, typeof(object)); + var lambda = Expression.Lambda>(member); + Func getter = lambda.Compile(); + var o = getter(); - SqlParameters.Add(o); + SqlParameters.Add(o); - // execute if not already compiled - return Visited ? string.Empty : "@" + (SqlParameters.Count - 1); - } + // execute if not already compiled + return Visited ? string.Empty : "@" + (SqlParameters.Count - 1); + } - protected virtual string GetFieldName(PocoData pocoData, string name, string? alias) - { - var column = pocoData.Columns.FirstOrDefault(x => x.Value.MemberInfoData.Name == name); - var tableName = SqlSyntax.GetQuotedTableName(alias ?? pocoData.TableInfo.TableName); - var columnName = SqlSyntax.GetQuotedColumnName(column.Value.ColumnName); + protected virtual string GetFieldName(PocoData pocoData, string name, string? alias) + { + KeyValuePair column = + pocoData.Columns.FirstOrDefault(x => x.Value.MemberInfoData.Name == name); + var tableName = SqlSyntax.GetQuotedTableName(alias ?? pocoData.TableInfo.TableName); + var columnName = SqlSyntax.GetQuotedColumnName(column.Value.ColumnName); - return tableName + "." + columnName; - } + return tableName + "." + columnName; } +} - /// - /// Represents an expression tree parser used to turn strongly typed expressions into SQL statements. - /// - /// The type of DTO 1. - /// The type of DTO 2. - /// The type of DTO 3. - /// This visitor is stateful and cannot be reused. - internal class PocoToSqlExpressionVisitor : ExpressionVisitorBase +/// +/// Represents an expression tree parser used to turn strongly typed expressions into SQL statements. +/// +/// The type of DTO 1. +/// The type of DTO 2. +/// The type of DTO 3. +/// This visitor is stateful and cannot be reused. +internal class PocoToSqlExpressionVisitor : ExpressionVisitorBase +{ + private readonly string? _alias1, _alias2, _alias3; + private readonly PocoData _pocoData1, _pocoData2, _pocoData3; + private string? _parameterName1, _parameterName2, _parameterName3; + + public PocoToSqlExpressionVisitor(ISqlContext sqlContext, string? alias1, string? alias2, string? alias3) + : base(sqlContext.SqlSyntax) { - private readonly PocoData _pocoData1, _pocoData2, _pocoData3; - private readonly string? _alias1, _alias2, _alias3; - private string? _parameterName1, _parameterName2, _parameterName3; + _pocoData1 = sqlContext.PocoDataFactory.ForType(typeof(TDto1)); + _pocoData2 = sqlContext.PocoDataFactory.ForType(typeof(TDto2)); + _pocoData3 = sqlContext.PocoDataFactory.ForType(typeof(TDto3)); + _alias1 = alias1; + _alias2 = alias2; + _alias3 = alias3; + } - public PocoToSqlExpressionVisitor(ISqlContext sqlContext, string? alias1, string? alias2, string? alias3) - : base(sqlContext.SqlSyntax) + protected override string VisitLambda(LambdaExpression? lambda) + { + if (lambda is null) { - _pocoData1 = sqlContext.PocoDataFactory.ForType(typeof(TDto1)); - _pocoData2 = sqlContext.PocoDataFactory.ForType(typeof(TDto2)); - _pocoData3 = sqlContext.PocoDataFactory.ForType(typeof(TDto3)); - _alias1 = alias1; - _alias2 = alias2; - _alias3 = alias3; + return string.Empty; } - protected override string VisitLambda(LambdaExpression? lambda) + if (lambda.Parameters.Count == 3) { - if (lambda is null) - { - return string.Empty; - } - if (lambda.Parameters.Count == 3) - { - _parameterName1 = lambda.Parameters[0].Name; - _parameterName2 = lambda.Parameters[1].Name; - _parameterName3 = lambda.Parameters[2].Name; - } - else if (lambda.Parameters.Count == 2) - { - _parameterName1 = lambda.Parameters[0].Name; - _parameterName2 = lambda.Parameters[1].Name; - } - else - { - _parameterName1 = _parameterName2 = null; - } - return base.VisitLambda(lambda); + _parameterName1 = lambda.Parameters[0].Name; + _parameterName2 = lambda.Parameters[1].Name; + _parameterName3 = lambda.Parameters[2].Name; + } + else if (lambda.Parameters.Count == 2) + { + _parameterName1 = lambda.Parameters[0].Name; + _parameterName2 = lambda.Parameters[1].Name; } + else + { + _parameterName1 = _parameterName2 = null; + } + + return base.VisitLambda(lambda); + } - protected override string VisitMemberAccess(MemberExpression? m) + protected override string VisitMemberAccess(MemberExpression? m) + { + if (m is null) { - if (m is null) - { - return string.Empty; - } - if (m.Expression != null) - { - if (m.Expression.NodeType == ExpressionType.Parameter) - { - var pex = (ParameterExpression)m.Expression; + return string.Empty; + } - if (pex.Name == _parameterName1) - return Visited ? string.Empty : GetFieldName(_pocoData1, m.Member.Name, _alias1); + if (m.Expression != null) + { + if (m.Expression.NodeType == ExpressionType.Parameter) + { + var pex = (ParameterExpression)m.Expression; - if (pex.Name == _parameterName2) - return Visited ? string.Empty : GetFieldName(_pocoData2, m.Member.Name, _alias2); + if (pex.Name == _parameterName1) + { + return Visited ? string.Empty : GetFieldName(_pocoData1, m.Member.Name, _alias1); + } - if (pex.Name == _parameterName3) - return Visited ? string.Empty : GetFieldName(_pocoData3, m.Member.Name, _alias3); + if (pex.Name == _parameterName2) + { + return Visited ? string.Empty : GetFieldName(_pocoData2, m.Member.Name, _alias2); } - else if (m.Expression.NodeType == ExpressionType.Convert) + + if (pex.Name == _parameterName3) { - // here: which _pd should we use?! - throw new NotSupportedException(); - //return Visited ? string.Empty : GetFieldName(_pd, m.Member.Name); + return Visited ? string.Empty : GetFieldName(_pocoData3, m.Member.Name, _alias3); } } + else if (m.Expression.NodeType == ExpressionType.Convert) + { + // here: which _pd should we use?! + throw new NotSupportedException(); + //return Visited ? string.Empty : GetFieldName(_pd, m.Member.Name); + } + } - var member = Expression.Convert(m, typeof(object)); - var lambda = Expression.Lambda>(member); - var getter = lambda.Compile(); - var o = getter(); + UnaryExpression member = Expression.Convert(m, typeof(object)); + var lambda = Expression.Lambda>(member); + Func getter = lambda.Compile(); + var o = getter(); - SqlParameters.Add(o); + SqlParameters.Add(o); - // execute if not already compiled - return Visited ? string.Empty : "@" + (SqlParameters.Count - 1); - } + // execute if not already compiled + return Visited ? string.Empty : "@" + (SqlParameters.Count - 1); + } - protected virtual string GetFieldName(PocoData pocoData, string name, string? alias) - { - var column = pocoData.Columns.FirstOrDefault(x => x.Value.MemberInfoData.Name == name); - var tableName = SqlSyntax.GetQuotedTableName(alias ?? pocoData.TableInfo.TableName); - var columnName = SqlSyntax.GetQuotedColumnName(column.Value.ColumnName); + protected virtual string GetFieldName(PocoData pocoData, string name, string? alias) + { + KeyValuePair column = + pocoData.Columns.FirstOrDefault(x => x.Value.MemberInfoData.Name == name); + var tableName = SqlSyntax.GetQuotedTableName(alias ?? pocoData.TableInfo.TableName); + var columnName = SqlSyntax.GetQuotedColumnName(column.Value.ColumnName); - return tableName + "." + columnName; - } + return tableName + "." + columnName; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Querying/Query.cs b/src/Umbraco.Infrastructure/Persistence/Querying/Query.cs index f4535f9734a3..767cffa267d7 100644 --- a/src/Umbraco.Infrastructure/Persistence/Querying/Query.cs +++ b/src/Umbraco.Infrastructure/Persistence/Querying/Query.cs @@ -1,100 +1,103 @@ -using System; -using System.Collections; -using System.Collections.Generic; +using System.Collections; using System.Linq.Expressions; using System.Text; using NPoco; using Umbraco.Cms.Core.Persistence.Querying; -namespace Umbraco.Cms.Infrastructure.Persistence.Querying +namespace Umbraco.Cms.Infrastructure.Persistence.Querying; + +/// +/// Represents a query builder. +/// +/// A query builder translates Linq queries into Sql queries. +public class Query : IQuery { + private readonly ISqlContext _sqlContext; + private readonly List> _wheres = new(); + + public Query(ISqlContext sqlContext) => _sqlContext = sqlContext; + /// - /// Represents a query builder. + /// Adds a where clause to the query. /// - /// A query builder translates Linq queries into Sql queries. - public class Query : IQuery + public virtual IQuery Where(Expression> predicate) { - private readonly ISqlContext _sqlContext; - private readonly List> _wheres = new List>(); - - public Query(ISqlContext sqlContext) + if (predicate == null) { - _sqlContext = sqlContext; + return this; } - /// - /// Adds a where clause to the query. - /// - public virtual IQuery Where(Expression> predicate) - { - if (predicate == null) return this; + var expressionHelper = new ModelToSqlExpressionVisitor(_sqlContext.SqlSyntax, _sqlContext.Mappers); + var whereExpression = expressionHelper.Visit(predicate); + _wheres.Add(new Tuple(whereExpression, expressionHelper.GetSqlParameters())); + return this; + } - var expressionHelper = new ModelToSqlExpressionVisitor(_sqlContext.SqlSyntax, _sqlContext.Mappers); - var whereExpression = expressionHelper.Visit(predicate); - _wheres.Add(new Tuple(whereExpression, expressionHelper.GetSqlParameters())); + /// + /// Adds a where-in clause to the query. + /// + public virtual IQuery WhereIn(Expression> fieldSelector, IEnumerable? values) + { + if (fieldSelector == null) + { return this; } - /// - /// Adds a where-in clause to the query. - /// - public virtual IQuery WhereIn(Expression> fieldSelector, IEnumerable? values) - { - if (fieldSelector == null) return this; + var expressionHelper = new ModelToSqlExpressionVisitor(_sqlContext.SqlSyntax, _sqlContext.Mappers); + var whereExpression = expressionHelper.Visit(fieldSelector); + _wheres.Add(new Tuple(whereExpression + " IN (@values)", new object[] {new {values}})); + return this; + } - var expressionHelper = new ModelToSqlExpressionVisitor(_sqlContext.SqlSyntax, _sqlContext.Mappers); - var whereExpression = expressionHelper.Visit(fieldSelector); - _wheres.Add(new Tuple(whereExpression + " IN (@values)", new object[] { new { values } })); + /// + /// Adds a set of OR-ed where clauses to the query. + /// + public virtual IQuery WhereAny(IEnumerable>> predicates) + { + if (predicates == null) + { return this; } - /// - /// Adds a set of OR-ed where clauses to the query. - /// - public virtual IQuery WhereAny(IEnumerable>> predicates) + StringBuilder? sb = null; + List? parameters = null; + Sql? sql = null; + foreach (Expression> predicate in predicates) { - if (predicates == null) return this; + // see notes in Where() + var expressionHelper = new ModelToSqlExpressionVisitor(_sqlContext.SqlSyntax, _sqlContext.Mappers); + var whereExpression = expressionHelper.Visit(predicate); - StringBuilder? sb = null; - List? parameters = null; - Sql? sql = null; - foreach (var predicate in predicates) + if (sb == null) { - // see notes in Where() - var expressionHelper = new ModelToSqlExpressionVisitor(_sqlContext.SqlSyntax, _sqlContext.Mappers); - var whereExpression = expressionHelper.Visit(predicate); - - if (sb == null) - { - sb = new StringBuilder("("); - parameters = new List(); - sql = Sql.BuilderFor(_sqlContext); - } - else - { - sb.Append(" OR "); - sql?.Append(" OR "); - } - - sb.Append(whereExpression); - parameters?.AddRange(expressionHelper.GetSqlParameters()); - sql?.Append(whereExpression, expressionHelper.GetSqlParameters()); + sb = new StringBuilder("("); + parameters = new List(); + sql = Sql.BuilderFor(_sqlContext); + } + else + { + sb.Append(" OR "); + sql?.Append(" OR "); } - if (sb == null) return this; - - sb.Append(")"); - _wheres.Add(Tuple.Create("(" + sql?.SQL + ")", sql?.Arguments)!); - - return this; + sb.Append(whereExpression); + parameters?.AddRange(expressionHelper.GetSqlParameters()); + sql?.Append(whereExpression, expressionHelper.GetSqlParameters()); } - /// - /// Returns all translated where clauses and their sql parameters - /// - public IEnumerable> GetWhereClauses() + if (sb == null) { - return _wheres; + return this; } + + sb.Append(")"); + _wheres.Add(Tuple.Create("(" + sql?.SQL + ")", sql?.Arguments)!); + + return this; } + + /// + /// Returns all translated where clauses and their sql parameters + /// + public IEnumerable> GetWhereClauses() => _wheres; } diff --git a/src/Umbraco.Infrastructure/Persistence/Querying/QueryExtensions.cs b/src/Umbraco.Infrastructure/Persistence/Querying/QueryExtensions.cs index 6abb97a5546e..9be612538967 100644 --- a/src/Umbraco.Infrastructure/Persistence/Querying/QueryExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/Querying/QueryExtensions.cs @@ -1,28 +1,27 @@ -using System; -using System.Collections.Generic; -using Umbraco.Cms.Core.Persistence.Querying; +using Umbraco.Cms.Core.Persistence.Querying; -namespace Umbraco.Cms.Infrastructure.Persistence.Querying +namespace Umbraco.Cms.Infrastructure.Persistence.Querying; + +/// +/// SD: This is a horrible hack but unless we break compatibility with anyone who's actually implemented IQuery{T} +/// there's not much we can do. +/// The IQuery{T} interface is useless without having a GetWhereClauses method and cannot be used for tests. +/// We have to wait till v8 to make this change I suppose. +/// +internal static class QueryExtensions { /// - /// SD: This is a horrible hack but unless we break compatibility with anyone who's actually implemented IQuery{T} there's not much we can do. - /// The IQuery{T} interface is useless without having a GetWhereClauses method and cannot be used for tests. - /// We have to wait till v8 to make this change I suppose. + /// Returns all translated where clauses and their sql parameters /// - internal static class QueryExtensions + /// + public static IEnumerable> GetWhereClauses(this IQuery query) { - /// - /// Returns all translated where clauses and their sql parameters - /// - /// - public static IEnumerable> GetWhereClauses(this IQuery query) + var q = query as Query; + if (q == null) { - var q = query as Query; - if (q == null) - { - throw new NotSupportedException(typeof(IQuery) + " cannot be cast to " + typeof(Query)); - } - return q.GetWhereClauses(); + throw new NotSupportedException(typeof(IQuery) + " cannot be cast to " + typeof(Query)); } + + return q.GetWhereClauses(); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Querying/SqlTranslator.cs b/src/Umbraco.Infrastructure/Persistence/Querying/SqlTranslator.cs index 85ccbd02ee64..26a4c42bee28 100644 --- a/src/Umbraco.Infrastructure/Persistence/Querying/SqlTranslator.cs +++ b/src/Umbraco.Infrastructure/Persistence/Querying/SqlTranslator.cs @@ -1,35 +1,29 @@ -using System; using NPoco; using Umbraco.Cms.Core.Persistence.Querying; -namespace Umbraco.Cms.Infrastructure.Persistence.Querying +namespace Umbraco.Cms.Infrastructure.Persistence.Querying; + +/// +/// Represents the Sql Translator for translating a IQuery object to Sql +/// +/// +public class SqlTranslator { - /// - /// Represents the Sql Translator for translating a IQuery object to Sql - /// - /// - public class SqlTranslator - { - private readonly Sql _sql; + private readonly Sql _sql; - public SqlTranslator(Sql sql, IQuery? query) + public SqlTranslator(Sql sql, IQuery? query) + { + _sql = sql ?? throw new ArgumentNullException(nameof(sql)); + if (query is not null) { - _sql = sql ?? throw new ArgumentNullException(nameof(sql)); - if (query is not null) + foreach (Tuple clause in query.GetWhereClauses()) { - foreach (var clause in query.GetWhereClauses()) - _sql.Where(clause.Item1, clause.Item2); + _sql.Where(clause.Item1, clause.Item2); } } + } - public Sql Translate() - { - return _sql; - } + public Sql Translate() => _sql; - public override string ToString() - { - return _sql.SQL; - } - } + public override string ToString() => _sql.SQL; } diff --git a/src/Umbraco.Infrastructure/Persistence/RecordPersistenceType.cs b/src/Umbraco.Infrastructure/Persistence/RecordPersistenceType.cs index 3162f58d1e27..6547509a2d3e 100644 --- a/src/Umbraco.Infrastructure/Persistence/RecordPersistenceType.cs +++ b/src/Umbraco.Infrastructure/Persistence/RecordPersistenceType.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +public enum RecordPersistenceType { - public enum RecordPersistenceType - { - Insert, - Update, - Delete - } + Insert, + Update, + Delete } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/IEntityRepositoryExtended.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/IEntityRepositoryExtended.cs index ac07cd19dd27..ad5de29d6386 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/IEntityRepositoryExtended.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/IEntityRepositoryExtended.cs @@ -1,34 +1,32 @@ -using System; -using System.Collections.Generic; using NPoco; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories; + +public interface IEntityRepositoryExtended : IEntityRepository { - public interface IEntityRepositoryExtended : IEntityRepository - { - /// - /// Gets paged entities for a query and a subset of object types - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// A callback providing the ability to customize the generated SQL used to retrieve entities - /// - /// - /// A collection of mixed entity types which would be of type , , , - /// - /// - IEnumerable GetPagedResultsByQuery( - IQuery query, Guid[] objectTypes, long pageIndex, int pageSize, out long totalRecords, - IQuery? filter, Ordering? ordering, Action>? sqlCustomization = null); - } + /// + /// Gets paged entities for a query and a subset of object types + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// A callback providing the ability to customize the generated SQL used to retrieve entities + /// + /// + /// A collection of mixed entity types which would be of type , + /// , , + /// + /// + IEnumerable GetPagedResultsByQuery( + IQuery query, Guid[] objectTypes, long pageIndex, int pageSize, out long totalRecords, + IQuery? filter, Ordering? ordering, Action>? sqlCustomization = null); } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditEntryRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditEntryRepository.cs index b94f99723af4..eab408823ee2 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditEntryRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditEntryRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -8,125 +5,123 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents the NPoco implementation of . +/// +internal class AuditEntryRepository : EntityRepositoryBase, IAuditEntryRepository { /// - /// Represents the NPoco implementation of . + /// Initializes a new instance of the class. /// - internal class AuditEntryRepository : EntityRepositoryBase, IAuditEntryRepository + public AuditEntryRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) { - /// - /// Initializes a new instance of the class. - /// - public AuditEntryRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { - } + } - /// - public IEnumerable GetPage(long pageIndex, int pageCount, out long records) + /// + public IEnumerable GetPage(long pageIndex, int pageCount, out long records) + { + Sql sql = Sql() + .Select() + .From() + .OrderByDescending(x => x.EventDateUtc); + + Page page = Database.Page(pageIndex + 1, pageCount, sql); + records = page.TotalItems; + return page.Items.Select(AuditEntryFactory.BuildEntity); + } + + /// + public bool IsAvailable() + { + var tables = SqlSyntax.GetTablesInSchema(Database).ToArray(); + return tables.InvariantContains(Constants.DatabaseSchema.Tables.AuditEntry); + } + + /// + protected override IAuditEntry? PerformGet(int id) + { + Sql sql = Sql() + .Select() + .From() + .Where(x => x.Id == id); + + AuditEntryDto dto = Database.FirstOrDefault(sql); + return dto == null ? null : AuditEntryFactory.BuildEntity(dto); + } + + /// + protected override IEnumerable PerformGetAll(params int[]? ids) + { + if (ids?.Length == 0) { Sql sql = Sql() .Select() - .From() - .OrderByDescending(x => x.EventDateUtc); + .From(); - Page page = Database.Page(pageIndex + 1, pageCount, sql); - records = page.TotalItems; - return page.Items.Select(AuditEntryFactory.BuildEntity); + return Database.Fetch(sql).Select(AuditEntryFactory.BuildEntity); } - /// - public bool IsAvailable() - { - var tables = SqlSyntax.GetTablesInSchema(Database).ToArray(); - return tables.InvariantContains(Constants.DatabaseSchema.Tables.AuditEntry); - } + var entries = new List(); - /// - protected override IAuditEntry? PerformGet(int id) + foreach (IEnumerable group in ids.InGroupsOf(Constants.Sql.MaxParameterCount)) { Sql sql = Sql() .Select() .From() - .Where(x => x.Id == id); + .WhereIn(x => x.Id, group); - AuditEntryDto dto = Database.FirstOrDefault(sql); - return dto == null ? null : AuditEntryFactory.BuildEntity(dto); + entries.AddRange(Database.Fetch(sql).Select(AuditEntryFactory.BuildEntity)); } - /// - protected override IEnumerable PerformGetAll(params int[]? ids) - { - if (ids?.Length == 0) - { - Sql sql = Sql() - .Select() - .From(); - - return Database.Fetch(sql).Select(AuditEntryFactory.BuildEntity); - } - - var entries = new List(); - - foreach (IEnumerable group in ids.InGroupsOf(Constants.Sql.MaxParameterCount)) - { - Sql sql = Sql() - .Select() - .From() - .WhereIn(x => x.Id, group); - - entries.AddRange(Database.Fetch(sql).Select(AuditEntryFactory.BuildEntity)); - } - - return entries; - } - - /// - protected override IEnumerable PerformGetByQuery(IQuery query) - { - Sql sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - Sql sql = translator.Translate(); - return Database.Fetch(sql).Select(AuditEntryFactory.BuildEntity); - } + return entries; + } - /// - protected override Sql GetBaseQuery(bool isCount) - { - Sql sql = Sql(); - sql = isCount ? sql.SelectCount() : sql.Select(); - sql = sql.From(); - return sql; - } + /// + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + return Database.Fetch(sql).Select(AuditEntryFactory.BuildEntity); + } - /// - protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.AuditEntry}.id = @id"; + /// + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); + sql = isCount ? sql.SelectCount() : sql.Select(); + sql = sql.From(); + return sql; + } - /// - protected override IEnumerable GetDeleteClauses() => - throw new NotSupportedException("Audit entries cannot be deleted."); + /// + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.AuditEntry}.id = @id"; - /// - protected override void PersistNewItem(IAuditEntry entity) - { - entity.AddingEntity(); + /// + protected override IEnumerable GetDeleteClauses() => + throw new NotSupportedException("Audit entries cannot be deleted."); - AuditEntryDto dto = AuditEntryFactory.BuildDto(entity); - Database.Insert(dto); - entity.Id = dto.Id; - entity.ResetDirtyProperties(); - } + /// + protected override void PersistNewItem(IAuditEntry entity) + { + entity.AddingEntity(); - /// - protected override void PersistUpdatedItem(IAuditEntry entity) => - throw new NotSupportedException("Audit entries cannot be updated."); + AuditEntryDto dto = AuditEntryFactory.BuildDto(entity); + Database.Insert(dto); + entity.Id = dto.Id; + entity.ResetDirtyProperties(); } + + /// + protected override void PersistUpdatedItem(IAuditEntry entity) => + throw new NotSupportedException("Audit entries cannot be updated."); } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs index cc5f08d58b11..a205746edf62 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -13,173 +10,180 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class AuditRepository : EntityRepositoryBase, IAuditRepository { - internal class AuditRepository : EntityRepositoryBase, IAuditRepository + public AuditRepository(IScopeAccessor scopeAccessor, ILogger logger) + : base(scopeAccessor, AppCaches.NoCache, logger) { - public AuditRepository(IScopeAccessor scopeAccessor, ILogger logger) - : base(scopeAccessor, AppCaches.NoCache, logger) - { } - - protected override void PersistNewItem(IAuditItem entity) - { - Database.Insert(new LogDto - { - Comment = entity.Comment, - Datestamp = DateTime.Now, - Header = entity.AuditType.ToString(), - NodeId = entity.Id, - UserId = entity.UserId, - EntityType = entity.EntityType, - Parameters = entity.Parameters - }); - } - - protected override void PersistUpdatedItem(IAuditItem entity) - { - // inserting when updating because we never update a log entry, perhaps this should throw? - Database.Insert(new LogDto - { - Comment = entity.Comment, - Datestamp = DateTime.Now, - Header = entity.AuditType.ToString(), - NodeId = entity.Id, - UserId = entity.UserId, - EntityType = entity.EntityType, - Parameters = entity.Parameters - }); - } + } - protected override IAuditItem? PerformGet(int id) - { - var sql = GetBaseQuery(false); - sql.Where(GetBaseWhereClause(), new { Id = id }); + public IEnumerable Get(AuditType type, IQuery query) + { + Sql? sqlClause = GetBaseQuery(false) + .Where("(logHeader=@0)", type.ToString()); - var dto = Database.First(sql); - return dto == null - ? null - : new AuditItem(dto.NodeId, Enum.Parse(dto.Header), dto.UserId ?? Cms.Core.Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters); - } + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); - protected override IEnumerable PerformGetAll(params int[]? ids) - { - throw new NotImplementedException(); - } + List? dtos = Database.Fetch(sql); - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); + return dtos.Select(x => new AuditItem(x.NodeId, Enum.Parse(x.Header), + x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters)).ToList(); + } - var dtos = Database.Fetch(sql); + public void CleanLogs(int maximumAgeOfLogsInMinutes) + { + DateTime oldestPermittedLogEntry = DateTime.Now.Subtract(new TimeSpan(0, maximumAgeOfLogsInMinutes, 0)); - return dtos.Select(x => new AuditItem(x.NodeId, Enum.Parse(x.Header), x.UserId ?? Cms.Core.Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters)).ToList(); - } + Database.Execute( + "delete from umbracoLog where datestamp < @oldestPermittedLogEntry and logHeader in ('open','system')", + new {oldestPermittedLogEntry}); + } - public IEnumerable Get(AuditType type, IQuery query) + /// + /// Return the audit items as paged result + /// + /// + /// The query coming from the service + /// + /// + /// + /// + /// + /// + /// Since we currently do not have enum support with our expression parser, we cannot query on AuditType in the query + /// or the custom filter + /// so we need to do that here + /// + /// + /// A user supplied custom filter + /// + /// + public IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, + out long totalRecords, Direction orderDirection, + AuditType[]? auditTypeFilter, + IQuery? customFilter) + { + if (auditTypeFilter == null) { - var sqlClause = GetBaseQuery(false) - .Where("(logHeader=@0)", type.ToString()); + auditTypeFilter = Array.Empty(); + } - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); + Sql sql = GetBaseQuery(false); - var dtos = Database.Fetch(sql); + var translator = new SqlTranslator(sql, query ?? Query()); + sql = translator.Translate(); - return dtos.Select(x => new AuditItem(x.NodeId, Enum.Parse(x.Header), x.UserId ?? Cms.Core.Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters)).ToList(); + if (customFilter != null) + { + foreach (Tuple filterClause in customFilter.GetWhereClauses()) + { + sql.Where(filterClause.Item1, filterClause.Item2); + } } - protected override Sql GetBaseQuery(bool isCount) + if (auditTypeFilter.Length > 0) { - var sql = SqlContext.Sql(); - - sql = isCount - ? sql.SelectCount() - : sql.Select(); + foreach (AuditType type in auditTypeFilter) + { + sql.Where("(logHeader=@0)", type.ToString()); + } + } - sql - .From(); + sql = orderDirection == Direction.Ascending + ? sql.OrderBy("Datestamp") + : sql.OrderByDescending("Datestamp"); - if (!isCount) - sql.LeftJoin().On((left, right) => left.UserId == right.Id); + // get page + Page? page = Database.Page(pageIndex + 1, pageSize, sql); + totalRecords = page.TotalItems; - return sql; - } + var items = page.Items.Select( + dto => new AuditItem(dto.NodeId, Enum.ParseOrNull(dto.Header) ?? AuditType.Custom, + dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters)).ToList(); - protected override string GetBaseWhereClause() + // map the DateStamp + for (var i = 0; i < items.Count; i++) { - return "id = @id"; + items[i].CreateDate = page.Items[i].Datestamp; } - protected override IEnumerable GetDeleteClauses() - { - throw new NotImplementedException(); - } + return items; + } - public void CleanLogs(int maximumAgeOfLogsInMinutes) + protected override void PersistNewItem(IAuditItem entity) => + Database.Insert(new LogDto { - var oldestPermittedLogEntry = DateTime.Now.Subtract(new TimeSpan(0, maximumAgeOfLogsInMinutes, 0)); - - Database.Execute( - "delete from umbracoLog where datestamp < @oldestPermittedLogEntry and logHeader in ('open','system')", - new {oldestPermittedLogEntry = oldestPermittedLogEntry}); - } - - /// - /// Return the audit items as paged result - /// - /// - /// The query coming from the service - /// - /// - /// - /// - /// - /// - /// Since we currently do not have enum support with our expression parser, we cannot query on AuditType in the query or the custom filter - /// so we need to do that here - /// - /// - /// A user supplied custom filter - /// - /// - public IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, - out long totalRecords, Direction orderDirection, - AuditType[]? auditTypeFilter, - IQuery? customFilter) + Comment = entity.Comment, + Datestamp = DateTime.Now, + Header = entity.AuditType.ToString(), + NodeId = entity.Id, + UserId = entity.UserId, + EntityType = entity.EntityType, + Parameters = entity.Parameters + }); + + protected override void PersistUpdatedItem(IAuditItem entity) => + // inserting when updating because we never update a log entry, perhaps this should throw? + Database.Insert(new LogDto { - if (auditTypeFilter == null) auditTypeFilter = Array.Empty(); - - var sql = GetBaseQuery(false); + Comment = entity.Comment, + Datestamp = DateTime.Now, + Header = entity.AuditType.ToString(), + NodeId = entity.Id, + UserId = entity.UserId, + EntityType = entity.EntityType, + Parameters = entity.Parameters + }); + + protected override IAuditItem? PerformGet(int id) + { + Sql sql = GetBaseQuery(false); + sql.Where(GetBaseWhereClause(), new {Id = id}); + + LogDto? dto = Database.First(sql); + return dto == null + ? null + : new AuditItem(dto.NodeId, Enum.Parse(dto.Header), + dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters); + } - var translator = new SqlTranslator(sql, query ?? Query()); - sql = translator.Translate(); + protected override IEnumerable PerformGetAll(params int[]? ids) => throw new NotImplementedException(); - if (customFilter != null) - foreach (var filterClause in customFilter.GetWhereClauses()) - sql.Where(filterClause.Item1, filterClause.Item2); + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); - if (auditTypeFilter.Length > 0) - foreach (var type in auditTypeFilter) - sql.Where("(logHeader=@0)", type.ToString()); + List? dtos = Database.Fetch(sql); - sql = orderDirection == Direction.Ascending - ? sql.OrderBy("Datestamp") - : sql.OrderByDescending("Datestamp"); + return dtos.Select(x => new AuditItem(x.NodeId, Enum.Parse(x.Header), + x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters)).ToList(); + } - // get page - var page = Database.Page(pageIndex + 1, pageSize, sql); - totalRecords = page.TotalItems; + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = SqlContext.Sql(); - var items = page.Items.Select( - dto => new AuditItem(dto.NodeId, Enum.ParseOrNull(dto.Header) ?? AuditType.Custom, dto.UserId ?? Cms.Core.Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters)).ToList(); + sql = isCount + ? sql.SelectCount() + : sql.Select(); - // map the DateStamp - for (var i = 0; i < items.Count; i++) - items[i].CreateDate = page.Items[i].Datestamp; + sql + .From(); - return items; + if (!isCount) + { + sql.LeftJoin().On((left, right) => left.UserId == right.Id); } + + return sql; } + + protected override string GetBaseWhereClause() => "id = @id"; + + protected override IEnumerable GetDeleteClauses() => throw new NotImplementedException(); } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CacheInstructionRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CacheInstructionRepository.cs index 60492773b017..242a0ce545fa 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CacheInstructionRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CacheInstructionRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using NPoco; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; @@ -9,65 +6,68 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents the NPoco implementation of . +/// +internal class CacheInstructionRepository : ICacheInstructionRepository { - /// - /// Represents the NPoco implementation of . - /// - internal class CacheInstructionRepository : ICacheInstructionRepository - { - private readonly IScopeAccessor _scopeAccessor; + private readonly IScopeAccessor _scopeAccessor; - public CacheInstructionRepository(IScopeAccessor scopeAccessor) => _scopeAccessor = scopeAccessor; + public CacheInstructionRepository(IScopeAccessor scopeAccessor) => _scopeAccessor = scopeAccessor; - /// - private Scoping.IScope? AmbientScope => _scopeAccessor.AmbientScope; + /// + private IScope? AmbientScope => _scopeAccessor.AmbientScope; - /// - public int CountAll() - { - Sql? sql = AmbientScope?.SqlContext.Sql().Select("COUNT(*)") - .From(); + /// + public int CountAll() + { + Sql? sql = AmbientScope?.SqlContext.Sql().Select("COUNT(*)") + .From(); - return AmbientScope?.Database.ExecuteScalar(sql) ?? 0; - } + return AmbientScope?.Database.ExecuteScalar(sql) ?? 0; + } - /// - public int CountPendingInstructions(int lastId) => - AmbientScope?.Database.ExecuteScalar("SELECT SUM(instructionCount) FROM umbracoCacheInstruction WHERE id > @lastId", new { lastId }) ?? 0; + /// + public int CountPendingInstructions(int lastId) => + AmbientScope?.Database.ExecuteScalar( + "SELECT SUM(instructionCount) FROM umbracoCacheInstruction WHERE id > @lastId", new {lastId}) ?? 0; - /// - public int GetMaxId() => - AmbientScope?.Database.ExecuteScalar("SELECT MAX(id) FROM umbracoCacheInstruction") ?? 0; + /// + public int GetMaxId() => + AmbientScope?.Database.ExecuteScalar("SELECT MAX(id) FROM umbracoCacheInstruction") ?? 0; - /// - public bool Exists(int id) => AmbientScope?.Database.Exists(id) ?? false; + /// + public bool Exists(int id) => AmbientScope?.Database.Exists(id) ?? false; - /// - public void Add(CacheInstruction cacheInstruction) - { - CacheInstructionDto dto = CacheInstructionFactory.BuildDto(cacheInstruction); - AmbientScope?.Database.Insert(dto); - } + /// + public void Add(CacheInstruction cacheInstruction) + { + CacheInstructionDto dto = CacheInstructionFactory.BuildDto(cacheInstruction); + AmbientScope?.Database.Insert(dto); + } - /// - public IEnumerable GetPendingInstructions(int lastId, int maxNumberToRetrieve) - { - Sql? sql = AmbientScope?.SqlContext.Sql().SelectAll() - .From() - .Where(dto => dto.Id > lastId) - .OrderBy(dto => dto.Id); - Sql? topSql = sql?.SelectTop(maxNumberToRetrieve); - return AmbientScope?.Database.Fetch(topSql).Select(CacheInstructionFactory.BuildEntity) ?? Array.Empty(); - } + /// + public IEnumerable GetPendingInstructions(int lastId, int maxNumberToRetrieve) + { + Sql? sql = AmbientScope?.SqlContext.Sql().SelectAll() + .From() + .Where(dto => dto.Id > lastId) + .OrderBy(dto => dto.Id); + Sql? topSql = sql?.SelectTop(maxNumberToRetrieve); + return AmbientScope?.Database.Fetch(topSql).Select(CacheInstructionFactory.BuildEntity) ?? + Array.Empty(); + } - /// - public void DeleteInstructionsOlderThan(DateTime pruneDate) - { - // Using 2 queries is faster than convoluted joins. - var maxId = AmbientScope?.Database.ExecuteScalar("SELECT MAX(id) FROM umbracoCacheInstruction;"); - Sql deleteSql = new Sql().Append(@"DELETE FROM umbracoCacheInstruction WHERE utcStamp < @pruneDate AND id < @maxId", new { pruneDate, maxId }); - AmbientScope?.Database.Execute(deleteSql); - } + /// + public void DeleteInstructionsOlderThan(DateTime pruneDate) + { + // Using 2 queries is faster than convoluted joins. + var maxId = AmbientScope?.Database.ExecuteScalar("SELECT MAX(id) FROM umbracoCacheInstruction;"); + Sql deleteSql = + new Sql().Append(@"DELETE FROM umbracoCacheInstruction WHERE utcStamp < @pruneDate AND id < @maxId", + new {pruneDate, maxId}); + AmbientScope?.Database.Execute(deleteSql); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ConsentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ConsentRepository.cs index 057fb7e01c84..74f3a419e51c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ConsentRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ConsentRepository.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core.Cache; @@ -12,89 +10,74 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents the NPoco implementation of . +/// +internal class ConsentRepository : EntityRepositoryBase, IConsentRepository { /// - /// Represents the NPoco implementation of . + /// Initializes a new instance of the class. /// - internal class ConsentRepository : EntityRepositoryBase, IConsentRepository + public ConsentRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) { - /// - /// Initializes a new instance of the class. - /// - public ConsentRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { } + } - /// - protected override IConsent PerformGet(int id) - { - throw new NotSupportedException(); - } + /// + public void ClearCurrent(string source, string context, string action) + { + Sql sql = Sql() + .Update(u => u.Set(x => x.Current, false)) + .Where(x => x.Source == source && x.Context == context && x.Action == action && x.Current); + Database.Execute(sql); + } - /// - protected override IEnumerable PerformGetAll(params int[]? ids) - { - throw new NotSupportedException(); - } + /// + protected override IConsent PerformGet(int id) => throw new NotSupportedException(); - /// - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = Sql().Select().From(); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate().OrderByDescending(x => x.CreateDate); - return ConsentFactory.BuildEntities(Database.Fetch(sql)); - } + /// + protected override IEnumerable PerformGetAll(params int[]? ids) => throw new NotSupportedException(); - /// - protected override Sql GetBaseQuery(bool isCount) - { - throw new NotSupportedException(); - } + /// + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = Sql().Select().From(); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate().OrderByDescending(x => x.CreateDate); + return ConsentFactory.BuildEntities(Database.Fetch(sql)); + } - /// - protected override string GetBaseWhereClause() - { - throw new NotSupportedException(); - } + /// + protected override Sql GetBaseQuery(bool isCount) => throw new NotSupportedException(); - /// - protected override IEnumerable GetDeleteClauses() - { - throw new NotSupportedException(); - } + /// + protected override string GetBaseWhereClause() => throw new NotSupportedException(); - /// - protected override void PersistNewItem(IConsent entity) - { - entity.AddingEntity(); + /// + protected override IEnumerable GetDeleteClauses() => throw new NotSupportedException(); - var dto = ConsentFactory.BuildDto(entity); - Database.Insert(dto); - entity.Id = dto.Id; - entity.ResetDirtyProperties(); - } + /// + protected override void PersistNewItem(IConsent entity) + { + entity.AddingEntity(); - /// - protected override void PersistUpdatedItem(IConsent entity) - { - entity.UpdatingEntity(); + ConsentDto dto = ConsentFactory.BuildDto(entity); + Database.Insert(dto); + entity.Id = dto.Id; + entity.ResetDirtyProperties(); + } - var dto = ConsentFactory.BuildDto(entity); - Database.Update(dto); - entity.ResetDirtyProperties(); + /// + protected override void PersistUpdatedItem(IConsent entity) + { + entity.UpdatingEntity(); - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Id)); - } + ConsentDto dto = ConsentFactory.BuildDto(entity); + Database.Update(dto); + entity.ResetDirtyProperties(); - /// - public void ClearCurrent(string source, string context, string action) - { - var sql = Sql() - .Update(u => u.Set(x => x.Current, false)) - .Where(x => x.Source == source && x.Context == context && x.Action == action && x.Current); - Database.Execute(sql); - } + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Id)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 651c3fb455bd..1f9e18199dc5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; using NPoco; @@ -24,1052 +21,1204 @@ using Umbraco.Extensions; using static Umbraco.Cms.Core.Persistence.SqlExtensionsStatics; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal sealed class ContentRepositoryBase { - internal sealed class ContentRepositoryBase - { - /// - /// This is used for unit tests ONLY - /// - public static bool ThrowOnWarning { get; set; } = false; - } + /// + /// This is used for unit tests ONLY + /// + public static bool ThrowOnWarning { get; set; } = false; +} - public abstract class ContentRepositoryBase : EntityRepositoryBase, IContentRepository - where TEntity : class, IContentBase - where TRepository : class, IRepository +public abstract class ContentRepositoryBase : EntityRepositoryBase, + IContentRepository + where TEntity : class, IContentBase + where TRepository : class, IRepository +{ + private readonly DataValueReferenceFactoryCollection _dataValueReferenceFactories; + private readonly IEventAggregator _eventAggregator; + + /// + /// + /// + /// + /// + /// + /// + /// Lazy property value collection - must be lazy because we have a circular dependency since some property editors + /// require services, yet these services require property editors + /// + protected ContentRepositoryBase( + IScopeAccessor scopeAccessor, + AppCaches cache, + ILogger> logger, + ILanguageRepository languageRepository, + IRelationRepository relationRepository, + IRelationTypeRepository relationTypeRepository, + PropertyEditorCollection propertyEditors, + DataValueReferenceFactoryCollection dataValueReferenceFactories, + IDataTypeService dataTypeService, + IEventAggregator eventAggregator) + : base(scopeAccessor, cache, logger) { - private readonly DataValueReferenceFactoryCollection _dataValueReferenceFactories; - private readonly IEventAggregator _eventAggregator; - - /// - /// - /// - /// - /// - /// - /// - /// - /// Lazy property value collection - must be lazy because we have a circular dependency since some property editors require services, yet these services require property editors - /// - protected ContentRepositoryBase( - IScopeAccessor scopeAccessor, - AppCaches cache, - ILogger> logger, - ILanguageRepository languageRepository, - IRelationRepository relationRepository, - IRelationTypeRepository relationTypeRepository, - PropertyEditorCollection propertyEditors, - DataValueReferenceFactoryCollection dataValueReferenceFactories, - IDataTypeService dataTypeService, - IEventAggregator eventAggregator) - : base(scopeAccessor, cache, logger) - { - DataTypeService = dataTypeService; - LanguageRepository = languageRepository; - RelationRepository = relationRepository; - RelationTypeRepository = relationTypeRepository; - PropertyEditors = propertyEditors; - _dataValueReferenceFactories = dataValueReferenceFactories; - _eventAggregator = eventAggregator; - } - - protected abstract TRepository This { get; } + DataTypeService = dataTypeService; + LanguageRepository = languageRepository; + RelationRepository = relationRepository; + RelationTypeRepository = relationTypeRepository; + PropertyEditors = propertyEditors; + _dataValueReferenceFactories = dataValueReferenceFactories; + _eventAggregator = eventAggregator; + } - /// - /// Gets the node object type for the repository's entity - /// - protected abstract Guid NodeObjectTypeId { get; } + protected abstract TRepository This { get; } - protected ILanguageRepository LanguageRepository { get; } - protected IDataTypeService DataTypeService { get; } - protected IRelationRepository RelationRepository { get; } - protected IRelationTypeRepository RelationTypeRepository { get; } + /// + /// Gets the node object type for the repository's entity + /// + protected abstract Guid NodeObjectTypeId { get; } - protected PropertyEditorCollection PropertyEditors { get; } + protected ILanguageRepository LanguageRepository { get; } + protected IDataTypeService DataTypeService { get; } + protected IRelationRepository RelationRepository { get; } + protected IRelationTypeRepository RelationTypeRepository { get; } - #region Versions + protected PropertyEditorCollection PropertyEditors { get; } - // gets a specific version - public abstract TEntity? GetVersion(int versionId); + public abstract IEnumerable GetPage(IQuery? query, + long pageIndex, int pageSize, out long totalRecords, + IQuery? filter, + Ordering? ordering); - // gets all versions, current first - public abstract IEnumerable GetAllVersions(int nodeId); + public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options) + { + var report = new Dictionary(); - // gets all versions, current first - public virtual IEnumerable GetAllVersionsSlim(int nodeId, int skip, int take) - => GetAllVersions(nodeId).Skip(skip).Take(take); + Sql sql = SqlContext.Sql() + .Select() + .From() + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); - // gets all version ids, current first - public virtual IEnumerable GetVersionIds(int nodeId, int maxRows) + var nodesToRebuild = new Dictionary>(); + var validNodes = new Dictionary(); + var rootIds = new[] { - var template = SqlContext.Templates.Get(Cms.Core.Constants.SqlTemplates.VersionableRepository.GetVersionIds, tsql => - tsql.Select(x => x.Id) - .From() - .Where(x => x.NodeId == SqlTemplate.Arg("nodeId")) - .OrderByDescending(x => x.Current) // current '1' comes before others '0' - .AndByDescending(x => x.VersionDate) // most recent first - ); - return Database.Fetch(SqlSyntax.SelectTop(template.Sql(nodeId), maxRows)); - } - - // deletes a specific version - public virtual void DeleteVersion(int versionId) + Constants.System.Root, Constants.System.RecycleBinContent, Constants.System.RecycleBinMedia + }; + var currentParentIds = new HashSet(rootIds); + HashSet prevParentIds = currentParentIds; + var lastLevel = -1; + + // use a forward cursor (query) + foreach (NodeDto? node in Database.Query(sql)) { - // TODO: test object node type? - - // get the version we want to delete - var template = SqlContext.Templates.Get(Cms.Core.Constants.SqlTemplates.VersionableRepository.GetVersion, tsql => - tsql.Select().From().Where(x => x.Id == SqlTemplate.Arg("versionId")) - ); - var versionDto = Database.Fetch(template.Sql(new { versionId })).FirstOrDefault(); - - // nothing to delete - if (versionDto == null) - return; - - // don't delete the current version - if (versionDto.Current) - throw new InvalidOperationException("Cannot delete the current version."); - - PerformDeleteVersion(versionDto.NodeId, versionId); - } - - // deletes all versions of an entity, older than a date. - public virtual void DeleteVersions(int nodeId, DateTime versionDate) - { - // TODO: test object node type? - - // get the versions we want to delete, excluding the current one - var template = SqlContext.Templates.Get(Cms.Core.Constants.SqlTemplates.VersionableRepository.GetVersions, tsql => - tsql.Select().From().Where(x => x.NodeId == SqlTemplate.Arg("nodeId") && !x.Current && x.VersionDate < SqlTemplate.Arg("versionDate")) - ); - var versionDtos = Database.Fetch(template.Sql(new { nodeId, versionDate })); - foreach (var versionDto in versionDtos) - PerformDeleteVersion(versionDto.NodeId, versionDto.Id); - } - - // actually deletes a version - protected abstract void PerformDeleteVersion(int id, int versionId); - - #endregion + if (node.Level != lastLevel) + { + // changing levels + prevParentIds = currentParentIds; + currentParentIds = null; + lastLevel = node.Level; + } - #region Count + if (currentParentIds == null) + { + // we're reset + currentParentIds = new HashSet(); + } - /// - /// Count descendants of an item. - /// - public int CountDescendants(int parentId, string? contentTypeAlias = null) - { - var pathMatch = parentId == -1 - ? "-1," - : "," + parentId + ","; + currentParentIds.Add(node.NodeId); - var sql = SqlContext.Sql() - .SelectCount() - .From(); + // paths parts without the roots + var pathParts = node.Path.Split(Constants.CharArrays.Comma) + .Where(x => !rootIds.Contains(int.Parse(x, CultureInfo.InvariantCulture))).ToArray(); - if (contentTypeAlias.IsNullOrWhiteSpace()) + if (!prevParentIds.Contains(node.ParentId)) { - sql - .Where(x => x.NodeObjectType == NodeObjectTypeId) - .Where(x => x.Path.Contains(pathMatch)); + // invalid, this will be because the level is wrong (which prob means path is wrong too) + report.Add(node.NodeId, + new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType + .InvalidPathAndLevelByParentId)); + AppendNodeToFix(nodesToRebuild, node); } - else + else if (pathParts.Length == 0) { - sql - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.ContentTypeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId) - .Where(x => x.Path.Contains(pathMatch)) - .Where(x => x.Alias == contentTypeAlias); + // invalid path + report.Add(node.NodeId, + new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathEmpty)); + AppendNodeToFix(nodesToRebuild, node); } - - return Database.ExecuteScalar(sql); - } - - /// - /// Count children of an item. - /// - public int CountChildren(int parentId, string? contentTypeAlias = null) - { - var sql = SqlContext.Sql() - .SelectCount() - .From(); - - if (contentTypeAlias.IsNullOrWhiteSpace()) + else if (pathParts.Length != node.Level) { - sql - .Where(x => x.NodeObjectType == NodeObjectTypeId) - .Where(x => x.ParentId == parentId); + // invalid, either path or level is wrong + report.Add(node.NodeId, + new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathLevelMismatch)); + AppendNodeToFix(nodesToRebuild, node); } - else + else if (pathParts[pathParts.Length - 1] != node.NodeId.ToString()) { - sql - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.ContentTypeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId) - .Where(x => x.ParentId == parentId) - .Where(x => x.Alias == contentTypeAlias); + // invalid path + report.Add(node.NodeId, + new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathById)); + AppendNodeToFix(nodesToRebuild, node); } - - return Database.ExecuteScalar(sql); - } - - /// - /// Count items. - /// - public int Count(string? contentTypeAlias = null) - { - var sql = SqlContext.Sql() - .SelectCount() - .From(); - - if (contentTypeAlias.IsNullOrWhiteSpace()) + else if (!rootIds.Contains(node.ParentId) && pathParts[pathParts.Length - 2] != node.ParentId.ToString()) { - sql - .Where(x => x.NodeObjectType == NodeObjectTypeId); + // invalid path + report.Add(node.NodeId, + new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathByParentId)); + AppendNodeToFix(nodesToRebuild, node); } else { - sql - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.ContentTypeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId) - .Where(x => x.Alias == contentTypeAlias); - } + // it's valid! - return Database.ExecuteScalar(sql); + // don't track unless we are configured to fix + if (options.FixIssues) + { + validNodes.Add(node.NodeId, node); + } + } } - #endregion + var updated = new List(); - #region Tags - - /// - /// Updates tags for an item. - /// - protected void SetEntityTags(IContentBase entity, ITagRepository tagRepo, IJsonSerializer serializer) + if (options.FixIssues) { - foreach (var property in entity.Properties) + // iterate all valid nodes to see if these are parents for invalid nodes + foreach ((var nodeId, NodeDto node) in validNodes) { - var tagConfiguration = property.GetTagConfiguration(PropertyEditors, DataTypeService); - if (tagConfiguration == null) continue; // not a tags property + if (!nodesToRebuild.TryGetValue(nodeId, out List? invalidNodes)) + { + continue; + } - if (property.PropertyType.VariesByCulture()) + // now we can try to rebuild the invalid paths. + + foreach (NodeDto invalidNode in invalidNodes) { - var tags = new List(); - foreach (var pvalue in property.Values) - { - var tagsValue = property.GetTagsValue(PropertyEditors, DataTypeService, serializer, pvalue.Culture); - var languageId = LanguageRepository.GetIdByIsoCode(pvalue.Culture); - var cultureTags = tagsValue.Select(x => new Tag { Group = tagConfiguration.Group, Text = x, LanguageId = languageId }); - tags.AddRange(cultureTags); - } - tagRepo.Assign(entity.Id, property.PropertyTypeId, tags); + invalidNode.Level = (short)(node.Level + 1); + invalidNode.Path = node.Path + "," + invalidNode.NodeId; + updated.Add(invalidNode); } - else + } + + foreach (NodeDto node in updated) + { + Database.Update(node); + if (report.TryGetValue(node.NodeId, out ContentDataIntegrityReportEntry? entry)) { - var tagsValue = property.GetTagsValue(PropertyEditors, DataTypeService, serializer); // strings - var tags = tagsValue.Select(x => new Tag { Group = tagConfiguration.Group, Text = x }); - tagRepo.Assign(entity.Id, property.PropertyTypeId, tags); + entry.Fixed = true; } } } - // TODO: should we do it when un-publishing? or? - /// - /// Clears tags for an item. - /// - protected void ClearEntityTags(IContentBase entity, ITagRepository tagRepo) + return new ContentDataIntegrityReport(report); + } + + private Sql PreparePageSql(Sql sql, Sql? filterSql, Ordering ordering) + { + // non-filtering, non-ordering = nothing to do + if (filterSql == null && ordering.IsEmpty) { - tagRepo.RemoveAll(entity.Id); + return sql; } - #endregion + // preserve original + var psql = new Sql(sql.SqlContext, sql.SQL, sql.Arguments); - private Sql PreparePageSql(Sql sql, Sql? filterSql, Ordering ordering) + // apply filter + if (filterSql != null) { - // non-filtering, non-ordering = nothing to do - if (filterSql == null && ordering.IsEmpty) return sql; - - // preserve original - var psql = new Sql(sql.SqlContext, sql.SQL, sql.Arguments); - - // apply filter - if (filterSql != null) - psql.Append(filterSql); + psql.Append(filterSql); + } - // non-sorting, we're done - if (ordering.IsEmpty) - return psql; + // non-sorting, we're done + if (ordering.IsEmpty) + { + return psql; + } - // else apply ordering - ApplyOrdering(ref psql, ordering); + // else apply ordering + ApplyOrdering(ref psql, ordering); - // no matter what we always MUST order the result also by umbracoNode.id to ensure that all records being ordered by are unique. - // if we do not do this then we end up with issues where we are ordering by a field that has duplicate values (i.e. the 'text' column - // is empty for many nodes) - see: http://issues.umbraco.org/issue/U4-8831 + // no matter what we always MUST order the result also by umbracoNode.id to ensure that all records being ordered by are unique. + // if we do not do this then we end up with issues where we are ordering by a field that has duplicate values (i.e. the 'text' column + // is empty for many nodes) - see: http://issues.umbraco.org/issue/U4-8831 - var (dbfield, _) = SqlContext.VisitDto(x => x.NodeId); - if (ordering.IsCustomField || !ordering.OrderBy.InvariantEquals("id")) - { - psql.OrderBy(GetAliasedField(dbfield, sql)); - } + var (dbfield, _) = SqlContext.VisitDto(x => x.NodeId); + if (ordering.IsCustomField || !ordering.OrderBy.InvariantEquals("id")) + { + psql.OrderBy(GetAliasedField(dbfield, sql)); + } - // create prepared sql - // ensure it's single-line as NPoco PagingHelper has issues with multi-lines - psql = Sql(psql.SQL.ToSingleLine(), psql.Arguments); + // create prepared sql + // ensure it's single-line as NPoco PagingHelper has issues with multi-lines + psql = Sql(psql.SQL.ToSingleLine(), psql.Arguments); - // replace the magic culture parameter (see DocumentRepository.GetBaseQuery()) - if (!ordering.Culture.IsNullOrWhiteSpace()) + // replace the magic culture parameter (see DocumentRepository.GetBaseQuery()) + if (!ordering.Culture.IsNullOrWhiteSpace()) + { + for (var i = 0; i < psql.Arguments.Length; i++) { - for (var i = 0; i < psql.Arguments.Length; i++) + if (psql.Arguments[i] is string s && s == "[[[ISOCODE]]]") { - if (psql.Arguments[i] is string s && s == "[[[ISOCODE]]]") - { - psql.Arguments[i] = ordering.Culture; - } + psql.Arguments[i] = ordering.Culture; } } - return psql; } - private void ApplyOrdering(ref Sql sql, Ordering ordering) + return psql; + } + + private void ApplyOrdering(ref Sql sql, Ordering ordering) + { + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + + if (ordering == null) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); - if (ordering == null) throw new ArgumentNullException(nameof(ordering)); + throw new ArgumentNullException(nameof(ordering)); + } - var orderBy = ordering.IsCustomField - ? ApplyCustomOrdering(ref sql, ordering) - : ApplySystemOrdering(ref sql, ordering); + var orderBy = ordering.IsCustomField + ? ApplyCustomOrdering(ref sql, ordering) + : ApplySystemOrdering(ref sql, ordering); - // beware! NPoco paging code parses the query to isolate the ORDER BY fragment, - // using a regex that wants "([\w\.\[\]\(\)\s""`,]+)" - meaning that anything - // else in orderBy is going to break NPoco / not be detected + // beware! NPoco paging code parses the query to isolate the ORDER BY fragment, + // using a regex that wants "([\w\.\[\]\(\)\s""`,]+)" - meaning that anything + // else in orderBy is going to break NPoco / not be detected - // beware! NPoco paging code (in PagingHelper) collapses everything [foo].[bar] - // to [bar] only, so we MUST use aliases, cannot use [table].[field] + // beware! NPoco paging code (in PagingHelper) collapses everything [foo].[bar] + // to [bar] only, so we MUST use aliases, cannot use [table].[field] - // beware! pre-2012 SqlServer is using a convoluted syntax for paging, which - // includes "SELECT ROW_NUMBER() OVER (ORDER BY ...) poco_rn FROM SELECT (...", - // so anything added here MUST also be part of the inner SELECT statement, ie - // the original statement, AND must be using the proper alias, as the inner SELECT - // will hide the original table.field names entirely + // beware! pre-2012 SqlServer is using a convoluted syntax for paging, which + // includes "SELECT ROW_NUMBER() OVER (ORDER BY ...) poco_rn FROM SELECT (...", + // so anything added here MUST also be part of the inner SELECT statement, ie + // the original statement, AND must be using the proper alias, as the inner SELECT + // will hide the original table.field names entirely - if (ordering.Direction == Direction.Ascending) - sql.OrderBy(orderBy); - else - sql.OrderByDescending(orderBy); + if (ordering.Direction == Direction.Ascending) + { + sql.OrderBy(orderBy); } + else + { + sql.OrderByDescending(orderBy); + } + } - protected virtual string ApplySystemOrdering(ref Sql sql, Ordering ordering) + protected virtual string ApplySystemOrdering(ref Sql sql, Ordering ordering) + { + // id is invariant + if (ordering.OrderBy.InvariantEquals("id")) { - // id is invariant - if (ordering.OrderBy.InvariantEquals("id")) - return GetAliasedField(SqlSyntax.GetFieldName(x => x.NodeId), sql); + return GetAliasedField(SqlSyntax.GetFieldName(x => x.NodeId), sql); + } - // sort order is invariant - if (ordering.OrderBy.InvariantEquals("sortOrder")) - return GetAliasedField(SqlSyntax.GetFieldName(x => x.SortOrder), sql); + // sort order is invariant + if (ordering.OrderBy.InvariantEquals("sortOrder")) + { + return GetAliasedField(SqlSyntax.GetFieldName(x => x.SortOrder), sql); + } - // path is invariant - if (ordering.OrderBy.InvariantEquals("path")) - return GetAliasedField(SqlSyntax.GetFieldName(x => x.Path), sql); + // path is invariant + if (ordering.OrderBy.InvariantEquals("path")) + { + return GetAliasedField(SqlSyntax.GetFieldName(x => x.Path), sql); + } - // note: 'owner' is the user who created the item as a whole, - // we don't have an 'owner' per culture (should we?) - if (ordering.OrderBy.InvariantEquals("owner")) - { - var joins = Sql() - .InnerJoin("ownerUser").On((node, user) => node.UserId == user.Id, aliasRight: "ownerUser"); + // note: 'owner' is the user who created the item as a whole, + // we don't have an 'owner' per culture (should we?) + if (ordering.OrderBy.InvariantEquals("owner")) + { + Sql joins = Sql() + .InnerJoin("ownerUser") + .On((node, user) => node.UserId == user.Id, aliasRight: "ownerUser"); - // see notes in ApplyOrdering: the field MUST be selected + aliased - sql = Sql(InsertBefore(sql, "FROM", ", " + SqlSyntax.GetFieldName(x => x.UserName, "ownerUser") + " AS ordering "), sql.Arguments); + // see notes in ApplyOrdering: the field MUST be selected + aliased + sql = Sql( + InsertBefore(sql, "FROM", + ", " + SqlSyntax.GetFieldName(x => x.UserName, "ownerUser") + " AS ordering "), + sql.Arguments); - sql = InsertJoins(sql, joins); + sql = InsertJoins(sql, joins); - return "ordering"; - } + return "ordering"; + } - // note: each version culture variation has a date too, - // maybe we would want to use it instead? - if (ordering.OrderBy.InvariantEquals("versionDate") || ordering.OrderBy.InvariantEquals("updateDate")) - return GetAliasedField(SqlSyntax.GetFieldName(x => x.VersionDate), sql); + // note: each version culture variation has a date too, + // maybe we would want to use it instead? + if (ordering.OrderBy.InvariantEquals("versionDate") || ordering.OrderBy.InvariantEquals("updateDate")) + { + return GetAliasedField(SqlSyntax.GetFieldName(x => x.VersionDate), sql); + } - // create date is invariant (we don't keep each culture's creation date) - if (ordering.OrderBy.InvariantEquals("createDate")) - return GetAliasedField(SqlSyntax.GetFieldName(x => x.CreateDate), sql); + // create date is invariant (we don't keep each culture's creation date) + if (ordering.OrderBy.InvariantEquals("createDate")) + { + return GetAliasedField(SqlSyntax.GetFieldName(x => x.CreateDate), sql); + } - // name is variant - if (ordering.OrderBy.InvariantEquals("name")) + // name is variant + if (ordering.OrderBy.InvariantEquals("name")) + { + // no culture = can only work on the invariant name + // see notes in ApplyOrdering: the field MUST be aliased + if (ordering.Culture.IsNullOrWhiteSpace()) { - // no culture = can only work on the invariant name - // see notes in ApplyOrdering: the field MUST be aliased - if (ordering.Culture.IsNullOrWhiteSpace()) - return GetAliasedField(SqlSyntax.GetFieldName(x => x.Text!), sql); - - // "variantName" alias is defined in DocumentRepository.GetBaseQuery - // TODO: what if it is NOT a document but a ... media or whatever? - // previously, we inserted the join+select *here* so we were sure to have it, - // but now that's not the case anymore! - return "variantName"; + return GetAliasedField(SqlSyntax.GetFieldName(x => x.Text!), sql); } - // content type alias is invariant - if (ordering.OrderBy.InvariantEquals("contentTypeAlias")) - { - var joins = Sql() - .InnerJoin("ctype").On((content, contentType) => content.ContentTypeId == contentType.NodeId, aliasRight: "ctype"); + // "variantName" alias is defined in DocumentRepository.GetBaseQuery + // TODO: what if it is NOT a document but a ... media or whatever? + // previously, we inserted the join+select *here* so we were sure to have it, + // but now that's not the case anymore! + return "variantName"; + } - // see notes in ApplyOrdering: the field MUST be selected + aliased - sql = Sql(InsertBefore(sql, "FROM", ", " + SqlSyntax.GetFieldName(x => x.Alias!, "ctype") + " AS ordering "), sql.Arguments); + // content type alias is invariant + if (ordering.OrderBy.InvariantEquals("contentTypeAlias")) + { + Sql joins = Sql() + .InnerJoin("ctype").On( + (content, contentType) => content.ContentTypeId == contentType.NodeId, aliasRight: "ctype"); - sql = InsertJoins(sql, joins); + // see notes in ApplyOrdering: the field MUST be selected + aliased + sql = Sql( + InsertBefore(sql, "FROM", + ", " + SqlSyntax.GetFieldName(x => x.Alias!, "ctype") + " AS ordering "), + sql.Arguments); - return "ordering"; - } + sql = InsertJoins(sql, joins); - // previously, we'd accept anything and just sanitize it - not anymore - throw new NotSupportedException($"Ordering by {ordering.OrderBy} not supported."); + return "ordering"; } - private string ApplyCustomOrdering(ref Sql sql, Ordering ordering) - { - // sorting by a custom field, so set-up sub-query for ORDER BY clause to pull through value - // from 'current' content version for the given order by field - var sortedInt = string.Format(SqlContext.SqlSyntax.ConvertIntegerToOrderableString, "intValue"); - var sortedDecimal = string.Format(SqlContext.SqlSyntax.ConvertDecimalToOrderableString, "decimalValue"); - var sortedDate = string.Format(SqlContext.SqlSyntax.ConvertDateToOrderableString, "dateValue"); - var sortedString = "COALESCE(varcharValue,'')"; // assuming COALESCE is ok for all syntaxes - - // needs to be an outer join since there's no guarantee that any of the nodes have values for this property - var innerSql = Sql().Select($@"CASE + // previously, we'd accept anything and just sanitize it - not anymore + throw new NotSupportedException($"Ordering by {ordering.OrderBy} not supported."); + } + + private string ApplyCustomOrdering(ref Sql sql, Ordering ordering) + { + // sorting by a custom field, so set-up sub-query for ORDER BY clause to pull through value + // from 'current' content version for the given order by field + var sortedInt = string.Format(SqlContext.SqlSyntax.ConvertIntegerToOrderableString, "intValue"); + var sortedDecimal = string.Format(SqlContext.SqlSyntax.ConvertDecimalToOrderableString, "decimalValue"); + var sortedDate = string.Format(SqlContext.SqlSyntax.ConvertDateToOrderableString, "dateValue"); + var sortedString = "COALESCE(varcharValue,'')"; // assuming COALESCE is ok for all syntaxes + + // needs to be an outer join since there's no guarantee that any of the nodes have values for this property + Sql innerSql = Sql().Select($@"CASE WHEN intValue IS NOT NULL THEN {sortedInt} WHEN decimalValue IS NOT NULL THEN {sortedDecimal} WHEN dateValue IS NOT NULL THEN {sortedDate} ELSE {sortedString} END AS customPropVal, cver.nodeId AS customPropNodeId") - .From("cver") - .InnerJoin("opdata") - .On((version, pdata) => version.Id == pdata.VersionId, "cver", "opdata") - .InnerJoin("optype").On((pdata, ptype) => pdata.PropertyTypeId == ptype.Id, "opdata", "optype") - .LeftJoin().On((pdata, lang) => pdata.LanguageId == lang.Id, "opdata") - .Where(x => x.Current, "cver") // always query on current (edit) values - .Where(x => x.Alias == ordering.OrderBy, "optype") - .Where((opdata, lang) => opdata.LanguageId == null || lang.IsoCode == ordering.Culture, "opdata"); - - // merge arguments - var argsList = sql.Arguments.ToList(); - var innerSqlString = ParameterHelper.ProcessParams(innerSql.SQL, innerSql.Arguments, argsList); - - // create the outer join complete sql fragment - var outerJoinTempTable = $@"LEFT OUTER JOIN ({innerSqlString}) AS customPropData - ON customPropData.customPropNodeId = {Cms.Core.Constants.DatabaseSchema.Tables.Node}.id "; // trailing space is important! - - // insert this just above the first WHERE - var newSql = InsertBefore(sql.SQL, "WHERE", outerJoinTempTable); + .From("cver") + .InnerJoin("opdata") + .On((version, pdata) => version.Id == pdata.VersionId, "cver", "opdata") + .InnerJoin("optype") + .On((pdata, ptype) => pdata.PropertyTypeId == ptype.Id, "opdata", + "optype") + .LeftJoin() + .On((pdata, lang) => pdata.LanguageId == lang.Id, "opdata") + .Where(x => x.Current, "cver") // always query on current (edit) values + .Where(x => x.Alias == ordering.OrderBy, "optype") + .Where( + (opdata, lang) => opdata.LanguageId == null || lang.IsoCode == ordering.Culture, "opdata"); + + // merge arguments + var argsList = sql.Arguments.ToList(); + var innerSqlString = ParameterHelper.ProcessParams(innerSql.SQL, innerSql.Arguments, argsList); + + // create the outer join complete sql fragment + var outerJoinTempTable = $@"LEFT OUTER JOIN ({innerSqlString}) AS customPropData + ON customPropData.customPropNodeId = {Constants.DatabaseSchema.Tables.Node}.id "; // trailing space is important! + + // insert this just above the first WHERE + var newSql = InsertBefore(sql.SQL, "WHERE", outerJoinTempTable); + + // see notes in ApplyOrdering: the field MUST be selected + aliased + newSql = InsertBefore(newSql, "FROM", + ", customPropData.customPropVal AS ordering "); // trailing space is important! + + // create the new sql + sql = Sql(newSql, argsList.ToArray()); + + // and order by the custom field + // this original code means that an ascending sort would first expose all NULL values, ie items without a value + return "ordering"; + + // note: adding an extra sorting criteria on + // "(CASE WHEN customPropData.customPropVal IS NULL THEN 1 ELSE 0 END") + // would ensure that items without a value always come last, both in ASC and DESC-ending sorts + } - // see notes in ApplyOrdering: the field MUST be selected + aliased - newSql = InsertBefore(newSql, "FROM", ", customPropData.customPropVal AS ordering "); // trailing space is important! + private static void AppendNodeToFix(IDictionary> nodesToRebuild, NodeDto node) + { + if (nodesToRebuild.TryGetValue(node.ParentId, out List? childIds)) + { + childIds.Add(node); + } + else + { + nodesToRebuild[node.ParentId] = new List {node}; + } + } - // create the new sql - sql = Sql(newSql, argsList.ToArray()); + // here, filter can be null and ordering cannot + protected IEnumerable GetPage(IQuery? query, + long pageIndex, int pageSize, out long totalRecords, + Func, IEnumerable> mapDtos, + Sql? filter, + Ordering? ordering) + { + if (ordering == null) + { + throw new ArgumentNullException(nameof(ordering)); + } - // and order by the custom field - // this original code means that an ascending sort would first expose all NULL values, ie items without a value - return "ordering"; + // start with base query, and apply the supplied IQuery + if (query == null) + { + query = Query(); + } + + Sql sql = new SqlTranslator(GetBaseQuery(QueryType.Many), query).Translate(); + + // sort and filter + sql = PreparePageSql(sql, filter, ordering); + + // get a page of DTOs and the total count + Page? pagedResult = Database.Page(pageIndex + 1, pageSize, sql); + totalRecords = Convert.ToInt32(pagedResult.TotalItems); + + // map the DTOs and return + return mapDtos(pagedResult.Items); + } - // note: adding an extra sorting criteria on - // "(CASE WHEN customPropData.customPropVal IS NULL THEN 1 ELSE 0 END") - // would ensure that items without a value always come last, both in ASC and DESC-ending sorts + protected IDictionary GetPropertyCollections(List> temps) + where T : class, IContentBase + { + var versions = new List(); + foreach (TempContent temp in temps) + { + versions.Add(temp.VersionId); + if (temp.PublishedVersionId > 0) + { + versions.Add(temp.PublishedVersionId); + } + } + + if (versions.Count == 0) + { + return new Dictionary(); } - public abstract IEnumerable GetPage(IQuery? query, - long pageIndex, int pageSize, out long totalRecords, - IQuery? filter, - Ordering? ordering); + // TODO: This is a bugger of a query and I believe is the main issue with regards to SQL performance drain when querying content + // which is done when rebuilding caches/indexes/etc... in bulk. We are using an "IN" query on umbracoPropertyData.VersionId + // which then performs a Clustered Index Scan on PK_umbracoPropertyData which means it iterates the entire table which can be enormous! + // especially if there are both a lot of content but worse if there is a lot of versions of that content. + // So is it possible to return this property data without doing an index scan on PK_umbracoPropertyData and without iterating every row + // in the table? + + // get all PropertyDataDto for all definitions / versions + var allPropertyDataDtos = Database.FetchByGroups(versions, + Constants.Sql.MaxParameterCount, batch => + SqlContext.Sql() + .Select() + .From() + .WhereIn(x => x.VersionId, batch)) + .ToList(); + + // get PropertyDataDto distinct PropertyTypeDto + var allPropertyTypeIds = allPropertyDataDtos.Select(x => x.PropertyTypeId).Distinct().ToList(); + IEnumerable allPropertyTypeDtos = Database.FetchByGroups( + allPropertyTypeIds, Constants.Sql.MaxParameterCount, batch => + SqlContext.Sql() + .Select(r => r.Select(x => x.DataTypeDto)) + .From() + .InnerJoin() + .On((left, right) => left.DataTypeId == right.NodeId) + .WhereIn(x => x.Id, batch)); - public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options) + // index the types for perfs, and assign to PropertyDataDto + var indexedPropertyTypeDtos = allPropertyTypeDtos.ToDictionary(x => x.Id, x => x); + foreach (PropertyDataDto a in allPropertyDataDtos) { - var report = new Dictionary(); + a.PropertyTypeDto = indexedPropertyTypeDtos[a.PropertyTypeId]; + } - var sql = SqlContext.Sql() - .Select() - .From() - .Where(x => x.NodeObjectType == NodeObjectTypeId) - .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + // now we have + // - the definitions + // - all property data dtos + // - tag editors (Actually ... no we don't since i removed that code, but we don't need them anyways it seems) + // and we need to build the proper property collections - var nodesToRebuild = new Dictionary>(); - var validNodes = new Dictionary(); - var rootIds = new[] {Cms.Core.Constants.System.Root, Cms.Core.Constants.System.RecycleBinContent, Cms.Core.Constants.System.RecycleBinMedia}; - var currentParentIds = new HashSet(rootIds); - var prevParentIds = currentParentIds; - var lastLevel = -1; + return GetPropertyCollections(temps, allPropertyDataDtos); + } + + private IDictionary GetPropertyCollections(List> temps, + IEnumerable allPropertyDataDtos) + where T : class, IContentBase + { + var result = new Dictionary(); + var compositionPropertiesIndex = new Dictionary(); - // use a forward cursor (query) - foreach (var node in Database.Query(sql)) + // index PropertyDataDto per versionId for perfs + // merge edited and published dtos + var indexedPropertyDataDtos = new Dictionary>(); + foreach (PropertyDataDto dto in allPropertyDataDtos) + { + var versionId = dto.VersionId; + if (indexedPropertyDataDtos.TryGetValue(versionId, out List? list) == false) { - if (node.Level != lastLevel) - { - // changing levels - prevParentIds = currentParentIds; - currentParentIds = null; - lastLevel = node.Level; - } + indexedPropertyDataDtos[versionId] = list = new List(); + } - if (currentParentIds == null) - { - // we're reset - currentParentIds = new HashSet(); - } + list.Add(dto); + } - currentParentIds.Add(node.NodeId); + foreach (TempContent temp in temps) + { + // compositionProperties is the property types for the entire composition + // use an index for perfs + if (temp.ContentType is null) + { + continue; + } - // paths parts without the roots - var pathParts = node.Path.Split(Constants.CharArrays.Comma).Where(x => !rootIds.Contains(int.Parse(x, CultureInfo.InvariantCulture))).ToArray(); + if (compositionPropertiesIndex.TryGetValue(temp.ContentType.Id, + out IPropertyType[]? compositionProperties) == false) + { + compositionPropertiesIndex[temp.ContentType.Id] = + compositionProperties = temp.ContentType.CompositionPropertyTypes.ToArray(); + } - if (!prevParentIds.Contains(node.ParentId)) - { - // invalid, this will be because the level is wrong (which prob means path is wrong too) - report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathAndLevelByParentId)); - AppendNodeToFix(nodesToRebuild, node); - } - else if (pathParts.Length == 0) - { - // invalid path - report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathEmpty)); - AppendNodeToFix(nodesToRebuild, node); - } - else if (pathParts.Length != node.Level) - { - // invalid, either path or level is wrong - report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathLevelMismatch)); - AppendNodeToFix(nodesToRebuild, node); - } - else if (pathParts[pathParts.Length - 1] != node.NodeId.ToString()) - { - // invalid path - report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathById)); - AppendNodeToFix(nodesToRebuild, node); - } - else if (!rootIds.Contains(node.ParentId) && pathParts[pathParts.Length - 2] != node.ParentId.ToString()) + // map the list of PropertyDataDto to a list of Property + var propertyDataDtos = new List(); + if (indexedPropertyDataDtos.TryGetValue(temp.VersionId, out List? propertyDataDtos1)) + { + propertyDataDtos.AddRange(propertyDataDtos1); + if (temp.VersionId == temp.PublishedVersionId) // dirty corner case { - // invalid path - report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathByParentId)); - AppendNodeToFix(nodesToRebuild, node); + propertyDataDtos.AddRange(propertyDataDtos1.Select(x => x.Clone(-1))); } - else - { - // it's valid! + } - // don't track unless we are configured to fix - if (options.FixIssues) - validNodes.Add(node.NodeId, node); - } + if (temp.VersionId != temp.PublishedVersionId && + indexedPropertyDataDtos.TryGetValue(temp.PublishedVersionId, + out List? propertyDataDtos2)) + { + propertyDataDtos.AddRange(propertyDataDtos2); } - var updated = new List(); + var properties = PropertyFactory.BuildEntities(compositionProperties, propertyDataDtos, + temp.PublishedVersionId, LanguageRepository).ToList(); - if (options.FixIssues) + if (result.ContainsKey(temp.VersionId)) { - // iterate all valid nodes to see if these are parents for invalid nodes - foreach (var (nodeId, node) in validNodes) + if (ContentRepositoryBase.ThrowOnWarning) { - if (!nodesToRebuild.TryGetValue(nodeId, out var invalidNodes)) continue; - - // now we can try to rebuild the invalid paths. - - foreach (var invalidNode in invalidNodes) - { - invalidNode.Level = (short)(node.Level + 1); - invalidNode.Path = node.Path + "," + invalidNode.NodeId; - updated.Add(invalidNode); - } + throw new InvalidOperationException( + $"The query returned multiple property sets for content {temp.Id}, {temp.ContentType.Name}"); } - foreach (var node in updated) - { - Database.Update(node); - if (report.TryGetValue(node.NodeId, out var entry)) - entry.Fixed = true; - } + Logger.LogWarning( + "The query returned multiple property sets for content {ContentId}, {ContentTypeName}", temp.Id, + temp.ContentType.Name); } - return new ContentDataIntegrityReport(report); + result[temp.VersionId] = new PropertyCollection(properties); } - private static void AppendNodeToFix(IDictionary> nodesToRebuild, NodeDto node) - { - if (nodesToRebuild.TryGetValue(node.ParentId, out var childIds)) - childIds.Add(node); - else - nodesToRebuild[node.ParentId] = new List { node }; - } + return result; + } - // here, filter can be null and ordering cannot - protected IEnumerable GetPage(IQuery? query, - long pageIndex, int pageSize, out long totalRecords, - Func, IEnumerable> mapDtos, - Sql? filter, - Ordering? ordering) - { - if (ordering == null) throw new ArgumentNullException(nameof(ordering)); + protected string InsertBefore(Sql s, string atToken, string insert) + => InsertBefore(s.SQL, atToken, insert); - // start with base query, and apply the supplied IQuery - if (query == null) query = Query(); - var sql = new SqlTranslator(GetBaseQuery(QueryType.Many), query).Translate(); + protected string InsertBefore(string s, string atToken, string insert) + { + var pos = s.InvariantIndexOf(atToken); + if (pos < 0) + { + throw new Exception($"Could not find token \"{atToken}\"."); + } - // sort and filter - sql = PreparePageSql(sql, filter, ordering); + return s.Insert(pos, insert); + } - // get a page of DTOs and the total count - var pagedResult = Database.Page(pageIndex + 1, pageSize, sql); - totalRecords = Convert.ToInt32(pagedResult.TotalItems); + protected Sql InsertJoins(Sql sql, Sql joins) + { + var joinsSql = joins.SQL; + var args = sql.Arguments; - // map the DTOs and return - return mapDtos(pagedResult.Items); + // merge args if any + if (joins.Arguments.Length > 0) + { + var argsList = args.ToList(); + joinsSql = ParameterHelper.ProcessParams(joinsSql, joins.Arguments, argsList); + args = argsList.ToArray(); } - protected IDictionary GetPropertyCollections(List> temps) - where T : class, IContentBase - { - var versions = new List(); - foreach (var temp in temps) - { - versions.Add(temp.VersionId); - if (temp.PublishedVersionId > 0) - versions.Add(temp.PublishedVersionId); - } - if (versions.Count == 0) return new Dictionary(); + return Sql(InsertBefore(sql.SQL, "WHERE", joinsSql), args); + } - // TODO: This is a bugger of a query and I believe is the main issue with regards to SQL performance drain when querying content - // which is done when rebuilding caches/indexes/etc... in bulk. We are using an "IN" query on umbracoPropertyData.VersionId - // which then performs a Clustered Index Scan on PK_umbracoPropertyData which means it iterates the entire table which can be enormous! - // especially if there are both a lot of content but worse if there is a lot of versions of that content. - // So is it possible to return this property data without doing an index scan on PK_umbracoPropertyData and without iterating every row - // in the table? + private string GetAliasedField(string field, Sql sql) + { + // get alias, if aliased + // + // regex looks for pattern "([\w+].[\w+]) AS ([\w+])" ie "(field) AS (alias)" + // and, if found & a group's field matches the field name, returns the alias + // + // so... if query contains "[umbracoNode].[nodeId] AS [umbracoNode__nodeId]" + // then GetAliased for "[umbracoNode].[nodeId]" returns "[umbracoNode__nodeId]" + + MatchCollection matches = SqlContext.SqlSyntax.AliasRegex.Matches(sql.SQL); + Match? match = matches.FirstOrDefault(m => m.Groups[1].Value.InvariantEquals(field)); + return match == null ? field : match.Groups[2].Value; + } - // get all PropertyDataDto for all definitions / versions - var allPropertyDataDtos = Database.FetchByGroups(versions, Constants.Sql.MaxParameterCount, batch => - SqlContext.Sql() - .Select() - .From() - .WhereIn(x => x.VersionId, batch)) - .ToList(); - - // get PropertyDataDto distinct PropertyTypeDto - var allPropertyTypeIds = allPropertyDataDtos.Select(x => x.PropertyTypeId).Distinct().ToList(); - var allPropertyTypeDtos = Database.FetchByGroups(allPropertyTypeIds, Constants.Sql.MaxParameterCount, batch => - SqlContext.Sql() - .Select(r => r.Select(x => x.DataTypeDto)) - .From() - .InnerJoin().On((left, right) => left.DataTypeId == right.NodeId) - .WhereIn(x => x.Id, batch)); + protected string GetQuotedFieldName(string tableName, string fieldName) => + SqlContext.SqlSyntax.GetQuotedTableName(tableName) + "." + SqlContext.SqlSyntax.GetQuotedColumnName(fieldName); - // index the types for perfs, and assign to PropertyDataDto - var indexedPropertyTypeDtos = allPropertyTypeDtos.ToDictionary(x => x.Id, x => x); - foreach (var a in allPropertyDataDtos) - a.PropertyTypeDto = indexedPropertyTypeDtos[a.PropertyTypeId]; + protected void PersistRelations(TEntity entity) + { + // Get all references from our core built in DataEditors/Property Editors + // Along with seeing if deverlopers want to collect additional references from the DataValueReferenceFactories collection + var trackedRelations = new List(); + trackedRelations.AddRange(_dataValueReferenceFactories.GetAllReferences(entity.Properties, PropertyEditors)); - // now we have - // - the definitions - // - all property data dtos - // - tag editors (Actually ... no we don't since i removed that code, but we don't need them anyways it seems) - // and we need to build the proper property collections + //First delete all auto-relations for this entity + RelationRepository.DeleteByParent(entity.Id, Constants.Conventions.RelationTypes.AutomaticRelationTypes); - return GetPropertyCollections(temps, allPropertyDataDtos); + if (trackedRelations.Count == 0) + { + return; } - private IDictionary GetPropertyCollections(List> temps, IEnumerable allPropertyDataDtos) - where T : class, IContentBase - { - var result = new Dictionary(); - var compositionPropertiesIndex = new Dictionary(); + trackedRelations = trackedRelations.Distinct().ToList(); + var udiToGuids = trackedRelations.Select(x => x.Udi as GuidUdi) + .ToDictionary(x => (Udi)x!, x => x!.Guid); + + //lookup in the DB all INT ids for the GUIDs and chuck into a dictionary + var keyToIds = Database.Fetch(Sql().Select(x => x.NodeId, x => x.UniqueId).From() + .WhereIn(x => x.UniqueId, udiToGuids.Values)) + .ToDictionary(x => x.UniqueId, x => x.NodeId); - // index PropertyDataDto per versionId for perfs - // merge edited and published dtos - var indexedPropertyDataDtos = new Dictionary>(); - foreach (var dto in allPropertyDataDtos) + var allRelationTypes = RelationTypeRepository.GetMany(Array.Empty())? + .ToDictionary(x => x.Alias, x => x); + + IEnumerable toSave = trackedRelations.Select(rel => + { + if (allRelationTypes is null || + !allRelationTypes.TryGetValue(rel.RelationTypeAlias, out IRelationType? relationType)) { - var versionId = dto.VersionId; - if (indexedPropertyDataDtos.TryGetValue(versionId, out var list) == false) - indexedPropertyDataDtos[versionId] = list = new List(); - list.Add(dto); + throw new InvalidOperationException($"The relation type {rel.RelationTypeAlias} does not exist"); } - foreach (var temp in temps) + if (!udiToGuids.TryGetValue(rel.Udi, out Guid guid)) { - // compositionProperties is the property types for the entire composition - // use an index for perfs - if (temp.ContentType is null) - { - continue; - } - if (compositionPropertiesIndex.TryGetValue(temp.ContentType.Id, out var compositionProperties) == false) - compositionPropertiesIndex[temp.ContentType.Id] = compositionProperties = temp.ContentType.CompositionPropertyTypes.ToArray(); - - // map the list of PropertyDataDto to a list of Property - var propertyDataDtos = new List(); - if (indexedPropertyDataDtos.TryGetValue(temp.VersionId, out var propertyDataDtos1)) - { - propertyDataDtos.AddRange(propertyDataDtos1); - if (temp.VersionId == temp.PublishedVersionId) // dirty corner case - propertyDataDtos.AddRange(propertyDataDtos1.Select(x => x.Clone(-1))); - } - if (temp.VersionId != temp.PublishedVersionId && indexedPropertyDataDtos.TryGetValue(temp.PublishedVersionId, out var propertyDataDtos2)) - propertyDataDtos.AddRange(propertyDataDtos2); - var properties = PropertyFactory.BuildEntities(compositionProperties, propertyDataDtos, temp.PublishedVersionId, LanguageRepository).ToList(); - - if (result.ContainsKey(temp.VersionId)) - { - if (ContentRepositoryBase.ThrowOnWarning) - throw new InvalidOperationException($"The query returned multiple property sets for content {temp.Id}, {temp.ContentType.Name}"); - Logger.LogWarning("The query returned multiple property sets for content {ContentId}, {ContentTypeName}", temp.Id, temp.ContentType.Name); - } + return null; // This shouldn't happen! + } - result[temp.VersionId] = new PropertyCollection(properties); + if (!keyToIds.TryGetValue(guid, out var id)) + { + return null; // This shouldn't happen! } - return result; - } + return new ReadOnlyRelation(entity.Id, id, relationType.Id); + }).WhereNotNull(); - protected string InsertBefore(Sql s, string atToken, string insert) - => InsertBefore(s.SQL, atToken, insert); + // Save bulk relations + RelationRepository.SaveBulk(toSave); + } - protected string InsertBefore(string s, string atToken, string insert) + /// + /// Inserts property values for the content entity + /// + /// + /// + /// + /// + /// + /// Used when creating a new entity + /// + protected void InsertPropertyValues(TEntity entity, int publishedVersionId, out bool edited, + out HashSet? editedCultures) + { + // persist the property data + IEnumerable propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, + entity.VersionId, publishedVersionId, entity.Properties, LanguageRepository, out edited, + out editedCultures); + foreach (PropertyDataDto propertyDataDto in propertyDataDtos) { - var pos = s.InvariantIndexOf(atToken); - if (pos < 0) throw new Exception($"Could not find token \"{atToken}\"."); - return s.Insert(pos, insert); + Database.Insert(propertyDataDto); } + // TODO: we can speed this up: Use BulkInsert and then do one SELECT to re-retrieve the property data inserted with assigned IDs. + // This is a perfect thing to benchmark with Benchmark.NET to compare perf between Nuget releases. + } - protected Sql InsertJoins(Sql sql, Sql joins) + /// + /// Used to atomically replace the property values for the entity version specified + /// + /// + /// + /// + /// + /// + protected void ReplacePropertyValues(TEntity entity, int versionId, int publishedVersionId, out bool edited, + out HashSet? editedCultures) + { + // Replace the property data. + // Lookup the data to update with a UPDLOCK (using ForUpdate()) this is because we need to be atomic + // and handle DB concurrency. Doing a clear and then re-insert is prone to concurrency issues. + + Sql propDataSql = SqlContext.Sql().Select("*").From() + .Where(x => x.VersionId == versionId).ForUpdate(); + List? existingPropData = Database.Fetch(propDataSql); + var propertyTypeToPropertyData = + new Dictionary<(int propertyTypeId, int versionId, int? languageId, string? segment), PropertyDataDto>(); + var existingPropDataIds = new List(); + foreach (PropertyDataDto? p in existingPropData) { - var joinsSql = joins.SQL; - var args = sql.Arguments; + existingPropDataIds.Add(p.Id); + propertyTypeToPropertyData[(p.PropertyTypeId, p.VersionId, p.LanguageId, p.Segment)] = p; + } + + IEnumerable propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, + entity.VersionId, publishedVersionId, entity.Properties, LanguageRepository, out edited, + out editedCultures); - // merge args if any - if (joins.Arguments.Length > 0) + foreach (PropertyDataDto propertyDataDto in propertyDataDtos) + { + // Check if this already exists and update, else insert a new one + if (propertyTypeToPropertyData.TryGetValue( + (propertyDataDto.PropertyTypeId, propertyDataDto.VersionId, propertyDataDto.LanguageId, + propertyDataDto.Segment), out PropertyDataDto? propData)) { - var argsList = args.ToList(); - joinsSql = ParameterHelper.ProcessParams(joinsSql, joins.Arguments, argsList); - args = argsList.ToArray(); + propertyDataDto.Id = propData.Id; + Database.Update(propertyDataDto); + } + else + { + // TODO: we can speed this up: Use BulkInsert and then do one SELECT to re-retrieve the property data inserted with assigned IDs. + // This is a perfect thing to benchmark with Benchmark.NET to compare perf between Nuget releases. + Database.Insert(propertyDataDto); } - return Sql(InsertBefore(sql.SQL, "WHERE", joinsSql), args); + // track which ones have been processed + existingPropDataIds.Remove(propertyDataDto.Id); } - private string GetAliasedField(string field, Sql sql) + // For any remaining that haven't been processed they need to be deleted + if (existingPropDataIds.Count > 0) { - // get alias, if aliased - // - // regex looks for pattern "([\w+].[\w+]) AS ([\w+])" ie "(field) AS (alias)" - // and, if found & a group's field matches the field name, returns the alias - // - // so... if query contains "[umbracoNode].[nodeId] AS [umbracoNode__nodeId]" - // then GetAliased for "[umbracoNode].[nodeId]" returns "[umbracoNode__nodeId]" - - var matches = SqlContext.SqlSyntax.AliasRegex.Matches(sql.SQL); - var match = matches.Cast().FirstOrDefault(m => m.Groups[1].Value.InvariantEquals(field)); - return match == null ? field : match.Groups[2].Value; + Database.Execute(SqlContext.Sql().Delete() + .WhereIn(x => x.Id, existingPropDataIds)); } + } - protected string GetQuotedFieldName(string tableName, string fieldName) - { - return SqlContext.SqlSyntax.GetQuotedTableName(tableName) + "." + SqlContext.SqlSyntax.GetQuotedColumnName(fieldName); - } + private class NodeIdKey + { + [Column("id")] public int NodeId { get; set; } - #region UnitOfWork Notification + [Column("uniqueId")] public Guid UniqueId { get; set; } + } - /* - * The reason why EntityRefreshNotification is published from the repository and not the service is because - * the published state of the IContent must be "Publishing" when the event is raised for the cache to handle it correctly. - * This state is changed half way through the repository method, meaning that if we publish the notification - * after the state will be "Published" and the cache won't handle it correctly, - * It wont call OnRepositoryRefreshed with the published flag set to true, the same is true for unpublishing - * where it wont remove the entity from the nucache. - * We can't publish the notification before calling Save method on the repository either, - * because that method sets certain fields such as create date, update date, etc... - */ + #region Versions - /// - /// Publishes a notification, used to publish for caching purposes. - /// - protected void OnUowRefreshedEntity(INotification notification) => _eventAggregator.Publish(notification); + // gets a specific version + public abstract TEntity? GetVersion(int versionId); + // gets all versions, current first + public abstract IEnumerable GetAllVersions(int nodeId); - protected void OnUowRemovingEntity(IContentBase entity) => _eventAggregator.Publish(new ScopedEntityRemoveNotification(entity, new EventMessages())); - #endregion + // gets all versions, current first + public virtual IEnumerable GetAllVersionsSlim(int nodeId, int skip, int take) + => GetAllVersions(nodeId).Skip(skip).Take(take); - #region Classes + // gets all version ids, current first + public virtual IEnumerable GetVersionIds(int nodeId, int maxRows) + { + SqlTemplate template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetVersionIds, + tsql => + tsql.Select(x => x.Id) + .From() + .Where(x => x.NodeId == SqlTemplate.Arg("nodeId")) + .OrderByDescending(x => x.Current) // current '1' comes before others '0' + .AndByDescending(x => x.VersionDate) // most recent first + ); + return Database.Fetch(SqlSyntax.SelectTop(template.Sql(nodeId), maxRows)); + } - protected class TempContent + // deletes a specific version + public virtual void DeleteVersion(int versionId) + { + // TODO: test object node type? + + // get the version we want to delete + SqlTemplate template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetVersion, tsql => + tsql.Select().From() + .Where(x => x.Id == SqlTemplate.Arg("versionId")) + ); + ContentVersionDto? versionDto = + Database.Fetch(template.Sql(new {versionId})).FirstOrDefault(); + + // nothing to delete + if (versionDto == null) { - public TempContent(int id, int versionId, int publishedVersionId, IContentTypeComposition? contentType) - { - Id = id; - VersionId = versionId; - PublishedVersionId = publishedVersionId; - ContentType = contentType; - } - - /// - /// Gets or sets the identifier of the content. - /// - public int Id { get; set; } - - /// - /// Gets or sets the version identifier of the content. - /// - public int VersionId { get; set; } - - /// - /// Gets or sets the published version identifier of the content. - /// - public int PublishedVersionId { get; set; } - - /// - /// Gets or sets the content type. - /// - public IContentTypeComposition? ContentType { get; set; } - - /// - /// Gets or sets the identifier of the template 1 of the content. - /// - public int? Template1Id { get; set; } - - /// - /// Gets or sets the identifier of the template 2 of the content. - /// - public int? Template2Id { get; set; } + return; } - protected class TempContent : TempContent - where T : class, IContentBase + // don't delete the current version + if (versionDto.Current) { - public TempContent(int id, int versionId, int publishedVersionId, IContentTypeComposition? contentType, T? content = null) - : base(id, versionId, publishedVersionId, contentType) - { - Content = content; - } + throw new InvalidOperationException("Cannot delete the current version."); + } + + PerformDeleteVersion(versionDto.NodeId, versionId); + } - /// - /// Gets or sets the associated actual content. - /// - public T? Content { get; set; } + // deletes all versions of an entity, older than a date. + public virtual void DeleteVersions(int nodeId, DateTime versionDate) + { + // TODO: test object node type? + + // get the versions we want to delete, excluding the current one + SqlTemplate template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetVersions, + tsql => + tsql.Select().From().Where(x => + x.NodeId == SqlTemplate.Arg("nodeId") && !x.Current && + x.VersionDate < SqlTemplate.Arg("versionDate")) + ); + List? versionDtos = + Database.Fetch(template.Sql(new {nodeId, versionDate})); + foreach (ContentVersionDto? versionDto in versionDtos) + { + PerformDeleteVersion(versionDto.NodeId, versionDto.Id); } + } - /// - /// For Paging, repositories must support returning different query for the query type specified - /// - /// - /// - protected abstract Sql GetBaseQuery(QueryType queryType); + // actually deletes a version + protected abstract void PerformDeleteVersion(int id, int versionId); - #endregion + #endregion - #region Utilities + #region Count - protected virtual string? EnsureUniqueNodeName(int parentId, string? nodeName, int id = 0) - { - var template = SqlContext.Templates.Get(Cms.Core.Constants.SqlTemplates.VersionableRepository.EnsureUniqueNodeName, tsql => tsql - .Select(x => Alias(x.NodeId, "id"), x => Alias(x.Text!, "name")) - .From() - .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType") && x.ParentId == SqlTemplate.Arg("parentId")) - ); + /// + /// Count descendants of an item. + /// + public int CountDescendants(int parentId, string? contentTypeAlias = null) + { + var pathMatch = parentId == -1 + ? "-1," + : "," + parentId + ","; - var sql = template.Sql(NodeObjectTypeId, parentId); - var names = Database.Fetch(sql); + Sql sql = SqlContext.Sql() + .SelectCount() + .From(); - return SimilarNodeName.GetUniqueName(names, id, nodeName); + if (contentTypeAlias.IsNullOrWhiteSpace()) + { + sql + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .Where(x => x.Path.Contains(pathMatch)); } - - protected virtual int GetNewChildSortOrder(int parentId, int first) + else { - var template = SqlContext.Templates.Get(Cms.Core.Constants.SqlTemplates.VersionableRepository.GetSortOrder, tsql => tsql - .Select("MAX(sortOrder)") - .From() - .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType") && x.ParentId == SqlTemplate.Arg("parentId")) - ); - - var sql = template.Sql(NodeObjectTypeId, parentId); - var sortOrder = Database.ExecuteScalar(sql); - - return (sortOrder + 1) ?? first; + sql + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.ContentTypeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .Where(x => x.Path.Contains(pathMatch)) + .Where(x => x.Alias == contentTypeAlias); } - protected virtual NodeDto GetParentNodeDto(int parentId) - { - var template = SqlContext.Templates.Get(Cms.Core.Constants.SqlTemplates.VersionableRepository.GetParentNode, tsql => tsql - .Select() - .From() - .Where(x => x.NodeId == SqlTemplate.Arg("parentId")) - ); + return Database.ExecuteScalar(sql); + } - var sql = template.Sql(parentId); - var nodeDto = Database.First(sql); + /// + /// Count children of an item. + /// + public int CountChildren(int parentId, string? contentTypeAlias = null) + { + Sql sql = SqlContext.Sql() + .SelectCount() + .From(); - return nodeDto; + if (contentTypeAlias.IsNullOrWhiteSpace()) + { + sql + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .Where(x => x.ParentId == parentId); } - - protected virtual int GetReservedId(Guid uniqueId) + else { - var template = SqlContext.Templates.Get(Cms.Core.Constants.SqlTemplates.VersionableRepository.GetReservedId, tsql => tsql - .Select(x => x.NodeId) - .From() - .Where(x => x.UniqueId == SqlTemplate.Arg("uniqueId") && x.NodeObjectType == Cms.Core.Constants.ObjectTypes.IdReservation) - ); + sql + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.ContentTypeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .Where(x => x.ParentId == parentId) + .Where(x => x.Alias == contentTypeAlias); + } - var sql = template.Sql(new { uniqueId }); - var id = Database.ExecuteScalar(sql); + return Database.ExecuteScalar(sql); + } - return id ?? 0; + /// + /// Count items. + /// + public int Count(string? contentTypeAlias = null) + { + Sql sql = SqlContext.Sql() + .SelectCount() + .From(); + + if (contentTypeAlias.IsNullOrWhiteSpace()) + { + sql + .Where(x => x.NodeObjectType == NodeObjectTypeId); + } + else + { + sql + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.ContentTypeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .Where(x => x.Alias == contentTypeAlias); } - #endregion + return Database.ExecuteScalar(sql); + } - #region Recycle bin + #endregion - public abstract int RecycleBinId { get; } + #region Tags - public virtual IEnumerable? GetRecycleBin() + /// + /// Updates tags for an item. + /// + protected void SetEntityTags(IContentBase entity, ITagRepository tagRepo, IJsonSerializer serializer) + { + foreach (IProperty property in entity.Properties) { - return Get(Query().Where(entity => entity.Trashed)); + TagConfiguration? tagConfiguration = property.GetTagConfiguration(PropertyEditors, DataTypeService); + if (tagConfiguration == null) + { + continue; // not a tags property + } + + if (property.PropertyType.VariesByCulture()) + { + var tags = new List(); + foreach (IPropertyValue pvalue in property.Values) + { + IEnumerable tagsValue = + property.GetTagsValue(PropertyEditors, DataTypeService, serializer, pvalue.Culture); + var languageId = LanguageRepository.GetIdByIsoCode(pvalue.Culture); + IEnumerable cultureTags = tagsValue.Select(x => + new Tag {Group = tagConfiguration.Group, Text = x, LanguageId = languageId}); + tags.AddRange(cultureTags); + } + + tagRepo.Assign(entity.Id, property.PropertyTypeId, tags); + } + else + { + IEnumerable + tagsValue = property.GetTagsValue(PropertyEditors, DataTypeService, serializer); // strings + IEnumerable tags = tagsValue.Select(x => new Tag {Group = tagConfiguration.Group, Text = x}); + tagRepo.Assign(entity.Id, property.PropertyTypeId, tags); + } } + } - #endregion + // TODO: should we do it when un-publishing? or? + /// + /// Clears tags for an item. + /// + protected void ClearEntityTags(IContentBase entity, ITagRepository tagRepo) => tagRepo.RemoveAll(entity.Id); - protected void PersistRelations(TEntity entity) - { - // Get all references from our core built in DataEditors/Property Editors - // Along with seeing if deverlopers want to collect additional references from the DataValueReferenceFactories collection - var trackedRelations = new List(); - trackedRelations.AddRange(_dataValueReferenceFactories.GetAllReferences(entity.Properties, PropertyEditors)); + #endregion - //First delete all auto-relations for this entity - RelationRepository.DeleteByParent(entity.Id, Cms.Core.Constants.Conventions.RelationTypes.AutomaticRelationTypes); + #region UnitOfWork Notification - if (trackedRelations.Count == 0) return; + /* + * The reason why EntityRefreshNotification is published from the repository and not the service is because + * the published state of the IContent must be "Publishing" when the event is raised for the cache to handle it correctly. + * This state is changed half way through the repository method, meaning that if we publish the notification + * after the state will be "Published" and the cache won't handle it correctly, + * It wont call OnRepositoryRefreshed with the published flag set to true, the same is true for unpublishing + * where it wont remove the entity from the nucache. + * We can't publish the notification before calling Save method on the repository either, + * because that method sets certain fields such as create date, update date, etc... + */ - trackedRelations = trackedRelations.Distinct().ToList(); - var udiToGuids = trackedRelations.Select(x => x.Udi as GuidUdi) - .ToDictionary(x => (Udi)x!, x => x!.Guid); + /// + /// Publishes a notification, used to publish for caching purposes. + /// + protected void OnUowRefreshedEntity(INotification notification) => _eventAggregator.Publish(notification); - //lookup in the DB all INT ids for the GUIDs and chuck into a dictionary - var keyToIds = Database.Fetch(Sql().Select(x => x.NodeId, x => x.UniqueId).From().WhereIn(x => x.UniqueId, udiToGuids.Values)) - .ToDictionary(x => x.UniqueId, x => x.NodeId); - var allRelationTypes = RelationTypeRepository.GetMany(Array.Empty())? - .ToDictionary(x => x.Alias, x => x); + protected void OnUowRemovingEntity(IContentBase entity) => + _eventAggregator.Publish(new ScopedEntityRemoveNotification(entity, new EventMessages())); - var toSave = trackedRelations.Select(rel => - { - if (allRelationTypes is null || !allRelationTypes.TryGetValue(rel.RelationTypeAlias, out var relationType)) - throw new InvalidOperationException($"The relation type {rel.RelationTypeAlias} does not exist"); + #endregion - if (!udiToGuids.TryGetValue(rel.Udi, out var guid)) - return null; // This shouldn't happen! + #region Classes - if (!keyToIds.TryGetValue(guid, out var id)) - return null; // This shouldn't happen! + protected class TempContent + { + public TempContent(int id, int versionId, int publishedVersionId, IContentTypeComposition? contentType) + { + Id = id; + VersionId = versionId; + PublishedVersionId = publishedVersionId; + ContentType = contentType; + } - return new ReadOnlyRelation(entity.Id, id, relationType.Id); - }).WhereNotNull(); + /// + /// Gets or sets the identifier of the content. + /// + public int Id { get; set; } - // Save bulk relations - RelationRepository.SaveBulk(toSave); + /// + /// Gets or sets the version identifier of the content. + /// + public int VersionId { get; set; } - } + /// + /// Gets or sets the published version identifier of the content. + /// + public int PublishedVersionId { get; set; } /// - /// Inserts property values for the content entity + /// Gets or sets the content type. /// - /// - /// - /// - /// - /// - /// Used when creating a new entity - /// - protected void InsertPropertyValues(TEntity entity, int publishedVersionId, out bool edited, out HashSet? editedCultures) - { - // persist the property data - var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, publishedVersionId, entity.Properties, LanguageRepository, out edited, out editedCultures); - foreach (var propertyDataDto in propertyDataDtos) - { - Database.Insert(propertyDataDto); - } - // TODO: we can speed this up: Use BulkInsert and then do one SELECT to re-retrieve the property data inserted with assigned IDs. - // This is a perfect thing to benchmark with Benchmark.NET to compare perf between Nuget releases. - } + public IContentTypeComposition? ContentType { get; set; } /// - /// Used to atomically replace the property values for the entity version specified + /// Gets or sets the identifier of the template 1 of the content. /// - /// - /// - /// - /// - /// + public int? Template1Id { get; set; } - protected void ReplacePropertyValues(TEntity entity, int versionId, int publishedVersionId, out bool edited, out HashSet? editedCultures) - { - // Replace the property data. - // Lookup the data to update with a UPDLOCK (using ForUpdate()) this is because we need to be atomic - // and handle DB concurrency. Doing a clear and then re-insert is prone to concurrency issues. - - var propDataSql = SqlContext.Sql().Select("*").From().Where(x => x.VersionId == versionId).ForUpdate(); - var existingPropData = Database.Fetch(propDataSql); - var propertyTypeToPropertyData = new Dictionary<(int propertyTypeId, int versionId, int? languageId, string? segment), PropertyDataDto>(); - var existingPropDataIds = new List(); - foreach (var p in existingPropData) - { - existingPropDataIds.Add(p.Id); - propertyTypeToPropertyData[(p.PropertyTypeId, p.VersionId, p.LanguageId, p.Segment)] = p; - } - var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, publishedVersionId, entity.Properties, LanguageRepository, out edited, out editedCultures); + /// + /// Gets or sets the identifier of the template 2 of the content. + /// + public int? Template2Id { get; set; } + } - foreach (var propertyDataDto in propertyDataDtos) - { + protected class TempContent : TempContent + where T : class, IContentBase + { + public TempContent(int id, int versionId, int publishedVersionId, IContentTypeComposition? contentType, + T? content = null) + : base(id, versionId, publishedVersionId, contentType) => + Content = content; - // Check if this already exists and update, else insert a new one - if (propertyTypeToPropertyData.TryGetValue((propertyDataDto.PropertyTypeId, propertyDataDto.VersionId, propertyDataDto.LanguageId, propertyDataDto.Segment), out var propData)) - { - propertyDataDto.Id = propData.Id; - Database.Update(propertyDataDto); - } - else - { - // TODO: we can speed this up: Use BulkInsert and then do one SELECT to re-retrieve the property data inserted with assigned IDs. - // This is a perfect thing to benchmark with Benchmark.NET to compare perf between Nuget releases. - Database.Insert(propertyDataDto); - } + /// + /// Gets or sets the associated actual content. + /// + public T? Content { get; set; } + } - // track which ones have been processed - existingPropDataIds.Remove(propertyDataDto.Id); - } - // For any remaining that haven't been processed they need to be deleted - if (existingPropDataIds.Count > 0) - { - Database.Execute(SqlContext.Sql().Delete().WhereIn(x => x.Id, existingPropDataIds)); - } + /// + /// For Paging, repositories must support returning different query for the query type specified + /// + /// + /// + protected abstract Sql GetBaseQuery(QueryType queryType); - } + #endregion - private class NodeIdKey - { - [Column("id")] - public int NodeId { get; set; } + #region Utilities - [Column("uniqueId")] - public Guid UniqueId { get; set; } - } + protected virtual string? EnsureUniqueNodeName(int parentId, string? nodeName, int id = 0) + { + SqlTemplate template = SqlContext.Templates.Get( + Constants.SqlTemplates.VersionableRepository.EnsureUniqueNodeName, tsql => tsql + .Select(x => Alias(x.NodeId, "id"), x => Alias(x.Text!, "name")) + .From() + .Where(x => + x.NodeObjectType == SqlTemplate.Arg("nodeObjectType") && + x.ParentId == SqlTemplate.Arg("parentId")) + ); + + Sql sql = template.Sql(NodeObjectTypeId, parentId); + List? names = Database.Fetch(sql); + + return SimilarNodeName.GetUniqueName(names, id, nodeName); + } + + protected virtual int GetNewChildSortOrder(int parentId, int first) + { + SqlTemplate template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetSortOrder, + tsql => tsql + .Select("MAX(sortOrder)") + .From() + .Where(x => + x.NodeObjectType == SqlTemplate.Arg("nodeObjectType") && + x.ParentId == SqlTemplate.Arg("parentId")) + ); + + Sql sql = template.Sql(NodeObjectTypeId, parentId); + var sortOrder = Database.ExecuteScalar(sql); + + return sortOrder + 1 ?? first; + } + + protected virtual NodeDto GetParentNodeDto(int parentId) + { + SqlTemplate template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetParentNode, + tsql => tsql + .Select() + .From() + .Where(x => x.NodeId == SqlTemplate.Arg("parentId")) + ); + + Sql sql = template.Sql(parentId); + NodeDto? nodeDto = Database.First(sql); + + return nodeDto; } + + protected virtual int GetReservedId(Guid uniqueId) + { + SqlTemplate template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetReservedId, + tsql => tsql + .Select(x => x.NodeId) + .From() + .Where(x => + x.UniqueId == SqlTemplate.Arg("uniqueId") && + x.NodeObjectType == Constants.ObjectTypes.IdReservation) + ); + + Sql sql = template.Sql(new {uniqueId}); + var id = Database.ExecuteScalar(sql); + + return id ?? 0; + } + + #endregion + + #region Recycle bin + + public abstract int RecycleBinId { get; } + + public virtual IEnumerable? GetRecycleBin() => Get(Query().Where(entity => entity.Trashed)); + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs index 3a305a637159..b4cc294360d4 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; @@ -8,409 +5,410 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -using Enumerable = System.Linq.Enumerable; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Implements . +/// +internal class ContentTypeCommonRepository : IContentTypeCommonRepository { + private const string CacheKey = + "Umbraco.Core.Persistence.Repositories.Implement.ContentTypeCommonRepository::AllTypes"; + + private readonly AppCaches _appCaches; + private readonly IScopeAccessor _scopeAccessor; + private readonly IShortStringHelper _shortStringHelper; + private readonly ITemplateRepository _templateRepository; + /// - /// Implements . + /// Initializes a new instance of the class. /// - internal class ContentTypeCommonRepository : IContentTypeCommonRepository + public ContentTypeCommonRepository(IScopeAccessor scopeAccessor, ITemplateRepository templateRepository, + AppCaches appCaches, IShortStringHelper shortStringHelper) { - private const string CacheKey = - "Umbraco.Core.Persistence.Repositories.Implement.ContentTypeCommonRepository::AllTypes"; - - private readonly AppCaches _appCaches; - private readonly IScopeAccessor _scopeAccessor; - private readonly IShortStringHelper _shortStringHelper; - private readonly ITemplateRepository _templateRepository; - - /// - /// Initializes a new instance of the class. - /// - public ContentTypeCommonRepository(IScopeAccessor scopeAccessor, ITemplateRepository templateRepository, - AppCaches appCaches, IShortStringHelper shortStringHelper) - { - _scopeAccessor = scopeAccessor; - _templateRepository = templateRepository; - _appCaches = appCaches; - _shortStringHelper = shortStringHelper; - } + _scopeAccessor = scopeAccessor; + _templateRepository = templateRepository; + _appCaches = appCaches; + _shortStringHelper = shortStringHelper; + } - private Scoping.IScope? AmbientScope => _scopeAccessor.AmbientScope; + private IScope? AmbientScope => _scopeAccessor.AmbientScope; - private IUmbracoDatabase? Database => AmbientScope?.Database; + private IUmbracoDatabase? Database => AmbientScope?.Database; - private ISqlContext? SqlContext => AmbientScope?.SqlContext; - //private Sql Sql(string sql, params object[] args) => SqlContext.Sql(sql, args); - //private ISqlSyntaxProvider SqlSyntax => SqlContext.SqlSyntax; - //private IQuery Query() => SqlContext.Query(); + private ISqlContext? SqlContext => AmbientScope?.SqlContext; + //private Sql Sql(string sql, params object[] args) => SqlContext.Sql(sql, args); + //private ISqlSyntaxProvider SqlSyntax => SqlContext.SqlSyntax; + //private IQuery Query() => SqlContext.Query(); - /// - public IEnumerable? GetAllTypes() => - // use a 5 minutes sliding cache - same as FullDataSet cache policy - _appCaches.RuntimeCache.GetCacheItem(CacheKey, GetAllTypesInternal, TimeSpan.FromMinutes(5), true); + /// + public IEnumerable? GetAllTypes() => + // use a 5 minutes sliding cache - same as FullDataSet cache policy + _appCaches.RuntimeCache.GetCacheItem(CacheKey, GetAllTypesInternal, TimeSpan.FromMinutes(5), true); - /// - public void ClearCache() => _appCaches.RuntimeCache.Clear(CacheKey); + /// + public void ClearCache() => _appCaches.RuntimeCache.Clear(CacheKey); - private Sql? Sql() => SqlContext?.Sql(); + private Sql? Sql() => SqlContext?.Sql(); - private IEnumerable GetAllTypesInternal() - { - var contentTypes = new Dictionary(); + private IEnumerable GetAllTypesInternal() + { + var contentTypes = new Dictionary(); - // get content types - Sql? sql1 = Sql()? - .Select(r => r.Select(x => x.NodeDto)) - .From() - .InnerJoin().On((ct, n) => ct.NodeId == n.NodeId) - .OrderBy(x => x.NodeId); + // get content types + Sql? sql1 = Sql()? + .Select(r => r.Select(x => x.NodeDto)) + .From() + .InnerJoin().On((ct, n) => ct.NodeId == n.NodeId) + .OrderBy(x => x.NodeId); - List? contentTypeDtos = Database?.Fetch(sql1); + List? contentTypeDtos = Database?.Fetch(sql1); - // get allowed content types - Sql? sql2 = Sql()? - .Select() - .From() - .OrderBy(x => x.Id); + // get allowed content types + Sql? sql2 = Sql()? + .Select() + .From() + .OrderBy(x => x.Id); - List? allowedDtos = Database?.Fetch(sql2); + List? allowedDtos = Database?.Fetch(sql2); - if (contentTypeDtos is null) + if (contentTypeDtos is null) + { + return contentTypes.Values; + } + + // prepare + // note: same alias could be used for media, content... but always different ids = ok + var aliases = contentTypeDtos.ToDictionary(x => x.NodeId, x => x.Alias); + + // create + var allowedDtoIx = 0; + foreach (ContentTypeDto contentTypeDto in contentTypeDtos) + { + // create content type + IContentTypeComposition contentType; + if (contentTypeDto.NodeDto.NodeObjectType == Constants.ObjectTypes.MediaType) { - return contentTypes.Values; + contentType = ContentTypeFactory.BuildMediaTypeEntity(_shortStringHelper, contentTypeDto); } - // prepare - // note: same alias could be used for media, content... but always different ids = ok - var aliases = Enumerable.ToDictionary(contentTypeDtos, x => x.NodeId, x => x.Alias); + else if (contentTypeDto.NodeDto.NodeObjectType == Constants.ObjectTypes.DocumentType) + { + contentType = ContentTypeFactory.BuildContentTypeEntity(_shortStringHelper, contentTypeDto); + } + else if (contentTypeDto.NodeDto.NodeObjectType == Constants.ObjectTypes.MemberType) + { + contentType = ContentTypeFactory.BuildMemberTypeEntity(_shortStringHelper, contentTypeDto); + } + else + { + throw new PanicException( + $"The node object type {contentTypeDto.NodeDto.NodeObjectType} is not supported"); + } + + contentTypes.Add(contentType.Id, contentType); - // create - var allowedDtoIx = 0; - foreach (ContentTypeDto contentTypeDto in contentTypeDtos) + // map allowed content types + var allowedContentTypes = new List(); + while (allowedDtoIx < allowedDtos?.Count && allowedDtos[allowedDtoIx].Id == contentTypeDto.NodeId) { - // create content type - IContentTypeComposition contentType; - if (contentTypeDto.NodeDto.NodeObjectType == Constants.ObjectTypes.MediaType) + ContentTypeAllowedContentTypeDto allowedDto = allowedDtos[allowedDtoIx]; + if (!aliases.TryGetValue(allowedDto.AllowedId, out var alias)) { - contentType = ContentTypeFactory.BuildMediaTypeEntity(_shortStringHelper, contentTypeDto); - } - else if (contentTypeDto.NodeDto.NodeObjectType == Constants.ObjectTypes.DocumentType) - { - contentType = ContentTypeFactory.BuildContentTypeEntity(_shortStringHelper, contentTypeDto); - } - else if (contentTypeDto.NodeDto.NodeObjectType == Constants.ObjectTypes.MemberType) - { - contentType = ContentTypeFactory.BuildMemberTypeEntity(_shortStringHelper, contentTypeDto); - } - else - { - throw new PanicException( - $"The node object type {contentTypeDto.NodeDto.NodeObjectType} is not supported"); + continue; } - contentTypes.Add(contentType.Id, contentType); + allowedContentTypes.Add(new ContentTypeSort(new Lazy(() => allowedDto.AllowedId), + allowedDto.SortOrder, alias!)); + allowedDtoIx++; + } - // map allowed content types - var allowedContentTypes = new List(); - while (allowedDtoIx < allowedDtos?.Count && allowedDtos[allowedDtoIx].Id == contentTypeDto.NodeId) - { - ContentTypeAllowedContentTypeDto allowedDto = allowedDtos[allowedDtoIx]; - if (!aliases.TryGetValue(allowedDto.AllowedId, out var alias)) - { - continue; - } + contentType.AllowedContentTypes = allowedContentTypes; + } - allowedContentTypes.Add(new ContentTypeSort(new Lazy(() => allowedDto.AllowedId), - allowedDto.SortOrder, alias!)); - allowedDtoIx++; - } + MapTemplates(contentTypes); + MapComposition(contentTypes); + MapGroupsAndProperties(contentTypes); + MapHistoryCleanup(contentTypes); + + // finalize + foreach (IContentTypeComposition contentType in contentTypes.Values) + { + contentType.ResetDirtyProperties(false); + } + + return contentTypes.Values; + } - contentType.AllowedContentTypes = allowedContentTypes; + private void MapHistoryCleanup(Dictionary contentTypes) + { + // get templates + Sql? sql1 = Sql()? + .Select() + .From() + .OrderBy(x => x.ContentTypeId); + + List? contentVersionCleanupPolicyDtos = + Database?.Fetch(sql1); + + var contentVersionCleanupPolicyDictionary = + contentVersionCleanupPolicyDtos?.ToDictionary(x => x.ContentTypeId); + foreach (IContentTypeComposition c in contentTypes.Values) + { + if (!(c is ContentType contentType)) + { + continue; } - MapTemplates(contentTypes); - MapComposition(contentTypes); - MapGroupsAndProperties(contentTypes); - MapHistoryCleanup(contentTypes); + var historyCleanup = new HistoryCleanup(); - // finalize - foreach (IContentTypeComposition contentType in contentTypes.Values) + if (contentVersionCleanupPolicyDictionary is not null && + contentVersionCleanupPolicyDictionary.TryGetValue(contentType.Id, + out ContentVersionCleanupPolicyDto? versionCleanup)) { - contentType.ResetDirtyProperties(false); + historyCleanup.PreventCleanup = versionCleanup.PreventCleanup; + historyCleanup.KeepAllVersionsNewerThanDays = versionCleanup.KeepAllVersionsNewerThanDays; + historyCleanup.KeepLatestVersionPerDayForDays = versionCleanup.KeepLatestVersionPerDayForDays; } - return contentTypes.Values; + contentType.HistoryCleanup = historyCleanup; } + } - private void MapHistoryCleanup(Dictionary contentTypes) + private void MapTemplates(Dictionary contentTypes) + { + // get templates + Sql? sql1 = Sql()? + .Select() + .From() + .OrderBy(x => x.ContentTypeNodeId); + + List? templateDtos = Database?.Fetch(sql1); + //var templates = templateRepository.GetMany(templateDtos.Select(x => x.TemplateNodeId).ToArray()).ToDictionary(x => x.Id, x => x); + IEnumerable? allTemplates = _templateRepository.GetMany(); + if (allTemplates is null) { - // get templates - Sql? sql1 = Sql()? - .Select() - .From() - .OrderBy(x => x.ContentTypeId); + return; + } - var contentVersionCleanupPolicyDtos = Database?.Fetch(sql1); + var templates = allTemplates.ToDictionary(x => x.Id, x => x); + var templateDtoIx = 0; - var contentVersionCleanupPolicyDictionary = - contentVersionCleanupPolicyDtos?.ToDictionary(x => x.ContentTypeId); - foreach (IContentTypeComposition c in contentTypes.Values) + foreach (IContentTypeComposition c in contentTypes.Values) + { + if (!(c is IContentType contentType)) { - if (!(c is ContentType contentType)) + continue; + } + + // map allowed templates + var allowedTemplates = new List(); + var defaultTemplateId = 0; + while (templateDtoIx < templateDtos?.Count && + templateDtos[templateDtoIx].ContentTypeNodeId == contentType.Id) + { + ContentTypeTemplateDto allowedDto = templateDtos[templateDtoIx]; + templateDtoIx++; + if (!templates.TryGetValue(allowedDto.TemplateNodeId, out ITemplate? template)) { continue; } - var historyCleanup = new HistoryCleanup(); + allowedTemplates.Add(template); - if (contentVersionCleanupPolicyDictionary is not null && contentVersionCleanupPolicyDictionary.TryGetValue(contentType.Id, out var versionCleanup)) + if (allowedDto.IsDefault) { - historyCleanup.PreventCleanup = versionCleanup.PreventCleanup; - historyCleanup.KeepAllVersionsNewerThanDays = versionCleanup.KeepAllVersionsNewerThanDays; - historyCleanup.KeepLatestVersionPerDayForDays = versionCleanup.KeepLatestVersionPerDayForDays; + defaultTemplateId = template.Id; } - - contentType.HistoryCleanup = historyCleanup; } + + contentType.AllowedTemplates = allowedTemplates; + contentType.DefaultTemplateId = defaultTemplateId; } + } + + private void MapComposition(IDictionary contentTypes) + { + // get parent/child + Sql? sql1 = Sql()? + .Select() + .From() + .OrderBy(x => x.ChildId); - private void MapTemplates(Dictionary contentTypes) + List? compositionDtos = Database?.Fetch(sql1); + + // map + var compositionIx = 0; + foreach (IContentTypeComposition contentType in contentTypes.Values) { - // get templates - Sql? sql1 = Sql()? - .Select() - .From() - .OrderBy(x => x.ContentTypeNodeId); - - List? templateDtos = Database?.Fetch(sql1); - //var templates = templateRepository.GetMany(templateDtos.Select(x => x.TemplateNodeId).ToArray()).ToDictionary(x => x.Id, x => x); - var allTemplates = _templateRepository.GetMany(); - if (allTemplates is null) + while (compositionIx < compositionDtos?.Count && + compositionDtos[compositionIx].ChildId == contentType.Id) { - return; - } - var templates = Enumerable.ToDictionary(allTemplates, x => x.Id, x => x); - var templateDtoIx = 0; + ContentType2ContentTypeDto parentDto = compositionDtos[compositionIx]; + compositionIx++; - foreach (IContentTypeComposition c in contentTypes.Values) - { - if (!(c is IContentType contentType)) + if (!contentTypes.TryGetValue(parentDto.ParentId, out IContentTypeComposition? parentContentType)) { continue; } - // map allowed templates - var allowedTemplates = new List(); - var defaultTemplateId = 0; - while (templateDtoIx < templateDtos?.Count && - templateDtos[templateDtoIx].ContentTypeNodeId == contentType.Id) - { - ContentTypeTemplateDto allowedDto = templateDtos[templateDtoIx]; - templateDtoIx++; - if (!templates.TryGetValue(allowedDto.TemplateNodeId, out ITemplate? template)) - { - continue; - } - - allowedTemplates.Add(template); - - if (allowedDto.IsDefault) - { - defaultTemplateId = template.Id; - } - } - - contentType.AllowedTemplates = allowedTemplates; - contentType.DefaultTemplateId = defaultTemplateId; + contentType.AddContentType(parentContentType); } } + } - private void MapComposition(IDictionary contentTypes) + private void MapGroupsAndProperties(IDictionary contentTypes) + { + Sql? sql1 = Sql()? + .Select() + .From() + .InnerJoin() + .On((ptg, ct) => ptg.ContentTypeNodeId == ct.NodeId) + .OrderBy(x => x.NodeId) + .AndBy(x => x.SortOrder, x => x.Id); + + List? groupDtos = Database?.Fetch(sql1); + + Sql? sql2 = Sql()? + .Select(r => r.Select(x => x.DataTypeDto, r1 => r1.Select(x => x.NodeDto))) + .AndSelect() + .From() + .InnerJoin().On((pt, dt) => pt.DataTypeId == dt.NodeId) + .InnerJoin().On((dt, n) => dt.NodeId == n.NodeId) + .InnerJoin() + .On((pt, ct) => pt.ContentTypeId == ct.NodeId) + .LeftJoin() + .On((pt, ptg) => pt.PropertyTypeGroupId == ptg.Id) + .LeftJoin() + .On((pt, mpt) => pt.Id == mpt.PropertyTypeId) + .OrderBy(x => x.NodeId) + .AndBy< + PropertyTypeGroupDto>(x => x.SortOrder, + x => x.Id) // NULLs will come first or last, never mind, we deal with it below + .AndBy(x => x.SortOrder, x => x.Id); + + List? propertyDtos = Database?.Fetch(sql2); + Dictionary builtinProperties = + ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper); + + var groupIx = 0; + var propertyIx = 0; + foreach (IContentTypeComposition contentType in contentTypes.Values) { - // get parent/child - Sql? sql1 = Sql()? - .Select() - .From() - .OrderBy(x => x.ChildId); + // only IContentType is publishing + var isPublishing = contentType is IContentType; - List? compositionDtos = Database?.Fetch(sql1); - - // map - var compositionIx = 0; - foreach (IContentTypeComposition contentType in contentTypes.Values) + // get group-less properties (in case NULL is ordered first) + var noGroupPropertyTypes = new PropertyTypeCollection(isPublishing); + while (propertyIx < propertyDtos?.Count && propertyDtos[propertyIx].ContentTypeId == contentType.Id && + propertyDtos[propertyIx].PropertyTypeGroupId == null) { - while (compositionIx < compositionDtos?.Count && - compositionDtos[compositionIx].ChildId == contentType.Id) - { - ContentType2ContentTypeDto parentDto = compositionDtos[compositionIx]; - compositionIx++; - - if (!contentTypes.TryGetValue(parentDto.ParentId, out IContentTypeComposition? parentContentType)) - { - continue; - } - - contentType.AddContentType(parentContentType); - } + noGroupPropertyTypes.Add(MapPropertyType(contentType, propertyDtos[propertyIx], builtinProperties)); + propertyIx++; } - } - private void MapGroupsAndProperties(IDictionary contentTypes) - { - Sql? sql1 = Sql()? - .Select() - .From() - .InnerJoin() - .On((ptg, ct) => ptg.ContentTypeNodeId == ct.NodeId) - .OrderBy(x => x.NodeId) - .AndBy(x => x.SortOrder, x => x.Id); - - List? groupDtos = Database?.Fetch(sql1); - - Sql? sql2 = Sql()? - .Select(r => r.Select(x => x.DataTypeDto, r1 => r1.Select(x => x.NodeDto))) - .AndSelect() - .From() - .InnerJoin().On((pt, dt) => pt.DataTypeId == dt.NodeId) - .InnerJoin().On((dt, n) => dt.NodeId == n.NodeId) - .InnerJoin() - .On((pt, ct) => pt.ContentTypeId == ct.NodeId) - .LeftJoin() - .On((pt, ptg) => pt.PropertyTypeGroupId == ptg.Id) - .LeftJoin() - .On((pt, mpt) => pt.Id == mpt.PropertyTypeId) - .OrderBy(x => x.NodeId) - .AndBy< - PropertyTypeGroupDto>(x => x.SortOrder, - x => x.Id) // NULLs will come first or last, never mind, we deal with it below - .AndBy(x => x.SortOrder, x => x.Id); - - List? propertyDtos = Database?.Fetch(sql2); - Dictionary builtinProperties = - ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper); - - var groupIx = 0; - var propertyIx = 0; - foreach (IContentTypeComposition contentType in contentTypes.Values) + // get groups and their properties + var groupCollection = new PropertyGroupCollection(); + while (groupIx < groupDtos?.Count && groupDtos[groupIx].ContentTypeNodeId == contentType.Id) { - // only IContentType is publishing - var isPublishing = contentType is IContentType; + PropertyGroup group = MapPropertyGroup(groupDtos[groupIx], isPublishing); + groupCollection.Add(group); + groupIx++; - // get group-less properties (in case NULL is ordered first) - var noGroupPropertyTypes = new PropertyTypeCollection(isPublishing); - while (propertyIx < propertyDtos?.Count && propertyDtos[propertyIx].ContentTypeId == contentType.Id && - propertyDtos[propertyIx].PropertyTypeGroupId == null) + while (propertyIx < propertyDtos?.Count && + propertyDtos[propertyIx].ContentTypeId == contentType.Id && + propertyDtos[propertyIx].PropertyTypeGroupId == group.Id) { - noGroupPropertyTypes.Add(MapPropertyType(contentType, propertyDtos[propertyIx], builtinProperties)); + group.PropertyTypes?.Add(MapPropertyType(contentType, propertyDtos[propertyIx], + builtinProperties)); propertyIx++; } + } - // get groups and their properties - var groupCollection = new PropertyGroupCollection(); - while (groupIx < groupDtos?.Count && groupDtos[groupIx].ContentTypeNodeId == contentType.Id) - { - PropertyGroup group = MapPropertyGroup(groupDtos[groupIx], isPublishing); - groupCollection.Add(group); - groupIx++; + contentType.PropertyGroups = groupCollection; - while (propertyIx < propertyDtos?.Count && - propertyDtos[propertyIx].ContentTypeId == contentType.Id && - propertyDtos[propertyIx].PropertyTypeGroupId == group.Id) - { - group.PropertyTypes?.Add(MapPropertyType(contentType, propertyDtos[propertyIx], - builtinProperties)); - propertyIx++; - } - } + // get group-less properties (in case NULL is ordered last) + while (propertyIx < propertyDtos?.Count && propertyDtos[propertyIx].ContentTypeId == contentType.Id && + propertyDtos[propertyIx].PropertyTypeGroupId == null) + { + noGroupPropertyTypes.Add(MapPropertyType(contentType, propertyDtos[propertyIx], builtinProperties)); + propertyIx++; + } - contentType.PropertyGroups = groupCollection; + contentType.NoGroupPropertyTypes = noGroupPropertyTypes; - // get group-less properties (in case NULL is ordered last) - while (propertyIx < propertyDtos?.Count && propertyDtos[propertyIx].ContentTypeId == contentType.Id && - propertyDtos[propertyIx].PropertyTypeGroupId == null) + // ensure builtin properties + if (contentType is IMemberType memberType) + { + // ensure that property types exist (ok if they already exist) + foreach ((var alias, PropertyType propertyType) in builtinProperties) { - noGroupPropertyTypes.Add(MapPropertyType(contentType, propertyDtos[propertyIx], builtinProperties)); - propertyIx++; - } - - contentType.NoGroupPropertyTypes = noGroupPropertyTypes; + var added = memberType.AddPropertyType(propertyType, + Constants.Conventions.Member.StandardPropertiesGroupAlias, + Constants.Conventions.Member.StandardPropertiesGroupName); - // ensure builtin properties - if (contentType is IMemberType memberType) - { - // ensure that property types exist (ok if they already exist) - foreach ((var alias, PropertyType propertyType) in builtinProperties) + if (added) { - - var added = memberType.AddPropertyType(propertyType, - Constants.Conventions.Member.StandardPropertiesGroupAlias, - Constants.Conventions.Member.StandardPropertiesGroupName); - - if (added) - { - memberType.SetIsSensitiveProperty(alias, false); - memberType.SetMemberCanEditProperty(alias, false); - memberType.SetMemberCanViewProperty(alias, false); - } + memberType.SetIsSensitiveProperty(alias, false); + memberType.SetMemberCanEditProperty(alias, false); + memberType.SetMemberCanViewProperty(alias, false); } } } } + } - private PropertyGroup MapPropertyGroup(PropertyTypeGroupDto dto, bool isPublishing) => - new PropertyGroup(new PropertyTypeCollection(isPublishing)) - { - Id = dto.Id, - Key = dto.UniqueId, - Type = (PropertyGroupType)dto.Type, - Name = dto.Text, - Alias = dto.Alias, - SortOrder = dto.SortOrder - }; - - private PropertyType MapPropertyType(IContentTypeComposition contentType, PropertyTypeCommonDto dto, - IDictionary builtinProperties) + private PropertyGroup MapPropertyGroup(PropertyTypeGroupDto dto, bool isPublishing) => + new(new PropertyTypeCollection(isPublishing)) { - var groupId = dto.PropertyTypeGroupId; + Id = dto.Id, + Key = dto.UniqueId, + Type = (PropertyGroupType)dto.Type, + Name = dto.Text, + Alias = dto.Alias, + SortOrder = dto.SortOrder + }; + + private PropertyType MapPropertyType(IContentTypeComposition contentType, PropertyTypeCommonDto dto, + IDictionary builtinProperties) + { + var groupId = dto.PropertyTypeGroupId; - var readonlyStorageType = builtinProperties.TryGetValue(dto.Alias!, out PropertyType? propertyType); - ValueStorageType storageType = readonlyStorageType - ? propertyType!.ValueStorageType - : Enum.Parse(dto.DataTypeDto.DbType); + var readonlyStorageType = builtinProperties.TryGetValue(dto.Alias!, out PropertyType? propertyType); + ValueStorageType storageType = readonlyStorageType + ? propertyType!.ValueStorageType + : Enum.Parse(dto.DataTypeDto.DbType); - if (contentType is IMemberType memberType && dto.Alias is not null) - { - memberType.SetIsSensitiveProperty(dto.Alias, dto.IsSensitive); - memberType.SetMemberCanEditProperty(dto.Alias, dto.CanEdit); - memberType.SetMemberCanViewProperty(dto.Alias, dto.ViewOnProfile); - } - - return new - PropertyType(_shortStringHelper, dto.DataTypeDto.EditorAlias, storageType, readonlyStorageType, - dto.Alias) - { - Description = dto.Description, - DataTypeId = dto.DataTypeId, - DataTypeKey = dto.DataTypeDto.NodeDto.UniqueId, - Id = dto.Id, - Key = dto.UniqueId, - Mandatory = dto.Mandatory, - MandatoryMessage = dto.MandatoryMessage, - Name = dto.Name ?? string.Empty, - PropertyGroupId = groupId.HasValue ? new Lazy(() => groupId.Value) : null, - SortOrder = dto.SortOrder, - ValidationRegExp = dto.ValidationRegExp, - ValidationRegExpMessage = dto.ValidationRegExpMessage, - Variations = (ContentVariation)dto.Variations, - LabelOnTop = dto.LabelOnTop - }; + if (contentType is IMemberType memberType && dto.Alias is not null) + { + memberType.SetIsSensitiveProperty(dto.Alias, dto.IsSensitive); + memberType.SetMemberCanEditProperty(dto.Alias, dto.CanEdit); + memberType.SetMemberCanViewProperty(dto.Alias, dto.ViewOnProfile); } + + return new + PropertyType(_shortStringHelper, dto.DataTypeDto.EditorAlias, storageType, readonlyStorageType, + dto.Alias) + { + Description = dto.Description, + DataTypeId = dto.DataTypeId, + DataTypeKey = dto.DataTypeDto.NodeDto.UniqueId, + Id = dto.Id, + Key = dto.UniqueId, + Mandatory = dto.Mandatory, + MandatoryMessage = dto.MandatoryMessage, + Name = dto.Name ?? string.Empty, + PropertyGroupId = groupId.HasValue ? new Lazy(() => groupId.Value) : null, + SortOrder = dto.SortOrder, + ValidationRegExp = dto.ValidationRegExp, + ValidationRegExpMessage = dto.ValidationRegExpMessage, + Variations = (ContentVariation)dto.Variations, + LabelOnTop = dto.LabelOnTop + }; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs index 9e2f0257b6ef..f57d89f46fe9 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -15,306 +12,309 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents a repository for doing CRUD operations for +/// +internal class ContentTypeRepository : ContentTypeRepositoryBase, IContentTypeRepository { - /// - /// Represents a repository for doing CRUD operations for - /// - internal class ContentTypeRepository : ContentTypeRepositoryBase, IContentTypeRepository + public ContentTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, + IContentTypeCommonRepository commonRepository, ILanguageRepository languageRepository, + IShortStringHelper shortStringHelper) + : base(scopeAccessor, cache, logger, commonRepository, languageRepository, shortStringHelper) { - public ContentTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository, ILanguageRepository languageRepository, IShortStringHelper shortStringHelper) - : base(scopeAccessor, cache, logger, commonRepository, languageRepository, shortStringHelper) - { } - - protected override bool SupportsPublishing => ContentType.SupportsPublishingConst; - - protected override IRepositoryCachePolicy CreateCachePolicy() - { - return new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ true); - } - - // every GetExists method goes cachePolicy.GetSomething which in turns goes PerformGetAll, - // since this is a FullDataSet policy - and everything is cached - // so here, - // every PerformGet/Exists just GetMany() and then filters - // except PerformGetAll which is the one really doing the job + } - // TODO: the filtering is highly inefficient as we deep-clone everything - // there should be a way to GetMany(predicate) right from the cache policy! - // and ah, well, this all caching should be refactored + the cache refreshers - // should to repository.Clear() not deal with magic caches by themselves + protected override bool SupportsPublishing => ContentType.SupportsPublishingConst; - protected override IContentType? PerformGet(int id) - => GetMany()?.FirstOrDefault(x => x.Id == id); + protected override Guid NodeObjectTypeId => Constants.ObjectTypes.DocumentType; - protected override IContentType? PerformGet(Guid id) - => GetMany()?.FirstOrDefault(x => x.Key == id); + /// + public IEnumerable GetByQuery(IQuery query) + { + var ints = PerformGetByQuery(query).ToArray(); + return ints.Length > 0 ? GetMany(ints) ?? Enumerable.Empty() : Enumerable.Empty(); + } - protected override IContentType? PerformGet(string alias) - => GetMany()?.FirstOrDefault(x => x.Alias.InvariantEquals(alias)); + /// + /// Gets all property type aliases. + /// + /// + public IEnumerable GetAllPropertyTypeAliases() => + Database.Fetch("SELECT DISTINCT Alias FROM cmsPropertyType ORDER BY Alias"); - protected override bool PerformExists(Guid id) - => GetMany()?.FirstOrDefault(x => x.Key == id) != null; + /// + /// Gets all content type aliases + /// + /// + /// If this list is empty, it will return all content type aliases for media, members and content, otherwise + /// it will only return content type aliases for the object types specified + /// + /// + public IEnumerable GetAllContentTypeAliases(params Guid[] objectTypes) + { + Sql sql = Sql() + .Select("cmsContentType.alias") + .From() + .InnerJoin() + .On(dto => dto.NodeId, dto => dto.NodeId); - protected override IEnumerable? GetAllWithFullCachePolicy() + if (objectTypes.Any()) { - return CommonRepository.GetAllTypes()?.OfType(); + sql = sql.WhereIn(dto => dto.NodeObjectType, objectTypes); } - protected override IEnumerable? PerformGetAll(params Guid[]? ids) + return Database.Fetch(sql); + } + + public IEnumerable GetAllContentTypeIds(string[] aliases) + { + if (aliases.Length == 0) { - var all = GetMany(); - return ids?.Any() ?? false ? all?.Where(x => ids.Contains(x.Key)) : all; + return Enumerable.Empty(); } - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var baseQuery = GetBaseQuery(false); - var translator = new SqlTranslator(baseQuery, query); - var sql = translator.Translate(); - var ids = Database.Fetch(sql).Distinct().ToArray(); + Sql sql = Sql() + .Select(x => x.NodeId) + .From() + .InnerJoin() + .On(dto => dto.NodeId, dto => dto.NodeId) + .Where(dto => aliases.Contains(dto.Alias)); - return ids.Length > 0 ? GetMany(ids)?.OrderBy(x => x.Name) ?? Enumerable.Empty() : Enumerable.Empty(); - } + return Database.Fetch(sql); + } - /// - public IEnumerable GetByQuery(IQuery query) - { - var ints = PerformGetByQuery(query).ToArray(); - return ints.Length > 0 ? GetMany(ints) ?? Enumerable.Empty() : Enumerable.Empty(); - } + protected override IRepositoryCachePolicy CreateCachePolicy() => + new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, + GetEntityId, /*expires:*/ true); - protected IEnumerable PerformGetByQuery(IQuery query) - { - // used by DataTypeService to remove properties - // from content types if they have a deleted data type - see - // notes in DataTypeService.Delete as it's a bit weird - - var sqlClause = Sql() - .SelectAll() - .From() - .LeftJoin() - .On(left => left.Id, right => right.PropertyTypeGroupId) - .InnerJoin() - .On(left => left.DataTypeId, right => right.NodeId); - - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate() - .OrderBy(x => x.PropertyTypeGroupId); - - return Database - .FetchOneToMany(x => x.PropertyTypeDtos, sql) - .Select(x => x.ContentTypeNodeId).Distinct(); - } + // every GetExists method goes cachePolicy.GetSomething which in turns goes PerformGetAll, + // since this is a FullDataSet policy - and everything is cached + // so here, + // every PerformGet/Exists just GetMany() and then filters + // except PerformGetAll which is the one really doing the job - /// - /// Gets all property type aliases. - /// - /// - public IEnumerable GetAllPropertyTypeAliases() - { - return Database.Fetch("SELECT DISTINCT Alias FROM cmsPropertyType ORDER BY Alias"); - } + // TODO: the filtering is highly inefficient as we deep-clone everything + // there should be a way to GetMany(predicate) right from the cache policy! + // and ah, well, this all caching should be refactored + the cache refreshers + // should to repository.Clear() not deal with magic caches by themselves - /// - /// Gets all content type aliases - /// - /// - /// If this list is empty, it will return all content type aliases for media, members and content, otherwise - /// it will only return content type aliases for the object types specified - /// - /// - public IEnumerable GetAllContentTypeAliases(params Guid[] objectTypes) - { - var sql = Sql() - .Select("cmsContentType.alias") - .From() - .InnerJoin() - .On(dto => dto.NodeId, dto => dto.NodeId); + protected override IContentType? PerformGet(int id) + => GetMany()?.FirstOrDefault(x => x.Id == id); - if (objectTypes.Any()) - { - sql = sql.WhereIn(dto => dto.NodeObjectType, objectTypes); - } + protected override IContentType? PerformGet(Guid id) + => GetMany()?.FirstOrDefault(x => x.Key == id); - return Database.Fetch(sql); - } + protected override IContentType? PerformGet(string alias) + => GetMany()?.FirstOrDefault(x => x.Alias.InvariantEquals(alias)); - public IEnumerable GetAllContentTypeIds(string[] aliases) - { - if (aliases.Length == 0) return Enumerable.Empty(); + protected override bool PerformExists(Guid id) + => GetMany()?.FirstOrDefault(x => x.Key == id) != null; - var sql = Sql() - .Select(x => x.NodeId) - .From() - .InnerJoin() - .On(dto => dto.NodeId, dto => dto.NodeId) - .Where(dto => aliases.Contains(dto.Alias)); + protected override IEnumerable? GetAllWithFullCachePolicy() => + CommonRepository.GetAllTypes()?.OfType(); - return Database.Fetch(sql); - } + protected override IEnumerable? PerformGetAll(params Guid[]? ids) + { + IEnumerable? all = GetMany(); + return ids?.Any() ?? false ? all?.Where(x => ids.Contains(x.Key)) : all; + } - protected override Sql GetBaseQuery(bool isCount) - { - var sql = Sql(); + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql baseQuery = GetBaseQuery(false); + var translator = new SqlTranslator(baseQuery, query); + Sql sql = translator.Translate(); + var ids = Database.Fetch(sql).Distinct().ToArray(); + + return ids.Length > 0 + ? GetMany(ids)?.OrderBy(x => x.Name) ?? Enumerable.Empty() + : Enumerable.Empty(); + } - sql = isCount - ? sql.SelectCount() - : sql.Select(x => x.NodeId); + protected IEnumerable PerformGetByQuery(IQuery query) + { + // used by DataTypeService to remove properties + // from content types if they have a deleted data type - see + // notes in DataTypeService.Delete as it's a bit weird + + Sql sqlClause = Sql() + .SelectAll() + .From() + .LeftJoin() + .On(left => left.Id, right => right.PropertyTypeGroupId) + .InnerJoin() + .On(left => left.DataTypeId, right => right.NodeId); + + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate() + .OrderBy(x => x.PropertyTypeGroupId); + + return Database + .FetchOneToMany(x => x.PropertyTypeDtos, sql) + .Select(x => x.ContentTypeNodeId).Distinct(); + } - sql - .From() - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .LeftJoin().On(left => left.ContentTypeNodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId); + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); - return sql; - } + sql = isCount + ? sql.SelectCount() + : sql.Select(x => x.NodeId); - protected override string GetBaseWhereClause() - { - return $"{Constants.DatabaseSchema.Tables.Node}.id = @id"; - } + sql + .From() + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .LeftJoin() + .On(left => left.ContentTypeNodeId, right => right.NodeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId); - protected override IEnumerable GetDeleteClauses() - { - var l = (List) base.GetDeleteClauses(); // we know it's a list - l.Add("DELETE FROM cmsDocumentType WHERE contentTypeNodeId = @id"); - l.Add("DELETE FROM cmsContentType WHERE nodeId = @id"); - l.Add("DELETE FROM umbracoNode WHERE id = @id"); - return l; - } + return sql; + } - protected override Guid NodeObjectTypeId => Cms.Core.Constants.ObjectTypes.DocumentType; + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Node}.id = @id"; - /// - /// Deletes a content type - /// - /// - /// - /// First checks for children and removes those first - /// - protected override void PersistDeletedItem(IContentType entity) + protected override IEnumerable GetDeleteClauses() + { + var l = (List)base.GetDeleteClauses(); // we know it's a list + l.Add("DELETE FROM cmsDocumentType WHERE contentTypeNodeId = @id"); + l.Add("DELETE FROM cmsContentType WHERE nodeId = @id"); + l.Add("DELETE FROM umbracoNode WHERE id = @id"); + return l; + } + + /// + /// Deletes a content type + /// + /// + /// + /// First checks for children and removes those first + /// + protected override void PersistDeletedItem(IContentType entity) + { + IQuery query = Query().Where(x => x.ParentId == entity.Id); + IEnumerable? children = Get(query); + if (children is not null) { - var query = Query().Where(x => x.ParentId == entity.Id); - var children = Get(query); - if (children is not null) + foreach (IContentType child in children) { - foreach (var child in children) - { - PersistDeletedItem(child); - } + PersistDeletedItem(child); } - - //Before we call the base class methods to run all delete clauses, we need to first - // delete all of the property data associated with this document type. Normally this will - // be done in the ContentTypeService by deleting all associated content first, but in some cases - // like when we switch a document type, there is property data left over that is linked - // to the previous document type. So we need to ensure it's removed. - var sql = Sql() - .Select("DISTINCT " + Cms.Core.Constants.DatabaseSchema.Tables.PropertyData + ".propertytypeid") - .From() - .InnerJoin() - .On(dto => dto.PropertyTypeId, dto => dto.Id) - .InnerJoin() - .On(dto => dto.NodeId, dto => dto.ContentTypeId) - .Where(dto => dto.NodeId == entity.Id); - - //Delete all PropertyData where propertytypeid EXISTS in the subquery above - Database.Execute(SqlSyntax.GetDeleteSubquery(Cms.Core.Constants.DatabaseSchema.Tables.PropertyData, "propertytypeid", sql)); - - base.PersistDeletedItem(entity); } - protected override void PersistNewItem(IContentType entity) + //Before we call the base class methods to run all delete clauses, we need to first + // delete all of the property data associated with this document type. Normally this will + // be done in the ContentTypeService by deleting all associated content first, but in some cases + // like when we switch a document type, there is property data left over that is linked + // to the previous document type. So we need to ensure it's removed. + Sql sql = Sql() + .Select("DISTINCT " + Constants.DatabaseSchema.Tables.PropertyData + ".propertytypeid") + .From() + .InnerJoin() + .On(dto => dto.PropertyTypeId, dto => dto.Id) + .InnerJoin() + .On(dto => dto.NodeId, dto => dto.ContentTypeId) + .Where(dto => dto.NodeId == entity.Id); + + //Delete all PropertyData where propertytypeid EXISTS in the subquery above + Database.Execute(SqlSyntax.GetDeleteSubquery(Constants.DatabaseSchema.Tables.PropertyData, "propertytypeid", + sql)); + + base.PersistDeletedItem(entity); + } + + protected override void PersistNewItem(IContentType entity) + { + if (string.IsNullOrWhiteSpace(entity.Alias)) { - if (string.IsNullOrWhiteSpace(entity.Alias)) - { - var ex = new Exception($"ContentType '{entity.Name}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias."); - Logger.LogError("ContentType '{EntityName}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias.", entity.Name); - throw ex; - } + var ex = new Exception( + $"ContentType '{entity.Name}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias."); + Logger.LogError( + "ContentType '{EntityName}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias.", + entity.Name); + throw ex; + } - entity.AddingEntity(); + entity.AddingEntity(); - PersistNewBaseContentType(entity); - PersistTemplates(entity, false); - PersistHistoryCleanup(entity); + PersistNewBaseContentType(entity); + PersistTemplates(entity, false); + PersistHistoryCleanup(entity); - entity.ResetDirtyProperties(); - } + entity.ResetDirtyProperties(); + } - protected void PersistTemplates(IContentType entity, bool clearAll) - { - // remove and insert, if required - Database.Delete("WHERE contentTypeNodeId = @Id", new { Id = entity.Id }); + protected void PersistTemplates(IContentType entity, bool clearAll) + { + // remove and insert, if required + Database.Delete("WHERE contentTypeNodeId = @Id", new {entity.Id}); - // we could do it all in foreach if we assume that the default template is an allowed template?? - var defaultTemplateId = entity.DefaultTemplateId; - if (defaultTemplateId > 0) - { - Database.Insert(new ContentTypeTemplateDto - { - ContentTypeNodeId = entity.Id, - TemplateNodeId = defaultTemplateId, - IsDefault = true - }); - } - foreach (var template in entity.AllowedTemplates?.Where(x => x != null && x.Id != defaultTemplateId) ?? Array.Empty()) + // we could do it all in foreach if we assume that the default template is an allowed template?? + var defaultTemplateId = entity.DefaultTemplateId; + if (defaultTemplateId > 0) + { + Database.Insert(new ContentTypeTemplateDto { - Database.Insert(new ContentTypeTemplateDto - { - ContentTypeNodeId = entity.Id, - TemplateNodeId = template.Id, - IsDefault = false - }); - } + ContentTypeNodeId = entity.Id, TemplateNodeId = defaultTemplateId, IsDefault = true + }); } - protected override void PersistUpdatedItem(IContentType entity) + foreach (ITemplate template in entity.AllowedTemplates?.Where(x => x != null && x.Id != defaultTemplateId) ?? + Array.Empty()) { - ValidateAlias(entity); - - //Updates Modified date - entity.UpdatingEntity(); - - //Look up parent to get and set the correct Path if ParentId has changed - if (entity.IsPropertyDirty("ParentId")) + Database.Insert(new ContentTypeTemplateDto { - var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); - entity.Path = string.Concat(parent.Path, ",", entity.Id); - entity.Level = parent.Level + 1; - var maxSortOrder = - Database.ExecuteScalar( - "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", - new { ParentId = entity.ParentId, NodeObjectType = NodeObjectTypeId }); - entity.SortOrder = maxSortOrder + 1; - } + ContentTypeNodeId = entity.Id, TemplateNodeId = template.Id, IsDefault = false + }); + } + } + + protected override void PersistUpdatedItem(IContentType entity) + { + ValidateAlias(entity); - PersistUpdatedBaseContentType(entity); - PersistTemplates(entity, true); - PersistHistoryCleanup(entity); + //Updates Modified date + entity.UpdatingEntity(); - entity.ResetDirtyProperties(); + //Look up parent to get and set the correct Path if ParentId has changed + if (entity.IsPropertyDirty("ParentId")) + { + NodeDto? parent = Database.First("WHERE id = @ParentId", new {entity.ParentId}); + entity.Path = string.Concat(parent.Path, ",", entity.Id); + entity.Level = parent.Level + 1; + var maxSortOrder = + Database.ExecuteScalar( + "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", + new {entity.ParentId, NodeObjectType = NodeObjectTypeId}); + entity.SortOrder = maxSortOrder + 1; } - private void PersistHistoryCleanup(IContentType entity) + PersistUpdatedBaseContentType(entity); + PersistTemplates(entity, true); + PersistHistoryCleanup(entity); + + entity.ResetDirtyProperties(); + } + + private void PersistHistoryCleanup(IContentType entity) + { + // historyCleanup property is not mandatory for api endpoint, handle the case where it's not present. + // DocumentTypeSave doesn't handle this for us like ContentType constructors do. + if (entity is IContentTypeWithHistoryCleanup entityWithHistoryCleanup) { - // historyCleanup property is not mandatory for api endpoint, handle the case where it's not present. - // DocumentTypeSave doesn't handle this for us like ContentType constructors do. - if (entity is IContentTypeWithHistoryCleanup entityWithHistoryCleanup) + var dto = new ContentVersionCleanupPolicyDto { - ContentVersionCleanupPolicyDto dto = new ContentVersionCleanupPolicyDto() - { - ContentTypeId = entity.Id, - Updated = DateTime.Now, - PreventCleanup = entityWithHistoryCleanup.HistoryCleanup?.PreventCleanup ?? false, - KeepAllVersionsNewerThanDays = entityWithHistoryCleanup.HistoryCleanup?.KeepAllVersionsNewerThanDays, - KeepLatestVersionPerDayForDays = entityWithHistoryCleanup.HistoryCleanup?.KeepLatestVersionPerDayForDays - }; - Database.InsertOrUpdate(dto); - } - + ContentTypeId = entity.Id, + Updated = DateTime.Now, + PreventCleanup = entityWithHistoryCleanup.HistoryCleanup?.PreventCleanup ?? false, + KeepAllVersionsNewerThanDays = + entityWithHistoryCleanup.HistoryCleanup?.KeepAllVersionsNewerThanDays, + KeepLatestVersionPerDayForDays = + entityWithHistoryCleanup.HistoryCleanup?.KeepLatestVersionPerDayForDays + }; + Database.InsertOrUpdate(dto); } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index 086542d307f6..58b6218ee8ac 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -1,8 +1,6 @@ -using System; -using System.Collections.Generic; using System.Data; using System.Globalization; -using System.Linq; +using System.Linq.Expressions; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -16,1480 +14,1552 @@ using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; -using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represent an abstract Repository for ContentType based repositories +/// +/// Exposes shared functionality +/// +internal abstract class ContentTypeRepositoryBase : EntityRepositoryBase, + IReadRepository + where TEntity : class, IContentTypeComposition { + private readonly IShortStringHelper _shortStringHelper; + + protected ContentTypeRepositoryBase(IScopeAccessor scopeAccessor, AppCaches cache, + ILogger> logger, IContentTypeCommonRepository commonRepository, + ILanguageRepository languageRepository, IShortStringHelper shortStringHelper) + : base(scopeAccessor, cache, logger) + { + _shortStringHelper = shortStringHelper; + CommonRepository = commonRepository; + LanguageRepository = languageRepository; + } + + protected IContentTypeCommonRepository CommonRepository { get; } + protected ILanguageRepository LanguageRepository { get; } + protected abstract bool SupportsPublishing { get; } + /// - /// Represent an abstract Repository for ContentType based repositories + /// Gets the node object type for the repository's entity /// - /// Exposes shared functionality - /// - internal abstract class ContentTypeRepositoryBase : EntityRepositoryBase, - IReadRepository - where TEntity : class, IContentTypeComposition - { - private readonly IShortStringHelper _shortStringHelper; + protected abstract Guid NodeObjectTypeId { get; } - protected ContentTypeRepositoryBase(IScopeAccessor scopeAccessor, AppCaches cache, - ILogger> logger, IContentTypeCommonRepository commonRepository, - ILanguageRepository languageRepository, IShortStringHelper shortStringHelper) - : base(scopeAccessor, cache, logger) - { - _shortStringHelper = shortStringHelper; - CommonRepository = commonRepository; - LanguageRepository = languageRepository; - } + /// + /// Gets an Entity by Id + /// + /// + /// + public TEntity? Get(Guid id) => PerformGet(id); - protected IContentTypeCommonRepository CommonRepository { get; } - protected ILanguageRepository LanguageRepository { get; } - protected abstract bool SupportsPublishing { get; } + /// + /// Gets all entities of the specified type + /// + /// + /// + /// + /// Ensure explicit implementation, we don't want to have any accidental calls to this since it is essentially the same + /// signature as the main GetAll when there are no parameters + /// + IEnumerable IReadRepository.GetMany(params Guid[]? ids) => + PerformGetAll(ids) ?? Enumerable.Empty(); - /// - /// Gets the node object type for the repository's entity - /// - protected abstract Guid NodeObjectTypeId { get; } + /// + /// Boolean indicating whether an Entity with the specified Id exists + /// + /// + /// + public bool Exists(Guid id) => PerformExists(id); - public IEnumerable> Move(TEntity moving, EntityContainer container) + public IEnumerable> Move(TEntity moving, EntityContainer container) + { + var parentId = Constants.System.Root; + if (container != null) { - var parentId = Cms.Core.Constants.System.Root; - if (container != null) + // check path + if (string.Format(",{0},", container.Path).IndexOf(string.Format(",{0},", moving.Id), + StringComparison.Ordinal) > -1) { - // check path - if ((string.Format(",{0},", container.Path)).IndexOf(string.Format(",{0},", moving.Id), - StringComparison.Ordinal) > -1) - throw new DataOperationException(MoveOperationStatusType - .FailedNotAllowedByPath); - - parentId = container.Id; + throw new DataOperationException(MoveOperationStatusType + .FailedNotAllowedByPath); } - // track moved entities - var moveInfo = new List> {new MoveEventInfo(moving, moving.Path, parentId)}; - - - // get the level delta (old pos to new pos) - var levelDelta = container == null - ? 1 - moving.Level - : container.Level + 1 - moving.Level; + parentId = container.Id; + } - // move to parent (or -1), update path, save - moving.ParentId = parentId; - var movingPath = moving.Path + ","; // save before changing - moving.Path = (container == null ? Cms.Core.Constants.System.RootString : container.Path) + "," + moving.Id; - moving.Level = container == null ? 1 : container.Level + 1; - Save(moving); + // track moved entities + var moveInfo = new List> {new(moving, moving.Path, parentId)}; - //update all descendants, update in order of level - var descendants = Get(Query().Where(type => type.Path.StartsWith(movingPath))); - var paths = new Dictionary(); - paths[moving.Id] = moving.Path; - if (descendants is null) - { - return moveInfo; - } - foreach (var descendant in descendants.OrderBy(x => x.Level)) - { - moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); + // get the level delta (old pos to new pos) + var levelDelta = container == null + ? 1 - moving.Level + : container.Level + 1 - moving.Level; - descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id; - descendant.Level += levelDelta; + // move to parent (or -1), update path, save + moving.ParentId = parentId; + var movingPath = moving.Path + ","; // save before changing + moving.Path = (container == null ? Constants.System.RootString : container.Path) + "," + moving.Id; + moving.Level = container == null ? 1 : container.Level + 1; + Save(moving); - Save(descendant); - } + //update all descendants, update in order of level + IEnumerable? descendants = Get(Query().Where(type => type.Path.StartsWith(movingPath))); + var paths = new Dictionary(); + paths[moving.Id] = moving.Path; + if (descendants is null) + { return moveInfo; } - protected override IEnumerable PerformGetAll(params int[]? ids) + foreach (TEntity descendant in descendants.OrderBy(x => x.Level)) { - var result = GetAllWithFullCachePolicy(); + moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); - // By default the cache policy will always want everything - // even GetMany(ids) gets everything and filters afterwards, - // however if we are using No Cache, we must still be able to support - // collections of Ids, so this is to work around that: - if (ids?.Any() ?? false) - { - return result?.Where(x => ids.Contains(x.Id)) ?? Enumerable.Empty(); - } + descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id; + descendant.Level += levelDelta; - return result ?? Enumerable.Empty();; + Save(descendant); } - protected abstract IEnumerable? GetAllWithFullCachePolicy(); + return moveInfo; + } - protected virtual PropertyType CreatePropertyType(string propertyEditorAlias, ValueStorageType storageType, - string propertyTypeAlias) - { - return new PropertyType(_shortStringHelper, propertyEditorAlias, storageType, propertyTypeAlias); - } + protected override IEnumerable PerformGetAll(params int[]? ids) + { + IEnumerable? result = GetAllWithFullCachePolicy(); - protected override void PersistDeletedItem(TEntity entity) + // By default the cache policy will always want everything + // even GetMany(ids) gets everything and filters afterwards, + // however if we are using No Cache, we must still be able to support + // collections of Ids, so this is to work around that: + if (ids?.Any() ?? false) { - base.PersistDeletedItem(entity); - CommonRepository.ClearCache(); // always + return result?.Where(x => ids.Contains(x.Id)) ?? Enumerable.Empty(); } - protected void PersistNewBaseContentType(IContentTypeComposition entity) - { - ValidateVariations(entity); + return result ?? Enumerable.Empty(); + ; + } + + protected abstract IEnumerable? GetAllWithFullCachePolicy(); + + protected virtual PropertyType CreatePropertyType(string propertyEditorAlias, ValueStorageType storageType, + string propertyTypeAlias) => + new PropertyType(_shortStringHelper, propertyEditorAlias, storageType, propertyTypeAlias); - var dto = ContentTypeFactory.BuildContentTypeDto(entity); + protected override void PersistDeletedItem(TEntity entity) + { + base.PersistDeletedItem(entity); + CommonRepository.ClearCache(); // always + } - //Cannot add a duplicate content type - var exists = Database.ExecuteScalar(@"SELECT COUNT(*) FROM cmsContentType + protected void PersistNewBaseContentType(IContentTypeComposition entity) + { + ValidateVariations(entity); + + ContentTypeDto dto = ContentTypeFactory.BuildContentTypeDto(entity); + + //Cannot add a duplicate content type + var exists = Database.ExecuteScalar(@"SELECT COUNT(*) FROM cmsContentType INNER JOIN umbracoNode ON cmsContentType.nodeId = umbracoNode.id WHERE cmsContentType." + SqlSyntax.GetQuotedColumnName("alias") + @"= @alias AND umbracoNode.nodeObjectType = @objectType", - new {alias = entity.Alias, objectType = NodeObjectTypeId}); - if (exists > 0) + new {alias = entity.Alias, objectType = NodeObjectTypeId}); + if (exists > 0) + { + throw new DuplicateNameException("An item with the alias " + entity.Alias + " already exists"); + } + + //Logic for setting Path, Level and SortOrder + NodeDto? parent = Database.First("WHERE id = @ParentId", new {entity.ParentId}); + var level = parent.Level + 1; + var sortOrder = + Database.ExecuteScalar( + "SELECT COUNT(*) FROM umbracoNode WHERE parentID = @ParentId AND nodeObjectType = @NodeObjectType", + new {entity.ParentId, NodeObjectType = NodeObjectTypeId}); + + //Create the (base) node data - umbracoNode + NodeDto nodeDto = dto.NodeDto; + nodeDto.Path = parent.Path; + nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); + nodeDto.SortOrder = sortOrder; + var o = Database.IsNew(nodeDto) + ? Convert.ToInt32(Database.Insert(nodeDto)) + : Database.Update(nodeDto); + + //Update with new correct path + nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); + Database.Update(nodeDto); + + //Update entity with correct values + entity.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set + entity.Path = nodeDto.Path; + entity.SortOrder = sortOrder; + entity.Level = level; + + //Insert new ContentType entry + dto.NodeId = nodeDto.NodeId; + Database.Insert(dto); + + //Insert ContentType composition in new table + foreach (IContentTypeComposition composition in entity.ContentTypeComposition) + { + if (composition.Id == entity.Id) { - throw new DuplicateNameException("An item with the alias " + entity.Alias + " already exists"); + continue; //Just to ensure that we aren't creating a reference to ourself. } - //Logic for setting Path, Level and SortOrder - var parent = Database.First("WHERE id = @ParentId", new {ParentId = entity.ParentId}); - int level = parent.Level + 1; - int sortOrder = - Database.ExecuteScalar( - "SELECT COUNT(*) FROM umbracoNode WHERE parentID = @ParentId AND nodeObjectType = @NodeObjectType", - new {ParentId = entity.ParentId, NodeObjectType = NodeObjectTypeId}); - - //Create the (base) node data - umbracoNode - var nodeDto = dto.NodeDto; - nodeDto.Path = parent.Path; - nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); - nodeDto.SortOrder = sortOrder; - var o = Database.IsNew(nodeDto) - ? Convert.ToInt32(Database.Insert(nodeDto)) - : Database.Update(nodeDto); - - //Update with new correct path - nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); - Database.Update(nodeDto); - - //Update entity with correct values - entity.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set - entity.Path = nodeDto.Path; - entity.SortOrder = sortOrder; - entity.Level = level; - - //Insert new ContentType entry - dto.NodeId = nodeDto.NodeId; - Database.Insert(dto); - - //Insert ContentType composition in new table - foreach (var composition in entity.ContentTypeComposition) + if (composition.HasIdentity) { - if (composition.Id == entity.Id) - continue; //Just to ensure that we aren't creating a reference to ourself. - - if (composition.HasIdentity) - { - Database.Insert(new ContentType2ContentTypeDto {ParentId = composition.Id, ChildId = entity.Id}); - } - else - { - //Fallback for ContentTypes with no identity - var contentTypeDto = - Database.FirstOrDefault("WHERE alias = @Alias", - new {Alias = composition.Alias}); - if (contentTypeDto != null) - { - Database.Insert(new ContentType2ContentTypeDto - { - ParentId = contentTypeDto.NodeId, ChildId = entity.Id - }); - } - } + Database.Insert(new ContentType2ContentTypeDto {ParentId = composition.Id, ChildId = entity.Id}); } - - if (entity.AllowedContentTypes is not null) + else { - //Insert collection of allowed content types - foreach (var allowedContentType in entity.AllowedContentTypes) + //Fallback for ContentTypes with no identity + ContentTypeDto? contentTypeDto = + Database.FirstOrDefault("WHERE alias = @Alias", + new {composition.Alias}); + if (contentTypeDto != null) { - Database.Insert(new ContentTypeAllowedContentTypeDto + Database.Insert(new ContentType2ContentTypeDto { - Id = entity.Id, - AllowedId = allowedContentType.Id.Value, - SortOrder = allowedContentType.SortOrder + ParentId = contentTypeDto.NodeId, ChildId = entity.Id }); } } + } - //Insert Tabs - foreach (var propertyGroup in entity.PropertyGroups) + if (entity.AllowedContentTypes is not null) + { + //Insert collection of allowed content types + foreach (ContentTypeSort allowedContentType in entity.AllowedContentTypes) { - var tabDto = PropertyGroupFactory.BuildGroupDto(propertyGroup, nodeDto.NodeId); - var primaryKey = Convert.ToInt32(Database.Insert(tabDto)); - propertyGroup.Id = primaryKey; //Set Id on PropertyGroup + Database.Insert(new ContentTypeAllowedContentTypeDto + { + Id = entity.Id, + AllowedId = allowedContentType.Id.Value, + SortOrder = allowedContentType.SortOrder + }); + } + } + + //Insert Tabs + foreach (PropertyGroup propertyGroup in entity.PropertyGroups) + { + PropertyTypeGroupDto tabDto = PropertyGroupFactory.BuildGroupDto(propertyGroup, nodeDto.NodeId); + var primaryKey = Convert.ToInt32(Database.Insert(tabDto)); + propertyGroup.Id = primaryKey; //Set Id on PropertyGroup - //Ensure that the PropertyGroup's Id is set on the PropertyTypes within a group - //unless the PropertyGroupId has already been changed. - if (propertyGroup.PropertyTypes is not null) + //Ensure that the PropertyGroup's Id is set on the PropertyTypes within a group + //unless the PropertyGroupId has already been changed. + if (propertyGroup.PropertyTypes is not null) + { + foreach (IPropertyType propertyType in propertyGroup.PropertyTypes) { - foreach (var propertyType in propertyGroup.PropertyTypes) + if (propertyType.IsPropertyDirty("PropertyGroupId") == false) { - if (propertyType.IsPropertyDirty("PropertyGroupId") == false) - { - var tempGroup = propertyGroup; - propertyType.PropertyGroupId = new Lazy(() => tempGroup.Id); - } + PropertyGroup tempGroup = propertyGroup; + propertyType.PropertyGroupId = new Lazy(() => tempGroup.Id); } } } + } - //Insert PropertyTypes - foreach (var propertyType in entity.PropertyTypes) + //Insert PropertyTypes + foreach (IPropertyType propertyType in entity.PropertyTypes) + { + var tabId = propertyType.PropertyGroupId != null ? propertyType.PropertyGroupId.Value : default; + //If the Id of the DataType is not set, we resolve it from the db by its PropertyEditorAlias + if (propertyType.DataTypeId == 0 || propertyType.DataTypeId == default) { - var tabId = propertyType.PropertyGroupId != null ? propertyType.PropertyGroupId.Value : default(int); - //If the Id of the DataType is not set, we resolve it from the db by its PropertyEditorAlias - if (propertyType.DataTypeId == 0 || propertyType.DataTypeId == default(int)) - { - AssignDataTypeFromPropertyEditor(propertyType); - } - - var propertyTypeDto = PropertyGroupFactory.BuildPropertyTypeDto(tabId, propertyType, nodeDto.NodeId); - int typePrimaryKey = Convert.ToInt32(Database.Insert(propertyTypeDto)); - propertyType.Id = typePrimaryKey; //Set Id on new PropertyType - - //Update the current PropertyType with correct PropertyEditorAlias and DatabaseType - var dataTypeDto = - Database.FirstOrDefault("WHERE nodeId = @Id", new {Id = propertyTypeDto.DataTypeId}); - propertyType.PropertyEditorAlias = dataTypeDto.EditorAlias; - propertyType.ValueStorageType = dataTypeDto.DbType.EnumParse(true); + AssignDataTypeFromPropertyEditor(propertyType); } - CommonRepository.ClearCache(); // always + PropertyTypeDto propertyTypeDto = + PropertyGroupFactory.BuildPropertyTypeDto(tabId, propertyType, nodeDto.NodeId); + var typePrimaryKey = Convert.ToInt32(Database.Insert(propertyTypeDto)); + propertyType.Id = typePrimaryKey; //Set Id on new PropertyType + + //Update the current PropertyType with correct PropertyEditorAlias and DatabaseType + DataTypeDto? dataTypeDto = + Database.FirstOrDefault("WHERE nodeId = @Id", new {Id = propertyTypeDto.DataTypeId}); + propertyType.PropertyEditorAlias = dataTypeDto.EditorAlias; + propertyType.ValueStorageType = dataTypeDto.DbType.EnumParse(true); } - protected void PersistUpdatedBaseContentType(IContentTypeComposition entity) - { - CorrectPropertyTypeVariations(entity); - ValidateVariations(entity); + CommonRepository.ClearCache(); // always + } + + protected void PersistUpdatedBaseContentType(IContentTypeComposition entity) + { + CorrectPropertyTypeVariations(entity); + ValidateVariations(entity); - var dto = ContentTypeFactory.BuildContentTypeDto(entity); + ContentTypeDto dto = ContentTypeFactory.BuildContentTypeDto(entity); - // ensure the alias is not used already - var exists = Database.ExecuteScalar(@"SELECT COUNT(*) FROM cmsContentType + // ensure the alias is not used already + var exists = Database.ExecuteScalar(@"SELECT COUNT(*) FROM cmsContentType INNER JOIN umbracoNode ON cmsContentType.nodeId = umbracoNode.id WHERE cmsContentType." + SqlSyntax.GetQuotedColumnName("alias") + @"= @alias AND umbracoNode.nodeObjectType = @objectType AND umbracoNode.id <> @id", - new {id = dto.NodeId, alias = dto.Alias, objectType = NodeObjectTypeId}); - if (exists > 0) - { - throw new DuplicateNameException("An item with the alias " + dto.Alias + " already exists"); - } + new {id = dto.NodeId, alias = dto.Alias, objectType = NodeObjectTypeId}); + if (exists > 0) + { + throw new DuplicateNameException("An item with the alias " + dto.Alias + " already exists"); + } - // repository should be write-locked when doing this, so we are safe from race-conds - // handle (update) the node - var nodeDto = dto.NodeDto; - Database.Update(nodeDto); - - // we NEED this: updating, so the .PrimaryKey already exists, but the entity does - // not carry it and therefore the dto does not have it yet - must get it from db, - // look up ContentType entry to get PrimaryKey for updating the DTO - var dtoPk = Database.First("WHERE nodeId = @Id", new {entity.Id}); - dto.PrimaryKey = dtoPk.PrimaryKey; - Database.Update(dto); - - // handle (delete then recreate) compositions - Database.Delete("WHERE childContentTypeId = @Id", new {entity.Id}); - foreach (var composition in entity.ContentTypeComposition) - Database.Insert(new ContentType2ContentTypeDto {ParentId = composition.Id, ChildId = entity.Id}); + // repository should be write-locked when doing this, so we are safe from race-conds + // handle (update) the node + NodeDto nodeDto = dto.NodeDto; + Database.Update(nodeDto); + + // we NEED this: updating, so the .PrimaryKey already exists, but the entity does + // not carry it and therefore the dto does not have it yet - must get it from db, + // look up ContentType entry to get PrimaryKey for updating the DTO + ContentTypeDto? dtoPk = Database.First("WHERE nodeId = @Id", new {entity.Id}); + dto.PrimaryKey = dtoPk.PrimaryKey; + Database.Update(dto); + + // handle (delete then recreate) compositions + Database.Delete("WHERE childContentTypeId = @Id", new {entity.Id}); + foreach (IContentTypeComposition composition in entity.ContentTypeComposition) + { + Database.Insert(new ContentType2ContentTypeDto {ParentId = composition.Id, ChildId = entity.Id}); + } - // removing a ContentType from a composition (U4-1690) - // 1. Find content based on the current ContentType: entity.Id - // 2. Find all PropertyTypes on the ContentType that was removed - tracked id (key) - // 3. Remove properties based on property types from the removed content type where the content ids correspond to those found in step one - if (entity.RemovedContentTypes.Any()) + // removing a ContentType from a composition (U4-1690) + // 1. Find content based on the current ContentType: entity.Id + // 2. Find all PropertyTypes on the ContentType that was removed - tracked id (key) + // 3. Remove properties based on property types from the removed content type where the content ids correspond to those found in step one + if (entity.RemovedContentTypes.Any()) + { + // TODO: Could we do the below with bulk SQL statements instead of looking everything up and then manipulating? + + // find Content based on the current ContentType + Sql sql = Sql() + .SelectAll() + .From() + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document) + .Where(x => x.ContentTypeId == entity.Id); + List? contentDtos = Database.Fetch(sql); + + // loop through all tracked keys, which corresponds to the ContentTypes that has been removed from the composition + foreach (var key in entity.RemovedContentTypes) { - // TODO: Could we do the below with bulk SQL statements instead of looking everything up and then manipulating? - - // find Content based on the current ContentType - var sql = Sql() - .SelectAll() - .From() - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == Cms.Core.Constants.ObjectTypes.Document) - .Where(x => x.ContentTypeId == entity.Id); - var contentDtos = Database.Fetch(sql); - - // loop through all tracked keys, which corresponds to the ContentTypes that has been removed from the composition - foreach (var key in entity.RemovedContentTypes) + // find PropertyTypes for the removed ContentType + List? propertyTypes = + Database.Fetch("WHERE contentTypeId = @Id", new {Id = key}); + // loop through the Content that is based on the current ContentType in order to remove the Properties that are + // based on the PropertyTypes that belong to the removed ContentType. + foreach (ContentDto? contentDto in contentDtos) { - // find PropertyTypes for the removed ContentType - var propertyTypes = Database.Fetch("WHERE contentTypeId = @Id", new {Id = key}); - // loop through the Content that is based on the current ContentType in order to remove the Properties that are - // based on the PropertyTypes that belong to the removed ContentType. - foreach (var contentDto in contentDtos) + // TODO: This could be done with bulk SQL statements + foreach (PropertyTypeDto? propertyType in propertyTypes) { - // TODO: This could be done with bulk SQL statements - foreach (var propertyType in propertyTypes) - { - var nodeId = contentDto.NodeId; - var propertyTypeId = propertyType.Id; - var propertySql = Sql() - .Select(x => x.Id) - .From() - .InnerJoin() - .On((left, right) => left.PropertyTypeId == right.Id) - .InnerJoin() - .On((left, right) => left.VersionId == right.Id) - .Where(x => x.NodeId == nodeId) - .Where(x => x.Id == propertyTypeId); - - // finally delete the properties that match our criteria for removing a ContentType from the composition - Database.Delete(new Sql("WHERE id IN (" + propertySql.SQL + ")", - propertySql.Arguments)); - } + var nodeId = contentDto.NodeId; + var propertyTypeId = propertyType.Id; + Sql propertySql = Sql() + .Select(x => x.Id) + .From() + .InnerJoin() + .On((left, right) => left.PropertyTypeId == right.Id) + .InnerJoin() + .On((left, right) => left.VersionId == right.Id) + .Where(x => x.NodeId == nodeId) + .Where(x => x.Id == propertyTypeId); + + // finally delete the properties that match our criteria for removing a ContentType from the composition + Database.Delete(new Sql("WHERE id IN (" + propertySql.SQL + ")", + propertySql.Arguments)); } } } + } - // delete the allowed content type entries before re-inserting the collection of allowed content types - Database.Delete("WHERE Id = @Id", new {entity.Id}); - if (entity.AllowedContentTypes is not null) + // delete the allowed content type entries before re-inserting the collection of allowed content types + Database.Delete("WHERE Id = @Id", new {entity.Id}); + if (entity.AllowedContentTypes is not null) + { + foreach (ContentTypeSort allowedContentType in entity.AllowedContentTypes) { - foreach (var allowedContentType in entity.AllowedContentTypes) + Database.Insert(new ContentTypeAllowedContentTypeDto { - Database.Insert(new ContentTypeAllowedContentTypeDto - { - Id = entity.Id, - AllowedId = allowedContentType.Id.Value, - SortOrder = allowedContentType.SortOrder - }); - } + Id = entity.Id, + AllowedId = allowedContentType.Id.Value, + SortOrder = allowedContentType.SortOrder + }); } + } - // Delete property types ... by excepting entries from db with entries from collections. - // We check if the entity's own PropertyTypes has been modified and then also check - // any of the property groups PropertyTypes has been modified. - // This specifically tells us if any property type collections have changed. - if (entity.IsPropertyDirty("NoGroupPropertyTypes") || - entity.PropertyGroups.Any(x => x.IsPropertyDirty("PropertyTypes"))) + // Delete property types ... by excepting entries from db with entries from collections. + // We check if the entity's own PropertyTypes has been modified and then also check + // any of the property groups PropertyTypes has been modified. + // This specifically tells us if any property type collections have changed. + if (entity.IsPropertyDirty("NoGroupPropertyTypes") || + entity.PropertyGroups.Any(x => x.IsPropertyDirty("PropertyTypes"))) + { + List? dbPropertyTypes = + Database.Fetch("WHERE contentTypeId = @Id", new {entity.Id}); + IEnumerable dbPropertyTypeIds = dbPropertyTypes.Select(x => x.Id); + IEnumerable entityPropertyTypes = entity.PropertyTypes.Where(x => x.HasIdentity).Select(x => x.Id); + IEnumerable propertyTypeToDeleteIds = dbPropertyTypeIds.Except(entityPropertyTypes); + foreach (var propertyTypeId in propertyTypeToDeleteIds) { - var dbPropertyTypes = Database.Fetch("WHERE contentTypeId = @Id", new {entity.Id}); - var dbPropertyTypeIds = dbPropertyTypes.Select(x => x.Id); - var entityPropertyTypes = entity.PropertyTypes.Where(x => x.HasIdentity).Select(x => x.Id); - var propertyTypeToDeleteIds = dbPropertyTypeIds.Except(entityPropertyTypes); - foreach (var propertyTypeId in propertyTypeToDeleteIds) - DeletePropertyType(entity.Id, propertyTypeId); + DeletePropertyType(entity.Id, propertyTypeId); } + } - // Delete tabs ... by excepting entries from db with entries from collections. - // We check if the entity's own PropertyGroups has been modified. - // This specifically tells us if the property group collections have changed. - List? orphanPropertyTypeIds = null; - if (entity.IsPropertyDirty("PropertyGroups")) + // Delete tabs ... by excepting entries from db with entries from collections. + // We check if the entity's own PropertyGroups has been modified. + // This specifically tells us if the property group collections have changed. + List? orphanPropertyTypeIds = null; + if (entity.IsPropertyDirty("PropertyGroups")) + { + // TODO: we used to try to propagate tabs renaming downstream, relying on ParentId, but + // 1) ParentId makes no sense (if a tab can be inherited from multiple composition + // types) so we would need to figure things out differently, visiting downstream + // content types and looking for tabs with the same name... + // 2) It was not deployable as changing a content type changes other content types + // that was not deterministic, because it would depend on the order of the changes. + // That last point could be fixed if (1) is fixed, but then it still is an issue with + // deploy because changing a content type changes other content types that are not + // dependencies but dependents, and then what? + // + // So... for the time being, all renaming propagation is disabled. We just don't do it. + + // (all gone) + + // delete tabs that do not exist anymore + // get the tabs that are currently existing (in the db), get the tabs that we want, + // now, and derive the tabs that we want to delete + var existingPropertyGroups = Database + .Fetch("WHERE contentTypeNodeId = @id", new {id = entity.Id}) + .Select(x => x.Id) + .ToList(); + var newPropertyGroups = entity.PropertyGroups.Select(x => x.Id).ToList(); + var groupsToDelete = existingPropertyGroups + .Except(newPropertyGroups) + .ToArray(); + + // delete the tabs + if (groupsToDelete.Length > 0) { - // TODO: we used to try to propagate tabs renaming downstream, relying on ParentId, but - // 1) ParentId makes no sense (if a tab can be inherited from multiple composition - // types) so we would need to figure things out differently, visiting downstream - // content types and looking for tabs with the same name... - // 2) It was not deployable as changing a content type changes other content types - // that was not deterministic, because it would depend on the order of the changes. - // That last point could be fixed if (1) is fixed, but then it still is an issue with - // deploy because changing a content type changes other content types that are not - // dependencies but dependents, and then what? - // - // So... for the time being, all renaming propagation is disabled. We just don't do it. - - // (all gone) - - // delete tabs that do not exist anymore - // get the tabs that are currently existing (in the db), get the tabs that we want, - // now, and derive the tabs that we want to delete - var existingPropertyGroups = Database - .Fetch("WHERE contentTypeNodeId = @id", new {id = entity.Id}) - .Select(x => x.Id) - .ToList(); - var newPropertyGroups = entity.PropertyGroups.Select(x => x.Id).ToList(); - var groupsToDelete = existingPropertyGroups - .Except(newPropertyGroups) - .ToArray(); - - // delete the tabs - if (groupsToDelete.Length > 0) - { - // if the tab contains properties, take care of them - // - move them to 'generic properties' so they remain consistent - // - keep track of them, later on we'll figure out what to do with them - // see http://issues.umbraco.org/issue/U4-8663 - orphanPropertyTypeIds = Database.Fetch("WHERE propertyTypeGroupId IN (@ids)", - new {ids = groupsToDelete}) - .Select(x => x.Id).ToList(); - Database.Update( - "SET propertyTypeGroupId = NULL WHERE propertyTypeGroupId IN (@ids)", - new {ids = groupsToDelete}); - - // now we can delete the tabs - Database.Delete("WHERE id IN (@ids)", new {ids = groupsToDelete}); - } + // if the tab contains properties, take care of them + // - move them to 'generic properties' so they remain consistent + // - keep track of them, later on we'll figure out what to do with them + // see http://issues.umbraco.org/issue/U4-8663 + orphanPropertyTypeIds = Database.Fetch("WHERE propertyTypeGroupId IN (@ids)", + new {ids = groupsToDelete}) + .Select(x => x.Id).ToList(); + Database.Update( + "SET propertyTypeGroupId = NULL WHERE propertyTypeGroupId IN (@ids)", + new {ids = groupsToDelete}); + + // now we can delete the tabs + Database.Delete("WHERE id IN (@ids)", new {ids = groupsToDelete}); } + } - // insert or update groups, assign properties - foreach (var propertyGroup in entity.PropertyGroups) + // insert or update groups, assign properties + foreach (PropertyGroup propertyGroup in entity.PropertyGroups) + { + // insert or update group + PropertyTypeGroupDto groupDto = PropertyGroupFactory.BuildGroupDto(propertyGroup, entity.Id); + var groupId = propertyGroup.HasIdentity + ? Database.Update(groupDto) + : Convert.ToInt32(Database.Insert(groupDto)); + if (propertyGroup.HasIdentity == false) { - // insert or update group - var groupDto = PropertyGroupFactory.BuildGroupDto(propertyGroup, entity.Id); - var groupId = propertyGroup.HasIdentity - ? Database.Update(groupDto) - : Convert.ToInt32(Database.Insert(groupDto)); - if (propertyGroup.HasIdentity == false) - propertyGroup.Id = groupId; - else - groupId = propertyGroup.Id; - - // assign properties to the group - // (all of them, even those that have .IsPropertyDirty("PropertyGroupId") == true, - // because it should have been set to this group anyways and better be safe) - if (propertyGroup.PropertyTypes is not null) - { - foreach (var propertyType in propertyGroup.PropertyTypes) - { - propertyType.PropertyGroupId = new Lazy(() => groupId); - } - } + propertyGroup.Id = groupId; + } + else + { + groupId = propertyGroup.Id; } - //check if the content type variation has been changed - var contentTypeVariationDirty = entity.IsPropertyDirty("Variations"); - var oldContentTypeVariation = (ContentVariation)dtoPk.Variations; - var newContentTypeVariation = entity.Variations; - var contentTypeVariationChanging = - contentTypeVariationDirty && oldContentTypeVariation != newContentTypeVariation; - if (contentTypeVariationChanging) + // assign properties to the group + // (all of them, even those that have .IsPropertyDirty("PropertyGroupId") == true, + // because it should have been set to this group anyways and better be safe) + if (propertyGroup.PropertyTypes is not null) { - MoveContentTypeVariantData(entity, oldContentTypeVariation, newContentTypeVariation); - Clear301Redirects(entity); - ClearScheduledPublishing(entity); + foreach (IPropertyType propertyType in propertyGroup.PropertyTypes) + { + propertyType.PropertyGroupId = new Lazy(() => groupId); + } } + } - // collect property types that have a dirty variation - List? propertyTypeVariationDirty = null; + //check if the content type variation has been changed + var contentTypeVariationDirty = entity.IsPropertyDirty("Variations"); + var oldContentTypeVariation = (ContentVariation)dtoPk.Variations; + ContentVariation newContentTypeVariation = entity.Variations; + var contentTypeVariationChanging = + contentTypeVariationDirty && oldContentTypeVariation != newContentTypeVariation; + if (contentTypeVariationChanging) + { + MoveContentTypeVariantData(entity, oldContentTypeVariation, newContentTypeVariation); + Clear301Redirects(entity); + ClearScheduledPublishing(entity); + } - // note: this only deals with *local* property types, we're dealing w/compositions later below - foreach (var propertyType in entity.PropertyTypes) + // collect property types that have a dirty variation + List? propertyTypeVariationDirty = null; + + // note: this only deals with *local* property types, we're dealing w/compositions later below + foreach (IPropertyType propertyType in entity.PropertyTypes) + { + // track each property individually + if (propertyType.IsPropertyDirty("Variations")) { - // track each property individually - if (propertyType.IsPropertyDirty("Variations")) + // allocate the list only when needed + if (propertyTypeVariationDirty == null) { - // allocate the list only when needed - if (propertyTypeVariationDirty == null) - propertyTypeVariationDirty = new List(); - - propertyTypeVariationDirty.Add(propertyType); + propertyTypeVariationDirty = new List(); } + + propertyTypeVariationDirty.Add(propertyType); } + } - // figure out dirty property types that have actually changed - // before we insert or update properties, so we can read the old variations - var propertyTypeVariationChanges = propertyTypeVariationDirty != null + // figure out dirty property types that have actually changed + // before we insert or update properties, so we can read the old variations + Dictionary? propertyTypeVariationChanges = + propertyTypeVariationDirty != null ? GetPropertyVariationChanges(propertyTypeVariationDirty) : null; - // deal with composition property types - // add changes for property types obtained via composition, which change due - // to this content type variations change - if (contentTypeVariationChanging) - { - // must use RawComposedPropertyTypes here: only those types that are obtained - // via composition, with their original variations (ie not filtered by this - // content type variations - we need this true value to make decisions. + // deal with composition property types + // add changes for property types obtained via composition, which change due + // to this content type variations change + if (contentTypeVariationChanging) + { + // must use RawComposedPropertyTypes here: only those types that are obtained + // via composition, with their original variations (ie not filtered by this + // content type variations - we need this true value to make decisions. - propertyTypeVariationChanges = propertyTypeVariationChanges ?? - new Dictionary(); + propertyTypeVariationChanges = propertyTypeVariationChanges ?? + new Dictionary(); - foreach (var composedPropertyType in entity.GetOriginalComposedPropertyTypes()) + foreach (IPropertyType composedPropertyType in entity.GetOriginalComposedPropertyTypes()) + { + if (composedPropertyType.Variations == ContentVariation.Nothing) { - if (composedPropertyType.Variations == ContentVariation.Nothing) continue; - - // Determine target variation of the composed property type. - // The composed property is only considered culture variant when the base content type is also culture variant. - // The composed property is only considered segment variant when the base content type is also segment variant. - // Example: Culture variant content type with a Culture+Segment variant property type will become ContentVariation.Culture - var target = newContentTypeVariation & composedPropertyType.Variations; - // Determine the previous variation - // We have to compare with the old content type variation because the composed property might already have changed - // Example: A property with variations in an element type with variations is used in a document without - // when you enable variations the property has already enabled variations from the element type, - // but it's still a change from nothing because the document did not have variations, but it does now. - var from = oldContentTypeVariation & composedPropertyType.Variations; - - propertyTypeVariationChanges[composedPropertyType.Id] = (from, target); + continue; } + + // Determine target variation of the composed property type. + // The composed property is only considered culture variant when the base content type is also culture variant. + // The composed property is only considered segment variant when the base content type is also segment variant. + // Example: Culture variant content type with a Culture+Segment variant property type will become ContentVariation.Culture + ContentVariation target = newContentTypeVariation & composedPropertyType.Variations; + // Determine the previous variation + // We have to compare with the old content type variation because the composed property might already have changed + // Example: A property with variations in an element type with variations is used in a document without + // when you enable variations the property has already enabled variations from the element type, + // but it's still a change from nothing because the document did not have variations, but it does now. + ContentVariation from = oldContentTypeVariation & composedPropertyType.Variations; + + propertyTypeVariationChanges[composedPropertyType.Id] = (from, target); } + } - // insert or update properties - // all of them, no-group and in-groups - foreach (var propertyType in entity.PropertyTypes) + // insert or update properties + // all of them, no-group and in-groups + foreach (IPropertyType propertyType in entity.PropertyTypes) + { + // if the Id of the DataType is not set, we resolve it from the db by its PropertyEditorAlias + if (propertyType.DataTypeId == 0 || propertyType.DataTypeId == default) { - // if the Id of the DataType is not set, we resolve it from the db by its PropertyEditorAlias - if (propertyType.DataTypeId == 0 || propertyType.DataTypeId == default) - AssignDataTypeFromPropertyEditor(propertyType); - - // validate the alias - ValidateAlias(propertyType); - - // insert or update property - var groupId = propertyType.PropertyGroupId?.Value ?? default; - var propertyTypeDto = PropertyGroupFactory.BuildPropertyTypeDto(groupId, propertyType, entity.Id); - var typeId = propertyType.HasIdentity - ? Database.Update(propertyTypeDto) - : Convert.ToInt32(Database.Insert(propertyTypeDto)); - if (propertyType.HasIdentity == false) - propertyType.Id = typeId; - else - typeId = propertyType.Id; - - // not an orphan anymore - orphanPropertyTypeIds?.Remove(typeId); + AssignDataTypeFromPropertyEditor(propertyType); } - // must restrict property data changes to impacted content types - if changing a composing - // type, some composed types (those that do not vary) are not impacted and should be left - // unchanged - // - // getting 'all' from the cache policy is prone to race conditions - fast but dangerous - //var all = ((FullDataSetRepositoryCachePolicy)CachePolicy).GetAllCached(PerformGetAll); - var all = PerformGetAll(); + // validate the alias + ValidateAlias(propertyType); + + // insert or update property + var groupId = propertyType.PropertyGroupId?.Value ?? default; + PropertyTypeDto propertyTypeDto = + PropertyGroupFactory.BuildPropertyTypeDto(groupId, propertyType, entity.Id); + var typeId = propertyType.HasIdentity + ? Database.Update(propertyTypeDto) + : Convert.ToInt32(Database.Insert(propertyTypeDto)); + if (propertyType.HasIdentity == false) + { + propertyType.Id = typeId; + } + else + { + typeId = propertyType.Id; + } - var impacted = GetImpactedContentTypes(entity, all); + // not an orphan anymore + orphanPropertyTypeIds?.Remove(typeId); + } - // if some property types have actually changed, move their variant data - if (propertyTypeVariationChanges?.Count > 0) - MovePropertyTypeVariantData(propertyTypeVariationChanges, impacted); + // must restrict property data changes to impacted content types - if changing a composing + // type, some composed types (those that do not vary) are not impacted and should be left + // unchanged + // + // getting 'all' from the cache policy is prone to race conditions - fast but dangerous + //var all = ((FullDataSetRepositoryCachePolicy)CachePolicy).GetAllCached(PerformGetAll); + IEnumerable? all = PerformGetAll(); - // deal with orphan properties: those that were in a deleted tab, - // and have not been re-mapped to another tab or to 'generic properties' - if (orphanPropertyTypeIds != null) - foreach (var id in orphanPropertyTypeIds) - DeletePropertyType(entity.Id, id); + IEnumerable impacted = GetImpactedContentTypes(entity, all); - CommonRepository.ClearCache(); // always + // if some property types have actually changed, move their variant data + if (propertyTypeVariationChanges?.Count > 0) + { + MovePropertyTypeVariantData(propertyTypeVariationChanges, impacted); } - /// - /// Corrects the property type variations for the given entity - /// to make sure the property type variation is compatible with the - /// variation set on the entity itself. - /// - /// Entity to correct properties for - private void CorrectPropertyTypeVariations(IContentTypeComposition entity) + // deal with orphan properties: those that were in a deleted tab, + // and have not been re-mapped to another tab or to 'generic properties' + if (orphanPropertyTypeIds != null) { - // Update property variations based on the content type variation - foreach (var propertyType in entity.PropertyTypes) + foreach (var id in orphanPropertyTypeIds) { - // Determine variation for the property type. - // The property is only considered culture variant when the base content type is also culture variant. - // The property is only considered segment variant when the base content type is also segment variant. - // Example: Culture variant content type with a Culture+Segment variant property type will become ContentVariation.Culture - propertyType.Variations = entity.Variations & propertyType.Variations; + DeletePropertyType(entity.Id, id); } } - /// - /// Ensures that no property types are flagged for a variance that is not supported by the content type itself - /// - /// The entity for which the property types will be validated - private void ValidateVariations(IContentTypeComposition entity) + CommonRepository.ClearCache(); // always + } + + /// + /// Corrects the property type variations for the given entity + /// to make sure the property type variation is compatible with the + /// variation set on the entity itself. + /// + /// Entity to correct properties for + private void CorrectPropertyTypeVariations(IContentTypeComposition entity) + { + // Update property variations based on the content type variation + foreach (IPropertyType propertyType in entity.PropertyTypes) { - foreach (var prop in entity.PropertyTypes) - { - // The variation of a property is only allowed if all its variation flags - // are also set on the entity itself. It cannot set anything that is not also set by the content type. - // For example, when entity.Variations is set to Culture a property cannot be set to Segment. - var isValid = entity.Variations.HasFlag(prop.Variations); - if (!isValid) - throw new InvalidOperationException( - $"The property {prop.Alias} cannot have variations of {prop.Variations} with the content type variations of {entity.Variations}"); - } + // Determine variation for the property type. + // The property is only considered culture variant when the base content type is also culture variant. + // The property is only considered segment variant when the base content type is also segment variant. + // Example: Culture variant content type with a Culture+Segment variant property type will become ContentVariation.Culture + propertyType.Variations = entity.Variations & propertyType.Variations; } + } - private IEnumerable GetImpactedContentTypes(IContentTypeComposition contentType, - IEnumerable? all) + /// + /// Ensures that no property types are flagged for a variance that is not supported by the content type itself + /// + /// The entity for which the property types will be validated + private void ValidateVariations(IContentTypeComposition entity) + { + foreach (IPropertyType prop in entity.PropertyTypes) { - if (all is null) + // The variation of a property is only allowed if all its variation flags + // are also set on the entity itself. It cannot set anything that is not also set by the content type. + // For example, when entity.Variations is set to Culture a property cannot be set to Segment. + var isValid = entity.Variations.HasFlag(prop.Variations); + if (!isValid) { - return Enumerable.Empty(); + throw new InvalidOperationException( + $"The property {prop.Alias} cannot have variations of {prop.Variations} with the content type variations of {entity.Variations}"); } - var impact = new List(); - var set = new List {contentType}; + } + } + + private IEnumerable GetImpactedContentTypes(IContentTypeComposition contentType, + IEnumerable? all) + { + if (all is null) + { + return Enumerable.Empty(); + } + + var impact = new List(); + var set = new List {contentType}; - var tree = new Dictionary>(); - foreach (var x in all) - foreach (var y in x.ContentTypeComposition) + var tree = new Dictionary>(); + foreach (IContentTypeComposition x in all) + foreach (IContentTypeComposition y in x.ContentTypeComposition) + { + if (!tree.TryGetValue(y.Id, out List? list)) { - if (!tree.TryGetValue(y.Id, out var list)) - list = tree[y.Id] = new List(); - list.Add(x); + list = tree[y.Id] = new List(); } - var nset = new List(); - do - { - impact.AddRange(set); + list.Add(x); + } - foreach (var x in set) + var nset = new List(); + do + { + impact.AddRange(set); + + foreach (IContentTypeComposition x in set) + { + if (!tree.TryGetValue(x.Id, out List? list)) { - if (!tree.TryGetValue(x.Id, out var list)) continue; - nset.AddRange(list.Where(y => y.VariesByCulture())); + continue; } - set = nset; - nset = new List(); - } while (set.Count > 0); + nset.AddRange(list.Where(y => y.VariesByCulture())); + } - return impact; - } + set = nset; + nset = new List(); + } while (set.Count > 0); - // gets property types that have actually changed, and the corresponding changes - // returns null if no property type has actually changed - private Dictionary? - GetPropertyVariationChanges(IEnumerable propertyTypes) - { - var propertyTypesL = propertyTypes.ToList(); - - // select the current variations (before the change) from database - var selectCurrentVariations = Sql() - .Select(x => x.Id, x => x.Variations) - .From() - .WhereIn(x => x.Id, propertyTypesL.Select(x => x.Id)); + return impact; + } - var oldVariations = Database.Dictionary(selectCurrentVariations); + // gets property types that have actually changed, and the corresponding changes + // returns null if no property type has actually changed + private Dictionary? + GetPropertyVariationChanges(IEnumerable propertyTypes) + { + var propertyTypesL = propertyTypes.ToList(); - // build a dictionary of actual changes - Dictionary? changes = null; + // select the current variations (before the change) from database + Sql selectCurrentVariations = Sql() + .Select(x => x.Id, x => x.Variations) + .From() + .WhereIn(x => x.Id, propertyTypesL.Select(x => x.Id)); - foreach (var propertyType in propertyTypesL) - { - // new property type, ignore - if (!oldVariations.TryGetValue(propertyType.Id, out var oldVariationB)) - continue; - var oldVariation = (ContentVariation)oldVariationB; // NPoco cannot fetch directly + Dictionary? oldVariations = Database.Dictionary(selectCurrentVariations); - // only those property types that *actually* changed - var newVariation = propertyType.Variations; - if (oldVariation == newVariation) - continue; + // build a dictionary of actual changes + Dictionary? changes = null; - // allocate the dictionary only when needed - if (changes == null) - changes = new Dictionary(); - - changes[propertyType.Id] = (oldVariation, newVariation); + foreach (IPropertyType propertyType in propertyTypesL) + { + // new property type, ignore + if (!oldVariations.TryGetValue(propertyType.Id, out var oldVariationB)) + { + continue; } - return changes; - } + var oldVariation = (ContentVariation)oldVariationB; // NPoco cannot fetch directly - /// - /// Clear any redirects associated with content for a content type - /// - private void Clear301Redirects(IContentTypeComposition contentType) - { - //first clear out any existing property data that might already exists under the default lang - var sqlSelect = Sql().Select(x => x.UniqueId) - .From() - .InnerJoin().On(x => x.NodeId, x => x.NodeId) - .Where(x => x.ContentTypeId == contentType.Id); - var sqlDelete = Sql() - .Delete() - .WhereIn((System.Linq.Expressions.Expression>)(x => x.ContentKey), - sqlSelect); + // only those property types that *actually* changed + ContentVariation newVariation = propertyType.Variations; + if (oldVariation == newVariation) + { + continue; + } - Database.Execute(sqlDelete); - } + // allocate the dictionary only when needed + if (changes == null) + { + changes = new Dictionary(); + } - /// - /// Clear any scheduled publishing associated with content for a content type - /// - private void ClearScheduledPublishing(IContentTypeComposition contentType) - { - // TODO: Fill this in when scheduled publishing is enabled for variants + changes[propertyType.Id] = (oldVariation, newVariation); } - /// - /// Gets the default language identifier. - /// - private int GetDefaultLanguageId() - { - var selectDefaultLanguageId = Sql() - .Select(x => x.Id) - .From() - .Where(x => x.IsDefault); + return changes; + } - return Database.First(selectDefaultLanguageId); - } + /// + /// Clear any redirects associated with content for a content type + /// + private void Clear301Redirects(IContentTypeComposition contentType) + { + //first clear out any existing property data that might already exists under the default lang + Sql sqlSelect = Sql().Select(x => x.UniqueId) + .From() + .InnerJoin().On(x => x.NodeId, x => x.NodeId) + .Where(x => x.ContentTypeId == contentType.Id); + Sql sqlDelete = Sql() + .Delete() + .WhereIn((Expression>)(x => x.ContentKey), + sqlSelect); + + Database.Execute(sqlDelete); + } - /// - /// Moves variant data for property type variation changes. - /// - private void MovePropertyTypeVariantData( - IDictionary propertyTypeChanges, - IEnumerable impacted) - { - var defaultLanguageId = GetDefaultLanguageId(); - var impactedL = impacted.Select(x => x.Id).ToList(); + /// + /// Clear any scheduled publishing associated with content for a content type + /// + private void ClearScheduledPublishing(IContentTypeComposition contentType) + { + // TODO: Fill this in when scheduled publishing is enabled for variants + } - //Group by the "To" variation so we can bulk update in the correct batches - foreach (var grouping in propertyTypeChanges.GroupBy(x => x.Value)) - { - var propertyTypeIds = grouping.Select(x => x.Key).ToList(); - var (FromVariation, ToVariation) = grouping.Key; + /// + /// Gets the default language identifier. + /// + private int GetDefaultLanguageId() + { + Sql selectDefaultLanguageId = Sql() + .Select(x => x.Id) + .From() + .Where(x => x.IsDefault); - var fromCultureEnabled = FromVariation.HasFlag(ContentVariation.Culture); - var toCultureEnabled = ToVariation.HasFlag(ContentVariation.Culture); + return Database.First(selectDefaultLanguageId); + } - if (!fromCultureEnabled && toCultureEnabled) - { - // Culture has been enabled - CopyPropertyData(null, defaultLanguageId, propertyTypeIds, impactedL); - CopyTagData(null, defaultLanguageId, propertyTypeIds, impactedL); - RenormalizeDocumentEditedFlags(propertyTypeIds, impactedL); - } - else if (fromCultureEnabled && !toCultureEnabled) - { - // Culture has been disabled - CopyPropertyData(defaultLanguageId, null, propertyTypeIds, impactedL); - CopyTagData(defaultLanguageId, null, propertyTypeIds, impactedL); - RenormalizeDocumentEditedFlags(propertyTypeIds, impactedL); - } - } - } + /// + /// Moves variant data for property type variation changes. + /// + private void MovePropertyTypeVariantData( + IDictionary propertyTypeChanges, + IEnumerable impacted) + { + var defaultLanguageId = GetDefaultLanguageId(); + var impactedL = impacted.Select(x => x.Id).ToList(); - /// - /// Moves variant data for a content type variation change. - /// - private void MoveContentTypeVariantData(IContentTypeComposition contentType, ContentVariation fromVariation, - ContentVariation toVariation) + //Group by the "To" variation so we can bulk update in the correct batches + foreach (IGrouping<(ContentVariation FromVariation, ContentVariation ToVariation), + KeyValuePair> grouping in + propertyTypeChanges.GroupBy(x => x.Value)) { - var defaultLanguageId = GetDefaultLanguageId(); + var propertyTypeIds = grouping.Select(x => x.Key).ToList(); + (ContentVariation FromVariation, ContentVariation ToVariation) = grouping.Key; - var cultureIsNotEnabled = !fromVariation.HasFlag(ContentVariation.Culture); - var cultureWillBeEnabled = toVariation.HasFlag(ContentVariation.Culture); + var fromCultureEnabled = FromVariation.HasFlag(ContentVariation.Culture); + var toCultureEnabled = ToVariation.HasFlag(ContentVariation.Culture); - if (cultureIsNotEnabled && cultureWillBeEnabled) + if (!fromCultureEnabled && toCultureEnabled) { - //move the names - //first clear out any existing names that might already exists under the default lang - //there's 2x tables to update - - //clear out the versionCultureVariation table - var sqlSelect = Sql().Select(x => x.Id) - .From() - .InnerJoin() - .On(x => x.Id, x => x.VersionId) - .InnerJoin().On(x => x.NodeId, x => x.NodeId) - .Where(x => x.ContentTypeId == contentType.Id) - .Where(x => x.LanguageId == defaultLanguageId); - var sqlDelete = Sql() - .Delete() - .WhereIn(x => x.Id, sqlSelect); - - Database.Execute(sqlDelete); - - //clear out the documentCultureVariation table - sqlSelect = Sql().Select(x => x.Id) - .From() - .InnerJoin().On(x => x.NodeId, x => x.NodeId) - .Where(x => x.ContentTypeId == contentType.Id) - .Where(x => x.LanguageId == defaultLanguageId); - sqlDelete = Sql() - .Delete() - .WhereIn(x => x.Id, sqlSelect); - - Database.Execute(sqlDelete); - - //now we need to insert names into these 2 tables based on the invariant data - - //insert rows into the versionCultureVariationDto table based on the data from contentVersionDto for the default lang - var cols = Sql().ColumnsForInsert(x => x.VersionId, x => x.Name, x => x.UpdateUserId, x => x.UpdateDate, x => x.LanguageId); - sqlSelect = Sql().Select(x => x.Id, x => x.Text, x => x.UserId, x => x.VersionDate) - .Append($", {defaultLanguageId}") //default language ID - .From() - .InnerJoin().On(x => x.NodeId, x => x.NodeId) - .Where(x => x.ContentTypeId == contentType.Id); - var sqlInsert = Sql($"INSERT INTO {ContentVersionCultureVariationDto.TableName} ({cols})").Append(sqlSelect); - - Database.Execute(sqlInsert); - - //insert rows into the documentCultureVariation table - cols = Sql().ColumnsForInsert(x => x.NodeId, x => x.Edited, x => x.Published, x => x.Name, x => x.Available, x => x.LanguageId); - sqlSelect = Sql().Select(x => x.NodeId, x => x.Edited, x => x.Published) - .AndSelect(x => x.Text) - .Append($", 1, {defaultLanguageId}") //make Available + default language ID - .From() - .InnerJoin().On(x => x.NodeId, x => x.NodeId) - .InnerJoin().On(x => x.NodeId, x => x.NodeId) - .Where(x => x.ContentTypeId == contentType.Id); - sqlInsert = Sql($"INSERT INTO {DocumentCultureVariationDto.TableName} ({cols})").Append(sqlSelect); - - Database.Execute(sqlInsert); + // Culture has been enabled + CopyPropertyData(null, defaultLanguageId, propertyTypeIds, impactedL); + CopyTagData(null, defaultLanguageId, propertyTypeIds, impactedL); + RenormalizeDocumentEditedFlags(propertyTypeIds, impactedL); } - else + else if (fromCultureEnabled && !toCultureEnabled) { - //we don't need to move the names! this is because we always keep the invariant names with the name of the default language. - - //however, if we were to move names, we could do this: BUT this doesn't work with SQLCE, for that we'd have to update row by row :( - // if we want these SQL statements back, look into GIT history + // Culture has been disabled + CopyPropertyData(defaultLanguageId, null, propertyTypeIds, impactedL); + CopyTagData(defaultLanguageId, null, propertyTypeIds, impactedL); + RenormalizeDocumentEditedFlags(propertyTypeIds, impactedL); } } + } - /// - private void CopyTagData( - int? sourceLanguageId, - int? targetLanguageId, - IReadOnlyCollection propertyTypeIds, - IReadOnlyCollection? contentTypeIds = null) - { - // note: important to use SqlNullableEquals for nullable types, cannot directly compare language identifiers + /// + /// Moves variant data for a content type variation change. + /// + private void MoveContentTypeVariantData(IContentTypeComposition contentType, ContentVariation fromVariation, + ContentVariation toVariation) + { + var defaultLanguageId = GetDefaultLanguageId(); - var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); - if (whereInArgsCount > Constants.Sql.MaxParameterCount) - throw new NotSupportedException("Too many property/content types."); + var cultureIsNotEnabled = !fromVariation.HasFlag(ContentVariation.Culture); + var cultureWillBeEnabled = toVariation.HasFlag(ContentVariation.Culture); - // delete existing relations (for target language) - // do *not* delete existing tags + if (cultureIsNotEnabled && cultureWillBeEnabled) + { + //move the names + //first clear out any existing names that might already exists under the default lang + //there's 2x tables to update - var sqlSelectTagsToDelete = Sql() - .Select(x => x.Id) - .From() - .InnerJoin().On((tag, rel) => tag.Id == rel.TagId); + //clear out the versionCultureVariation table + Sql sqlSelect = Sql().Select(x => x.Id) + .From() + .InnerJoin() + .On(x => x.Id, x => x.VersionId) + .InnerJoin().On(x => x.NodeId, x => x.NodeId) + .Where(x => x.ContentTypeId == contentType.Id) + .Where(x => x.LanguageId == defaultLanguageId); + Sql sqlDelete = Sql() + .Delete() + .WhereIn(x => x.Id, sqlSelect); - if (contentTypeIds != null) - sqlSelectTagsToDelete - .InnerJoin() - .On((rel, content) => rel.NodeId == content.NodeId) - .WhereIn(x => x.ContentTypeId, contentTypeIds); + Database.Execute(sqlDelete); - sqlSelectTagsToDelete - .WhereIn(x => x.PropertyTypeId, propertyTypeIds) - .Where(x => x.LanguageId.SqlNullableEquals(targetLanguageId, -1)); + //clear out the documentCultureVariation table + sqlSelect = Sql().Select(x => x.Id) + .From() + .InnerJoin().On(x => x.NodeId, x => x.NodeId) + .Where(x => x.ContentTypeId == contentType.Id) + .Where(x => x.LanguageId == defaultLanguageId); + sqlDelete = Sql() + .Delete() + .WhereIn(x => x.Id, sqlSelect); - var sqlDeleteRelations = Sql() - .Delete() - .WhereIn(x => x.TagId, sqlSelectTagsToDelete); + Database.Execute(sqlDelete); - Database.Execute(sqlDeleteRelations); + //now we need to insert names into these 2 tables based on the invariant data - // do *not* delete the tags - they could be used by other content types / property types - /* - var sqlDeleteTag = Sql() - .Delete() - .WhereIn(x => x.Id, sqlTagToDelete); - Database.Execute(sqlDeleteTag); - */ + //insert rows into the versionCultureVariationDto table based on the data from contentVersionDto for the default lang + var cols = Sql().ColumnsForInsert(x => x.VersionId, x => x.Name, + x => x.UpdateUserId, x => x.UpdateDate, x => x.LanguageId); + sqlSelect = Sql().Select(x => x.Id, x => x.Text, x => x.UserId, x => x.VersionDate) + .Append($", {defaultLanguageId}") //default language ID + .From() + .InnerJoin().On(x => x.NodeId, x => x.NodeId) + .Where(x => x.ContentTypeId == contentType.Id); + Sql? sqlInsert = Sql($"INSERT INTO {ContentVersionCultureVariationDto.TableName} ({cols})") + .Append(sqlSelect); - // copy tags from source language to target language - // target tags may exist already, so we have to check for existence here - // - // select tags to insert: tags pointed to by a relation ship, for proper property/content types, - // and of source language, and where we cannot left join to an existing tag with same text, - // group and languageId + Database.Execute(sqlInsert); - var targetLanguageIdS = targetLanguageId.HasValue ? targetLanguageId.ToString() : "NULL"; - var sqlSelectTagsToInsert = Sql() - .SelectDistinct(x => x.Text, x => x.Group) - .Append(", " + targetLanguageIdS) - .From(); + //insert rows into the documentCultureVariation table + cols = Sql().ColumnsForInsert(x => x.NodeId, x => x.Edited, x => x.Published, + x => x.Name, x => x.Available, x => x.LanguageId); + sqlSelect = Sql().Select(x => x.NodeId, x => x.Edited, x => x.Published) + .AndSelect(x => x.Text) + .Append($", 1, {defaultLanguageId}") //make Available + default language ID + .From() + .InnerJoin().On(x => x.NodeId, x => x.NodeId) + .InnerJoin().On(x => x.NodeId, x => x.NodeId) + .Where(x => x.ContentTypeId == contentType.Id); + sqlInsert = Sql($"INSERT INTO {DocumentCultureVariationDto.TableName} ({cols})").Append(sqlSelect); - sqlSelectTagsToInsert - .InnerJoin().On((tag, rel) => tag.Id == rel.TagId) - .LeftJoin("xtags") - .On( - (tag, xtag) => tag.Text == xtag.Text && tag.Group == xtag.Group && - xtag.LanguageId.SqlNullableEquals(targetLanguageId, -1), aliasRight: "xtags"); + Database.Execute(sqlInsert); + } + } - if (contentTypeIds != null) - sqlSelectTagsToInsert - .InnerJoin() - .On((rel, content) => rel.NodeId == content.NodeId) - .WhereIn(x => x.ContentTypeId, contentTypeIds); + /// + private void CopyTagData( + int? sourceLanguageId, + int? targetLanguageId, + IReadOnlyCollection propertyTypeIds, + IReadOnlyCollection? contentTypeIds = null) + { + // note: important to use SqlNullableEquals for nullable types, cannot directly compare language identifiers - sqlSelectTagsToInsert - .WhereIn(x => x.PropertyTypeId, propertyTypeIds) - .WhereNull(x => x.Id, "xtags") // ie, not exists - .Where(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1)); + var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); + if (whereInArgsCount > Constants.Sql.MaxParameterCount) + { + throw new NotSupportedException("Too many property/content types."); + } - var cols = Sql().ColumnsForInsert(x => x.Text, x => x.Group, x => x.LanguageId); - var sqlInsertTags = Sql($"INSERT INTO {TagDto.TableName} ({cols})").Append(sqlSelectTagsToInsert); + // delete existing relations (for target language) + // do *not* delete existing tags - Database.Execute(sqlInsertTags); + Sql sqlSelectTagsToDelete = Sql() + .Select(x => x.Id) + .From() + .InnerJoin().On((tag, rel) => tag.Id == rel.TagId); - // create relations to new tags - // any existing relations have been deleted above, no need to check for existence here - // - // select node id and property type id from existing relations to tags of source language, - // for proper property/content types, and select new tag id from tags, with matching text, - // and group, but for the target language - - var sqlSelectRelationsToInsert = Sql() - .SelectDistinct(x => x.NodeId, x => x.PropertyTypeId) - .AndSelect("otag", x => x.Id) - .From() - .InnerJoin().On((rel, tag) => rel.TagId == tag.Id) - .InnerJoin("otag") - .On( - (tag, otag) => tag.Text == otag.Text && tag.Group == otag.Group && - otag.LanguageId.SqlNullableEquals(targetLanguageId, -1), aliasRight: "otag"); + if (contentTypeIds != null) + { + sqlSelectTagsToDelete + .InnerJoin() + .On((rel, content) => rel.NodeId == content.NodeId) + .WhereIn(x => x.ContentTypeId, contentTypeIds); + } - if (contentTypeIds != null) - sqlSelectRelationsToInsert - .InnerJoin() - .On((rel, content) => rel.NodeId == content.NodeId) - .WhereIn(x => x.ContentTypeId, contentTypeIds); + sqlSelectTagsToDelete + .WhereIn(x => x.PropertyTypeId, propertyTypeIds) + .Where(x => x.LanguageId.SqlNullableEquals(targetLanguageId, -1)); + + Sql sqlDeleteRelations = Sql() + .Delete() + .WhereIn(x => x.TagId, sqlSelectTagsToDelete); + + Database.Execute(sqlDeleteRelations); + + // do *not* delete the tags - they could be used by other content types / property types + /* + var sqlDeleteTag = Sql() + .Delete() + .WhereIn(x => x.Id, sqlTagToDelete); + Database.Execute(sqlDeleteTag); + */ + + // copy tags from source language to target language + // target tags may exist already, so we have to check for existence here + // + // select tags to insert: tags pointed to by a relation ship, for proper property/content types, + // and of source language, and where we cannot left join to an existing tag with same text, + // group and languageId + + var targetLanguageIdS = targetLanguageId.HasValue ? targetLanguageId.ToString() : "NULL"; + Sql sqlSelectTagsToInsert = Sql() + .SelectDistinct(x => x.Text, x => x.Group) + .Append(", " + targetLanguageIdS) + .From(); + + sqlSelectTagsToInsert + .InnerJoin().On((tag, rel) => tag.Id == rel.TagId) + .LeftJoin("xtags") + .On( + (tag, xtag) => tag.Text == xtag.Text && tag.Group == xtag.Group && + xtag.LanguageId.SqlNullableEquals(targetLanguageId, -1), aliasRight: "xtags"); + + if (contentTypeIds != null) + { + sqlSelectTagsToInsert + .InnerJoin() + .On((rel, content) => rel.NodeId == content.NodeId) + .WhereIn(x => x.ContentTypeId, contentTypeIds); + } + sqlSelectTagsToInsert + .WhereIn(x => x.PropertyTypeId, propertyTypeIds) + .WhereNull(x => x.Id, "xtags") // ie, not exists + .Where(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1)); + + var cols = Sql().ColumnsForInsert(x => x.Text, x => x.Group, x => x.LanguageId); + Sql? sqlInsertTags = Sql($"INSERT INTO {TagDto.TableName} ({cols})").Append(sqlSelectTagsToInsert); + + Database.Execute(sqlInsertTags); + + // create relations to new tags + // any existing relations have been deleted above, no need to check for existence here + // + // select node id and property type id from existing relations to tags of source language, + // for proper property/content types, and select new tag id from tags, with matching text, + // and group, but for the target language + + Sql sqlSelectRelationsToInsert = Sql() + .SelectDistinct(x => x.NodeId, x => x.PropertyTypeId) + .AndSelect("otag", x => x.Id) + .From() + .InnerJoin().On((rel, tag) => rel.TagId == tag.Id) + .InnerJoin("otag") + .On( + (tag, otag) => tag.Text == otag.Text && tag.Group == otag.Group && + otag.LanguageId.SqlNullableEquals(targetLanguageId, -1), aliasRight: "otag"); + + if (contentTypeIds != null) + { sqlSelectRelationsToInsert - .Where(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1)) - .WhereIn(x => x.PropertyTypeId, propertyTypeIds); + .InnerJoin() + .On((rel, content) => rel.NodeId == content.NodeId) + .WhereIn(x => x.ContentTypeId, contentTypeIds); + } - var relationColumnsToInsert = Sql().ColumnsForInsert(x => x.NodeId, x => x.PropertyTypeId, x => x.TagId); - var sqlInsertRelations = Sql($"INSERT INTO {TagRelationshipDto.TableName} ({relationColumnsToInsert})").Append(sqlSelectRelationsToInsert); + sqlSelectRelationsToInsert + .Where(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1)) + .WhereIn(x => x.PropertyTypeId, propertyTypeIds); - Database.Execute(sqlInsertRelations); + var relationColumnsToInsert = + Sql().ColumnsForInsert(x => x.NodeId, x => x.PropertyTypeId, x => x.TagId); + Sql? sqlInsertRelations = + Sql($"INSERT INTO {TagRelationshipDto.TableName} ({relationColumnsToInsert})") + .Append(sqlSelectRelationsToInsert); - // delete original relations - *not* the tags - all of them - // cannot really "go back" with relations, would have to do it with property values + Database.Execute(sqlInsertRelations); - sqlSelectTagsToDelete = Sql() - .Select(x => x.Id) - .From() - .InnerJoin().On((tag, rel) => tag.Id == rel.TagId); + // delete original relations - *not* the tags - all of them + // cannot really "go back" with relations, would have to do it with property values - if (contentTypeIds != null) - sqlSelectTagsToDelete - .InnerJoin() - .On((rel, content) => rel.NodeId == content.NodeId) - .WhereIn(x => x.ContentTypeId, contentTypeIds); + sqlSelectTagsToDelete = Sql() + .Select(x => x.Id) + .From() + .InnerJoin().On((tag, rel) => tag.Id == rel.TagId); + if (contentTypeIds != null) + { sqlSelectTagsToDelete - .WhereIn(x => x.PropertyTypeId, propertyTypeIds) - .Where(x => !x.LanguageId.SqlNullableEquals(targetLanguageId, -1)); - - sqlDeleteRelations = Sql() - .Delete() - .WhereIn(x => x.TagId, sqlSelectTagsToDelete); - - Database.Execute(sqlDeleteRelations); - - // no - /* - var sqlDeleteTag = Sql() - .Delete() - .WhereIn(x => x.Id, sqlTagToDelete); - Database.Execute(sqlDeleteTag); - */ + .InnerJoin() + .On((rel, content) => rel.NodeId == content.NodeId) + .WhereIn(x => x.ContentTypeId, contentTypeIds); } - /// - /// Copies property data from one language to another. - /// - /// The source language (can be null ie invariant). - /// The target language (can be null ie invariant) - /// The property type identifiers. - /// The content type identifiers. - private void CopyPropertyData(int? sourceLanguageId, int? targetLanguageId, - IReadOnlyCollection propertyTypeIds, IReadOnlyCollection? contentTypeIds = null) - { - // note: important to use SqlNullableEquals for nullable types, cannot directly compare language identifiers - // - var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); - if (whereInArgsCount > Constants.Sql.MaxParameterCount) - throw new NotSupportedException("Too many property/content types."); - - //first clear out any existing property data that might already exists under the target language - var sqlDelete = Sql() - .Delete(); - - // not ok for SqlCe (no JOIN in DELETE) - //if (contentTypeIds != null) - // sqlDelete - // .From() - // .InnerJoin().On((pdata, cversion) => pdata.VersionId == cversion.Id) - // .InnerJoin().On((cversion, c) => cversion.NodeId == c.NodeId); - - Sql? inSql = null; - if (contentTypeIds != null) - { - inSql = Sql() - .Select(x => x.Id) - .From() - .InnerJoin() - .On((cversion, c) => cversion.NodeId == c.NodeId) - .WhereIn(x => x.ContentTypeId, contentTypeIds); - sqlDelete.WhereIn(x => x.VersionId, inSql); - } + sqlSelectTagsToDelete + .WhereIn(x => x.PropertyTypeId, propertyTypeIds) + .Where(x => !x.LanguageId.SqlNullableEquals(targetLanguageId, -1)); - sqlDelete.Where(x => x.LanguageId.SqlNullableEquals(targetLanguageId, -1)); + sqlDeleteRelations = Sql() + .Delete() + .WhereIn(x => x.TagId, sqlSelectTagsToDelete); - sqlDelete - .WhereIn(x => x.PropertyTypeId, propertyTypeIds); + Database.Execute(sqlDeleteRelations); - // see note above, not ok for SqlCe - //if (contentTypeIds != null) - // sqlDelete - // .WhereIn(x => x.ContentTypeId, contentTypeIds); + // no + /* + var sqlDeleteTag = Sql() + .Delete() + .WhereIn(x => x.Id, sqlTagToDelete); + Database.Execute(sqlDeleteTag); + */ + } - Database.Execute(sqlDelete); + /// + /// Copies property data from one language to another. + /// + /// The source language (can be null ie invariant). + /// The target language (can be null ie invariant) + /// The property type identifiers. + /// The content type identifiers. + private void CopyPropertyData(int? sourceLanguageId, int? targetLanguageId, + IReadOnlyCollection propertyTypeIds, IReadOnlyCollection? contentTypeIds = null) + { + // note: important to use SqlNullableEquals for nullable types, cannot directly compare language identifiers + // + var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); + if (whereInArgsCount > Constants.Sql.MaxParameterCount) + { + throw new NotSupportedException("Too many property/content types."); + } - //now insert all property data into the target language that exists under the source language - var targetLanguageIdS = targetLanguageId.HasValue ? targetLanguageId.ToString() : "NULL"; - var cols = Sql().ColumnsForInsert(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue, x => x.LanguageId); - var sqlSelectData = Sql().Select(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue) - .Append(", " + targetLanguageIdS) //default language ID - .From(); + //first clear out any existing property data that might already exists under the target language + Sql sqlDelete = Sql() + .Delete(); - if (contentTypeIds != null) - sqlSelectData - .InnerJoin() - .On((pdata, cversion) => pdata.VersionId == cversion.Id) - .InnerJoin() - .On((cversion, c) => cversion.NodeId == c.NodeId); + // not ok for SqlCe (no JOIN in DELETE) + //if (contentTypeIds != null) + // sqlDelete + // .From() + // .InnerJoin().On((pdata, cversion) => pdata.VersionId == cversion.Id) + // .InnerJoin().On((cversion, c) => cversion.NodeId == c.NodeId); - sqlSelectData.Where(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1)); + Sql? inSql = null; + if (contentTypeIds != null) + { + inSql = Sql() + .Select(x => x.Id) + .From() + .InnerJoin() + .On((cversion, c) => cversion.NodeId == c.NodeId) + .WhereIn(x => x.ContentTypeId, contentTypeIds); + sqlDelete.WhereIn(x => x.VersionId, inSql); + } - sqlSelectData - .WhereIn(x => x.PropertyTypeId, propertyTypeIds); + sqlDelete.Where(x => x.LanguageId.SqlNullableEquals(targetLanguageId, -1)); - if (contentTypeIds != null) - sqlSelectData - .WhereIn(x => x.ContentTypeId, contentTypeIds); + sqlDelete + .WhereIn(x => x.PropertyTypeId, propertyTypeIds); - var sqlInsert = Sql($"INSERT INTO {PropertyDataDto.TableName} ({cols})").Append(sqlSelectData); + // see note above, not ok for SqlCe + //if (contentTypeIds != null) + // sqlDelete + // .WhereIn(x => x.ContentTypeId, contentTypeIds); - Database.Execute(sqlInsert); + Database.Execute(sqlDelete); - // when copying from Culture, keep the original values around in case we want to go back - // when copying from Nothing, kill the original values, we don't want them around - if (sourceLanguageId == null) - { - sqlDelete = Sql() - .Delete(); + //now insert all property data into the target language that exists under the source language + var targetLanguageIdS = targetLanguageId.HasValue ? targetLanguageId.ToString() : "NULL"; + var cols = Sql().ColumnsForInsert(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, + x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue, + x => x.LanguageId); + Sql sqlSelectData = Sql().Select(x => x.VersionId, x => x.PropertyTypeId, + x => x.Segment, x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, + x => x.TextValue) + .Append(", " + targetLanguageIdS) //default language ID + .From(); - if (contentTypeIds != null) - sqlDelete.WhereIn(x => x.VersionId, inSql); + if (contentTypeIds != null) + { + sqlSelectData + .InnerJoin() + .On((pdata, cversion) => pdata.VersionId == cversion.Id) + .InnerJoin() + .On((cversion, c) => cversion.NodeId == c.NodeId); + } - sqlDelete - .Where(x => x.LanguageId == null) - .WhereIn(x => x.PropertyTypeId, propertyTypeIds); + sqlSelectData.Where(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1)); - Database.Execute(sqlDelete); - } - } + sqlSelectData + .WhereIn(x => x.PropertyTypeId, propertyTypeIds); - /// - /// Re-normalizes the edited value in the umbracoDocumentCultureVariation and umbracoDocument table when variations are changed - /// - /// - /// - /// - /// If this is not done, then in some cases the "edited" value for a particular culture for a document will remain true when it should be false - /// if the property was changed to invariant. In order to do this we need to recalculate this value based on the values stored for each - /// property, culture and current/published version. - /// - private void RenormalizeDocumentEditedFlags(IReadOnlyCollection propertyTypeIds, - IReadOnlyCollection? contentTypeIds = null) + if (contentTypeIds != null) { - var defaultLang = LanguageRepository.GetDefaultId(); + sqlSelectData + .WhereIn(x => x.ContentTypeId, contentTypeIds); + } - //This will build up a query to get the property values of both the current and the published version so that we can check - //based on the current variance of each item to see if it's 'edited' value should be true/false. + Sql? sqlInsert = Sql($"INSERT INTO {PropertyDataDto.TableName} ({cols})").Append(sqlSelectData); - var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); - if (whereInArgsCount > Constants.Sql.MaxParameterCount) - throw new NotSupportedException("Too many property/content types."); + Database.Execute(sqlInsert); - var propertySql = Sql() - .Select() - .AndSelect(x => x.NodeId, x => x.Current) - .AndSelect(x => x.Published) - .AndSelect(x => x.Variations) - .From() - .InnerJoin() - .On((left, right) => left.Id == right.VersionId) - .InnerJoin() - .On((left, right) => left.Id == right.PropertyTypeId); + // when copying from Culture, keep the original values around in case we want to go back + // when copying from Nothing, kill the original values, we don't want them around + if (sourceLanguageId == null) + { + sqlDelete = Sql() + .Delete(); if (contentTypeIds != null) { - propertySql.InnerJoin() - .On((c, cversion) => c.NodeId == cversion.NodeId); + sqlDelete.WhereIn(x => x.VersionId, inSql); } - propertySql.LeftJoin() - .On((docversion, cversion) => cversion.Id == docversion.Id) - .Where((docversion, cversion) => - cversion.Current || docversion.Published) + sqlDelete + .Where(x => x.LanguageId == null) .WhereIn(x => x.PropertyTypeId, propertyTypeIds); - if (contentTypeIds != null) - { - propertySql.WhereIn(x => x.ContentTypeId, contentTypeIds); - } + Database.Execute(sqlDelete); + } + } - propertySql - .OrderBy(x => x.NodeId) - .OrderBy(x => x.PropertyTypeId, x => x.LanguageId, x => x.VersionId); + /// + /// Re-normalizes the edited value in the umbracoDocumentCultureVariation and umbracoDocument table when variations are + /// changed + /// + /// + /// + /// + /// If this is not done, then in some cases the "edited" value for a particular culture for a document will remain true + /// when it should be false + /// if the property was changed to invariant. In order to do this we need to recalculate this value based on the values + /// stored for each + /// property, culture and current/published version. + /// + private void RenormalizeDocumentEditedFlags(IReadOnlyCollection propertyTypeIds, + IReadOnlyCollection? contentTypeIds = null) + { + var defaultLang = LanguageRepository.GetDefaultId(); - //keep track of this node/lang to mark or unmark a culture as edited - var editedLanguageVersions = new Dictionary<(int nodeId, int? langId), bool>(); - //keep track of which node to mark or unmark as edited - var editedDocument = new Dictionary(); - var nodeId = -1; - var propertyTypeId = -1; + //This will build up a query to get the property values of both the current and the published version so that we can check + //based on the current variance of each item to see if it's 'edited' value should be true/false. - PropertyValueVersionDto? pubRow = null; + var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); + if (whereInArgsCount > Constants.Sql.MaxParameterCount) + { + throw new NotSupportedException("Too many property/content types."); + } - //This is a reader (Query), we are not fetching this all into memory so we cannot make any changes during this iteration, we are just collecting data. - //Published data will always come before Current data based on the version id sort. - //There will only be one published row (max) and one current row per property. - foreach (var row in Database.Query(propertySql)) - { - //make sure to reset on each node/property change - if (nodeId != row.NodeId || propertyTypeId != row.PropertyTypeId) - { - nodeId = row.NodeId; - propertyTypeId = row.PropertyTypeId; - pubRow = null; - } + Sql propertySql = Sql() + .Select() + .AndSelect(x => x.NodeId, x => x.Current) + .AndSelect(x => x.Published) + .AndSelect(x => x.Variations) + .From() + .InnerJoin() + .On((left, right) => left.Id == right.VersionId) + .InnerJoin() + .On((left, right) => left.Id == right.PropertyTypeId); + + if (contentTypeIds != null) + { + propertySql.InnerJoin() + .On((c, cversion) => c.NodeId == cversion.NodeId); + } - if (row.Published) - pubRow = row; + propertySql.LeftJoin() + .On((docversion, cversion) => cversion.Id == docversion.Id) + .Where((docversion, cversion) => + cversion.Current || docversion.Published) + .WhereIn(x => x.PropertyTypeId, propertyTypeIds); - if (row.Current) - { - var propVariations = (ContentVariation)row.Variations; + if (contentTypeIds != null) + { + propertySql.WhereIn(x => x.ContentTypeId, contentTypeIds); + } - //if this prop doesn't vary but the row has a lang assigned or vice versa, flag this as not edited - if (!propVariations.VariesByCulture() && row.LanguageId.HasValue - || propVariations.VariesByCulture() && !row.LanguageId.HasValue) - { - //Flag this as not edited for this node/lang if the key doesn't exist - if (!editedLanguageVersions.TryGetValue((row.NodeId, row.LanguageId), out _)) - editedLanguageVersions.Add((row.NodeId, row.LanguageId), false); - - //mark as false if the item doesn't exist, else coerce to true - editedDocument[row.NodeId] = editedDocument.TryGetValue(row.NodeId, out var edited) - ? (edited |= false) - : false; - } - else if (pubRow == null) - { - //this would mean that that this property is 'edited' since there is no published version - editedLanguageVersions[(row.NodeId, row.LanguageId)] = true; - editedDocument[row.NodeId] = true; - } - //compare the property values, if they differ from versions then flag the current version as edited - else if (IsPropertyValueChanged(pubRow, row)) - { - //Here we would check if the property is invariant, in which case the edited language should be indicated by the default lang - editedLanguageVersions[ - (row.NodeId, !propVariations.VariesByCulture() ? defaultLang : row.LanguageId)] = true; - editedDocument[row.NodeId] = true; - } + propertySql + .OrderBy(x => x.NodeId) + .OrderBy(x => x.PropertyTypeId, x => x.LanguageId, x => x.VersionId); - //reset - pubRow = null; - } - } + //keep track of this node/lang to mark or unmark a culture as edited + var editedLanguageVersions = new Dictionary<(int nodeId, int? langId), bool>(); + //keep track of which node to mark or unmark as edited + var editedDocument = new Dictionary(); + var nodeId = -1; + var propertyTypeId = -1; - // lookup all matching rows in umbracoDocumentCultureVariation - // fetch in batches to account for maximum parameter count (distinct languages can't exceed 2000) - var languageIds = editedLanguageVersions.Keys.Select(x => x.langId).Distinct().ToArray(); - var nodeIds = editedLanguageVersions.Keys.Select(x => x.nodeId).Distinct(); - var docCultureVariationsToUpdate = nodeIds.InGroupsOf(Constants.Sql.MaxParameterCount - languageIds.Length) - .SelectMany(group => - { - var sql = Sql().Select().From() - .WhereIn(x => x.LanguageId, languageIds) - .WhereIn(x => x.NodeId, group); + PropertyValueVersionDto? pubRow = null; - return Database.Fetch(sql); - }) - .ToDictionary(x => (x.NodeId, (int?)x.LanguageId), - x => x); //convert to dictionary with the same key type + //This is a reader (Query), we are not fetching this all into memory so we cannot make any changes during this iteration, we are just collecting data. + //Published data will always come before Current data based on the version id sort. + //There will only be one published row (max) and one current row per property. + foreach (PropertyValueVersionDto? row in Database.Query(propertySql)) + { + //make sure to reset on each node/property change + if (nodeId != row.NodeId || propertyTypeId != row.PropertyTypeId) + { + nodeId = row.NodeId; + propertyTypeId = row.PropertyTypeId; + pubRow = null; + } - var toUpdate = new List(); - foreach (var ev in editedLanguageVersions) + if (row.Published) { - if (docCultureVariationsToUpdate.TryGetValue(ev.Key, out var docVariations)) + pubRow = row; + } + + if (row.Current) + { + var propVariations = (ContentVariation)row.Variations; + + //if this prop doesn't vary but the row has a lang assigned or vice versa, flag this as not edited + if ((!propVariations.VariesByCulture() && row.LanguageId.HasValue) + || (propVariations.VariesByCulture() && !row.LanguageId.HasValue)) { - //check if it needs updating - if (docVariations.Edited != ev.Value) + //Flag this as not edited for this node/lang if the key doesn't exist + if (!editedLanguageVersions.TryGetValue((row.NodeId, row.LanguageId), out _)) { - docVariations.Edited = ev.Value; - toUpdate.Add(docVariations); + editedLanguageVersions.Add((row.NodeId, row.LanguageId), false); } + + //mark as false if the item doesn't exist, else coerce to true + editedDocument[row.NodeId] = editedDocument.TryGetValue(row.NodeId, out var edited) + ? edited |= false + : false; + } + else if (pubRow == null) + { + //this would mean that that this property is 'edited' since there is no published version + editedLanguageVersions[(row.NodeId, row.LanguageId)] = true; + editedDocument[row.NodeId] = true; } - else if (ev.Key.langId.HasValue) + //compare the property values, if they differ from versions then flag the current version as edited + else if (IsPropertyValueChanged(pubRow, row)) { - //This should never happen! If a property culture is flagged as edited then the culture must exist at the document level - throw new PanicException( - $"The existing DocumentCultureVariationDto was not found for node {ev.Key.nodeId} and language {ev.Key.langId}"); + //Here we would check if the property is invariant, in which case the edited language should be indicated by the default lang + editedLanguageVersions[ + (row.NodeId, !propVariations.VariesByCulture() ? defaultLang : row.LanguageId)] = true; + editedDocument[row.NodeId] = true; } + + //reset + pubRow = null; } + } - //Now bulk update the table DocumentCultureVariationDto, once for edited = true, another for edited = false - foreach (var editValue in toUpdate.GroupBy(x => x.Edited)) + // lookup all matching rows in umbracoDocumentCultureVariation + // fetch in batches to account for maximum parameter count (distinct languages can't exceed 2000) + var languageIds = editedLanguageVersions.Keys.Select(x => x.langId).Distinct().ToArray(); + IEnumerable nodeIds = editedLanguageVersions.Keys.Select(x => x.nodeId).Distinct(); + var docCultureVariationsToUpdate = nodeIds.InGroupsOf(Constants.Sql.MaxParameterCount - languageIds.Length) + .SelectMany(group => { - Database.Execute(Sql().Update(u => u.Set(x => x.Edited, editValue.Key)) - .WhereIn(x => x.Id, editValue.Select(x => x.Id))); - } + Sql sql = Sql().Select().From() + .WhereIn(x => x.LanguageId, languageIds) + .WhereIn(x => x.NodeId, group); + + return Database.Fetch(sql); + }) + .ToDictionary(x => (x.NodeId, (int?)x.LanguageId), + x => x); //convert to dictionary with the same key type - //Now bulk update the umbracoDocument table - foreach (var editValue in editedDocument.GroupBy(x => x.Value)) + var toUpdate = new List(); + foreach (KeyValuePair<(int nodeId, int? langId), bool> ev in editedLanguageVersions) + { + if (docCultureVariationsToUpdate.TryGetValue(ev.Key, out DocumentCultureVariationDto? docVariations)) { - Database.Execute(Sql().Update(u => u.Set(x => x.Edited, editValue.Key)) - .WhereIn(x => x.NodeId, editValue.Select(x => x.Key))); + //check if it needs updating + if (docVariations.Edited != ev.Value) + { + docVariations.Edited = ev.Value; + toUpdate.Add(docVariations); + } + } + else if (ev.Key.langId.HasValue) + { + //This should never happen! If a property culture is flagged as edited then the culture must exist at the document level + throw new PanicException( + $"The existing DocumentCultureVariationDto was not found for node {ev.Key.nodeId} and language {ev.Key.langId}"); } } - private static bool IsPropertyValueChanged(PropertyValueVersionDto pubRow, PropertyValueVersionDto row) + //Now bulk update the table DocumentCultureVariationDto, once for edited = true, another for edited = false + foreach (IGrouping editValue in toUpdate.GroupBy(x => x.Edited)) { - return !pubRow.TextValue.IsNullOrWhiteSpace() && pubRow.TextValue != row.TextValue - || !pubRow.VarcharValue.IsNullOrWhiteSpace() && pubRow.VarcharValue != row.VarcharValue - || pubRow.DateValue.HasValue && pubRow.DateValue != row.DateValue - || pubRow.DecimalValue.HasValue && pubRow.DecimalValue != row.DecimalValue - || pubRow.IntValue.HasValue && pubRow.IntValue != row.IntValue; + Database.Execute(Sql().Update(u => u.Set(x => x.Edited, editValue.Key)) + .WhereIn(x => x.Id, editValue.Select(x => x.Id))); } - private class NameCompareDto + //Now bulk update the umbracoDocument table + foreach (IGrouping> editValue in editedDocument.GroupBy(x => x.Value)) { - public int NodeId { get; set; } - public int CurrentVersion { get; set; } - public int LanguageId { get; set; } - public string? CurrentName { get; set; } - public string? PublishedName { get; set; } - public int? PublishedVersion { get; set; } - public int Id { get; set; } // the Id of the DocumentCultureVariationDto - public bool Edited { get; set; } + Database.Execute(Sql().Update(u => u.Set(x => x.Edited, editValue.Key)) + .WhereIn(x => x.NodeId, editValue.Select(x => x.Key))); } + } - private class PropertyValueVersionDto - { - public int VersionId { get; set; } - public int PropertyTypeId { get; set; } - public int? LanguageId { get; set; } - public string? Segment { get; set; } - public int? IntValue { get; set; } - - private decimal? _decimalValue; - - [Column("decimalValue")] - public decimal? DecimalValue - { - get => _decimalValue; - set => _decimalValue = value?.Normalize(); - } - - public DateTime? DateValue { get; set; } - public string? VarcharValue { get; set; } - public string? TextValue { get; set; } + private static bool IsPropertyValueChanged(PropertyValueVersionDto pubRow, PropertyValueVersionDto row) => + (!pubRow.TextValue.IsNullOrWhiteSpace() && pubRow.TextValue != row.TextValue) + || (!pubRow.VarcharValue.IsNullOrWhiteSpace() && pubRow.VarcharValue != row.VarcharValue) + || (pubRow.DateValue.HasValue && pubRow.DateValue != row.DateValue) + || (pubRow.DecimalValue.HasValue && pubRow.DecimalValue != row.DecimalValue) + || (pubRow.IntValue.HasValue && pubRow.IntValue != row.IntValue); - public int NodeId { get; set; } - public bool Current { get; set; } - public bool Published { get; set; } + private void DeletePropertyType(int contentTypeId, int propertyTypeId) + { + // first clear dependencies + Database.Delete("WHERE propertyTypeId = @Id", new {Id = propertyTypeId}); + Database.Delete("WHERE propertyTypeId = @Id", new {Id = propertyTypeId}); - public byte Variations { get; set; } - } + // then delete the property type + Database.Delete("WHERE contentTypeId = @Id AND id = @PropertyTypeId", + new {Id = contentTypeId, PropertyTypeId = propertyTypeId}); + } - private void DeletePropertyType(int contentTypeId, int propertyTypeId) + protected void ValidateAlias(IPropertyType pt) + { + if (string.IsNullOrWhiteSpace(pt.Alias)) { - // first clear dependencies - Database.Delete("WHERE propertyTypeId = @Id", new {Id = propertyTypeId}); - Database.Delete("WHERE propertyTypeId = @Id", new {Id = propertyTypeId}); + var ex = new InvalidOperationException( + $"Property Type '{pt.Name}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias."); + + Logger.LogError( + "Property Type '{PropertyTypeName}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias.", + pt.Name); - // then delete the property type - Database.Delete("WHERE contentTypeId = @Id AND id = @PropertyTypeId", - new {Id = contentTypeId, PropertyTypeId = propertyTypeId}); + throw ex; } + } - protected void ValidateAlias(IPropertyType pt) + protected void ValidateAlias(TEntity entity) + { + if (string.IsNullOrWhiteSpace(entity.Alias)) { - if (string.IsNullOrWhiteSpace(pt.Alias)) - { - var ex = new InvalidOperationException( - $"Property Type '{pt.Name}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias."); + var ex = new InvalidOperationException( + $"{typeof(TEntity).Name} '{entity.Name}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias."); - Logger.LogError( - "Property Type '{PropertyTypeName}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias.", - pt.Name); + Logger.LogError( + "{EntityTypeName} '{EntityName}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias.", + typeof(TEntity).Name, + entity.Name); - throw ex; - } + throw ex; } + } - protected void ValidateAlias(TEntity entity) + /// + /// Try to set the data type id based on its ControlId + /// + /// + private void AssignDataTypeFromPropertyEditor(IPropertyType propertyType) + { + //we cannot try to assign a data type of it's empty + if (propertyType.PropertyEditorAlias.IsNullOrWhiteSpace() == false) { - if (string.IsNullOrWhiteSpace(entity.Alias)) + Sql sql = Sql() + .Select(dt => dt.Select(x => x.NodeDto)) + .From() + .InnerJoin().On((dt, n) => dt.NodeId == n.NodeId) + .Where("propertyEditorAlias = @propertyEditorAlias", + new {propertyEditorAlias = propertyType.PropertyEditorAlias}) + .OrderBy(typeDto => typeDto.NodeId); + DataTypeDto? datatype = Database.FirstOrDefault(sql); + //we cannot assign a data type if one was not found + if (datatype != null) { - var ex = new InvalidOperationException( - $"{typeof(TEntity).Name} '{entity.Name}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias."); - - Logger.LogError( - "{EntityTypeName} '{EntityName}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias.", - typeof(TEntity).Name, - entity.Name); - - throw ex; + propertyType.DataTypeId = datatype.NodeId; + propertyType.DataTypeKey = datatype.NodeDto.UniqueId; } - } - - /// - /// Try to set the data type id based on its ControlId - /// - /// - private void AssignDataTypeFromPropertyEditor(IPropertyType propertyType) - { - //we cannot try to assign a data type of it's empty - if (propertyType.PropertyEditorAlias.IsNullOrWhiteSpace() == false) + else { - var sql = Sql() - .Select(dt => dt.Select(x => x.NodeDto)) - .From() - .InnerJoin().On((dt, n) => dt.NodeId == n.NodeId) - .Where("propertyEditorAlias = @propertyEditorAlias", - new {propertyEditorAlias = propertyType.PropertyEditorAlias}) - .OrderBy(typeDto => typeDto.NodeId); - var datatype = Database.FirstOrDefault(sql); - //we cannot assign a data type if one was not found - if (datatype != null) - { - propertyType.DataTypeId = datatype.NodeId; - propertyType.DataTypeKey = datatype.NodeDto.UniqueId; - } - else - { - Logger.LogWarning( - "Could not assign a data type for the property type {PropertyTypeAlias} since no data type was found with a property editor {PropertyEditorAlias}", - propertyType.Alias, propertyType.PropertyEditorAlias); - } + Logger.LogWarning( + "Could not assign a data type for the property type {PropertyTypeAlias} since no data type was found with a property editor {PropertyEditorAlias}", + propertyType.Alias, propertyType.PropertyEditorAlias); } } + } - protected abstract TEntity? PerformGet(Guid id); - protected abstract TEntity? PerformGet(string alias); - protected abstract IEnumerable? PerformGetAll(params Guid[]? ids); - protected abstract bool PerformExists(Guid id); - - /// - /// Gets an Entity by alias - /// - /// - /// - public TEntity? Get(string alias) - { - return PerformGet(alias); - } + protected abstract TEntity? PerformGet(Guid id); + protected abstract TEntity? PerformGet(string alias); + protected abstract IEnumerable? PerformGetAll(params Guid[]? ids); + protected abstract bool PerformExists(Guid id); - /// - /// Gets an Entity by Id - /// - /// - /// - public TEntity? Get(Guid id) - { - return PerformGet(id); - } + /// + /// Gets an Entity by alias + /// + /// + /// + public TEntity? Get(string alias) => PerformGet(alias); - /// - /// Gets all entities of the specified type - /// - /// - /// - /// - /// Ensure explicit implementation, we don't want to have any accidental calls to this since it is essentially the same signature as the main GetAll when there are no parameters - /// - IEnumerable IReadRepository.GetMany(params Guid[]? ids) + public string GetUniqueAlias(string alias) + { + // alias is unique across ALL content types! + var aliasColumn = SqlSyntax.GetQuotedColumnName("alias"); + List? aliases = Database.Fetch(@"SELECT cmsContentType." + aliasColumn + @" FROM cmsContentType +INNER JOIN umbracoNode ON cmsContentType.nodeId = umbracoNode.id +WHERE cmsContentType." + aliasColumn + @" LIKE @pattern", + new {pattern = alias + "%", objectType = NodeObjectTypeId}); + var i = 1; + string test; + while (aliases.Contains(test = alias + i)) { - return PerformGetAll(ids) ?? Enumerable.Empty(); + i++; } - /// - /// Boolean indicating whether an Entity with the specified Id exists - /// - /// - /// - public bool Exists(Guid id) - { - return PerformExists(id); - } + return test; + } - public string GetUniqueAlias(string alias) - { - // alias is unique across ALL content types! - var aliasColumn = SqlSyntax.GetQuotedColumnName("alias"); - var aliases = Database.Fetch(@"SELECT cmsContentType." + aliasColumn + @" FROM cmsContentType -INNER JOIN umbracoNode ON cmsContentType.nodeId = umbracoNode.id -WHERE cmsContentType." + aliasColumn + @" LIKE @pattern", - new {pattern = alias + "%", objectType = NodeObjectTypeId}); - var i = 1; - string test; - while (aliases.Contains(test = alias + i)) i++; - return test; - } + /// + public bool HasContainerInPath(string contentPath) + { + var ids = contentPath.Split(Constants.CharArrays.Comma) + .Select(s => int.Parse(s, CultureInfo.InvariantCulture)).ToArray(); + return HasContainerInPath(ids); + } - /// - public bool HasContainerInPath(string contentPath) - { - var ids = contentPath.Split(Constants.CharArrays.Comma) - .Select(s => int.Parse(s, CultureInfo.InvariantCulture)).ToArray(); - return HasContainerInPath(ids); - } + /// + public bool HasContainerInPath(params int[] ids) + { + var sql = new Sql($@"SELECT COUNT(*) FROM cmsContentType +INNER JOIN {Constants.DatabaseSchema.Tables.Content} ON cmsContentType.nodeId={Constants.DatabaseSchema.Tables.Content}.contentTypeId +WHERE {Constants.DatabaseSchema.Tables.Content}.nodeId IN (@ids) AND cmsContentType.isContainer=@isContainer", + new {ids, isContainer = true}); + return Database.ExecuteScalar(sql) > 0; + } + + /// + /// Returns true or false depending on whether content nodes have been created based on the provided content type id. + /// + public bool HasContentNodes(int id) + { + var sql = new Sql( + $"SELECT CASE WHEN EXISTS (SELECT * FROM {Constants.DatabaseSchema.Tables.Content} WHERE contentTypeId = @id) THEN 1 ELSE 0 END", + new {id}); + return Database.ExecuteScalar(sql) == 1; + } - /// - public bool HasContainerInPath(params int[] ids) + protected override IEnumerable GetDeleteClauses() + { + // in theory, services should have ensured that content items of the given content type + // have been deleted and therefore PropertyData has been cleared, so PropertyData + // is included here just to be 100% sure since it has a FK on cmsPropertyType. + + var list = new List { - var sql = new Sql($@"SELECT COUNT(*) FROM cmsContentType -INNER JOIN {Cms.Core.Constants.DatabaseSchema.Tables.Content} ON cmsContentType.nodeId={Cms.Core.Constants.DatabaseSchema.Tables.Content}.contentTypeId -WHERE {Cms.Core.Constants.DatabaseSchema.Tables.Content}.nodeId IN (@ids) AND cmsContentType.isContainer=@isContainer", - new {ids, isContainer = true}); - return Database.ExecuteScalar(sql) > 0; - } + "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @id", + "DELETE FROM umbracoUserGroup2Node WHERE nodeId = @id", + "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @id", + "DELETE FROM cmsTagRelationship WHERE nodeId = @id", + "DELETE FROM cmsContentTypeAllowedContentType WHERE Id = @id", + "DELETE FROM cmsContentTypeAllowedContentType WHERE AllowedId = @id", + "DELETE FROM cmsContentType2ContentType WHERE parentContentTypeId = @id", + "DELETE FROM cmsContentType2ContentType WHERE childContentTypeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyData + + " WHERE propertyTypeId IN (SELECT id FROM cmsPropertyType WHERE contentTypeId = @id)", + "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyType + + " WHERE contentTypeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyTypeGroup + + " WHERE contenttypeNodeId = @id" + }; + return list; + } + + private class NameCompareDto + { + public int NodeId { get; set; } + public int CurrentVersion { get; set; } + public int LanguageId { get; set; } + public string? CurrentName { get; set; } + public string? PublishedName { get; set; } + public int? PublishedVersion { get; set; } + public int Id { get; set; } // the Id of the DocumentCultureVariationDto + public bool Edited { get; set; } + } - /// - /// Returns true or false depending on whether content nodes have been created based on the provided content type id. - /// - public bool HasContentNodes(int id) + private class PropertyValueVersionDto + { + private decimal? _decimalValue; + public int VersionId { get; set; } + public int PropertyTypeId { get; set; } + public int? LanguageId { get; set; } + public string? Segment { get; set; } + public int? IntValue { get; set; } + + [Column("decimalValue")] + public decimal? DecimalValue { - var sql = new Sql( - $"SELECT CASE WHEN EXISTS (SELECT * FROM {Cms.Core.Constants.DatabaseSchema.Tables.Content} WHERE contentTypeId = @id) THEN 1 ELSE 0 END", - new {id}); - return Database.ExecuteScalar(sql) == 1; + get => _decimalValue; + set => _decimalValue = value?.Normalize(); } - protected override IEnumerable GetDeleteClauses() - { - // in theory, services should have ensured that content items of the given content type - // have been deleted and therefore PropertyData has been cleared, so PropertyData - // is included here just to be 100% sure since it has a FK on cmsPropertyType. + public DateTime? DateValue { get; set; } + public string? VarcharValue { get; set; } + public string? TextValue { get; set; } - var list = new List - { - "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @id", - "DELETE FROM umbracoUserGroup2Node WHERE nodeId = @id", - "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @id", - "DELETE FROM cmsTagRelationship WHERE nodeId = @id", - "DELETE FROM cmsContentTypeAllowedContentType WHERE Id = @id", - "DELETE FROM cmsContentTypeAllowedContentType WHERE AllowedId = @id", - "DELETE FROM cmsContentType2ContentType WHERE parentContentTypeId = @id", - "DELETE FROM cmsContentType2ContentType WHERE childContentTypeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.PropertyData + - " WHERE propertyTypeId IN (SELECT id FROM cmsPropertyType WHERE contentTypeId = @id)", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.PropertyType + - " WHERE contentTypeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup + - " WHERE contenttypeNodeId = @id" - }; - return list; - } + public int NodeId { get; set; } + public bool Current { get; set; } + public bool Published { get; set; } + + public byte Variations { get; set; } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs index 6ca327dfab21..6d461f057fe8 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs @@ -1,10 +1,6 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Globalization; -using System.IO; using System.IO.Compression; -using System.Linq; using System.Xml.Linq; using Microsoft.Extensions.Options; using NPoco; @@ -19,739 +15,737 @@ using Umbraco.Extensions; using File = System.IO.File; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +public class CreatedPackageSchemaRepository : ICreatedPackagesRepository { - /// - public class CreatedPackageSchemaRepository : ICreatedPackagesRepository + private readonly IContentService _contentService; + private readonly IContentTypeService _contentTypeService; + private readonly string _createdPackagesFolderPath; + private readonly IDataTypeService _dataTypeService; + private readonly IFileService _fileService; + private readonly FileSystems _fileSystems; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly ILocalizationService _localizationService; + private readonly IMacroService _macroService; + private readonly MediaFileManager _mediaFileManager; + private readonly IMediaService _mediaService; + private readonly IMediaTypeService _mediaTypeService; + private readonly IEntityXmlSerializer _serializer; + private readonly string _tempFolderPath; + private readonly IUmbracoDatabase? _umbracoDatabase; + private readonly PackageDefinitionXmlParser _xmlParser; + + /// + /// Initializes a new instance of the class. + /// + public CreatedPackageSchemaRepository( + IUmbracoDatabaseFactory umbracoDatabaseFactory, + IHostingEnvironment hostingEnvironment, + IOptions globalSettings, + FileSystems fileSystems, + IEntityXmlSerializer serializer, + IDataTypeService dataTypeService, + ILocalizationService localizationService, + IFileService fileService, + IMediaService mediaService, + IMediaTypeService mediaTypeService, + IContentService contentService, + MediaFileManager mediaFileManager, + IMacroService macroService, + IContentTypeService contentTypeService, + string? mediaFolderPath = null, + string? tempFolderPath = null) { - private readonly PackageDefinitionXmlParser _xmlParser; - private readonly IUmbracoDatabase? _umbracoDatabase; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly FileSystems _fileSystems; - private readonly IEntityXmlSerializer _serializer; - private readonly IDataTypeService _dataTypeService; - private readonly ILocalizationService _localizationService; - private readonly IFileService _fileService; - private readonly IMediaService _mediaService; - private readonly IMediaTypeService _mediaTypeService; - private readonly IContentService _contentService; - private readonly MediaFileManager _mediaFileManager; - private readonly IMacroService _macroService; - private readonly IContentTypeService _contentTypeService; - private readonly string _tempFolderPath; - private readonly string _createdPackagesFolderPath; - - /// - /// Initializes a new instance of the class. - /// - public CreatedPackageSchemaRepository( - IUmbracoDatabaseFactory umbracoDatabaseFactory, - IHostingEnvironment hostingEnvironment, - IOptions globalSettings, - FileSystems fileSystems, - IEntityXmlSerializer serializer, - IDataTypeService dataTypeService, - ILocalizationService localizationService, - IFileService fileService, - IMediaService mediaService, - IMediaTypeService mediaTypeService, - IContentService contentService, - MediaFileManager mediaFileManager, - IMacroService macroService, - IContentTypeService contentTypeService, - string? mediaFolderPath = null, - string? tempFolderPath = null) - { - _umbracoDatabase = umbracoDatabaseFactory.CreateDatabase(); - _hostingEnvironment = hostingEnvironment; - _fileSystems = fileSystems; - _serializer = serializer; - _dataTypeService = dataTypeService; - _localizationService = localizationService; - _fileService = fileService; - _mediaService = mediaService; - _mediaTypeService = mediaTypeService; - _contentService = contentService; - _mediaFileManager = mediaFileManager; - _macroService = macroService; - _contentTypeService = contentTypeService; - _xmlParser = new PackageDefinitionXmlParser(); - _createdPackagesFolderPath = mediaFolderPath ?? Constants.SystemDirectories.CreatedPackages; - _tempFolderPath = tempFolderPath ?? Constants.SystemDirectories.TempData + "/PackageFiles"; - } - - public IEnumerable GetAll() - { - Sql query = new Sql(_umbracoDatabase!.SqlContext) - .Select() - .From() - .OrderBy(x => x.Id); - - var packageDefinitions = new List(); - - List xmlSchemas = _umbracoDatabase.Fetch(query); - foreach (CreatedPackageSchemaDto packageSchema in xmlSchemas) - { - var packageDefinition = _xmlParser.ToPackageDefinition(XElement.Parse(packageSchema.Value)); - if (packageDefinition is not null) - { - packageDefinition.Id = packageSchema.Id; - packageDefinition.Name = packageSchema.Name; - packageDefinition.PackageId = packageSchema.PackageId; - packageDefinitions.Add(packageDefinition); - } - } - - return packageDefinitions; - } + _umbracoDatabase = umbracoDatabaseFactory.CreateDatabase(); + _hostingEnvironment = hostingEnvironment; + _fileSystems = fileSystems; + _serializer = serializer; + _dataTypeService = dataTypeService; + _localizationService = localizationService; + _fileService = fileService; + _mediaService = mediaService; + _mediaTypeService = mediaTypeService; + _contentService = contentService; + _mediaFileManager = mediaFileManager; + _macroService = macroService; + _contentTypeService = contentTypeService; + _xmlParser = new PackageDefinitionXmlParser(); + _createdPackagesFolderPath = mediaFolderPath ?? Constants.SystemDirectories.CreatedPackages; + _tempFolderPath = tempFolderPath ?? Constants.SystemDirectories.TempData + "/PackageFiles"; + } - public PackageDefinition? GetById(int id) - { - Sql query = new Sql(_umbracoDatabase!.SqlContext) - .Select() - .From() - .Where(x => x.Id == id); - List schemaDtos = _umbracoDatabase.Fetch(query); + public IEnumerable GetAll() + { + Sql query = new Sql(_umbracoDatabase!.SqlContext) + .Select() + .From() + .OrderBy(x => x.Id); - if (schemaDtos.IsCollectionEmpty()) - { - return null; - } + var packageDefinitions = new List(); - var packageSchema = schemaDtos.First(); + List xmlSchemas = _umbracoDatabase.Fetch(query); + foreach (CreatedPackageSchemaDto packageSchema in xmlSchemas) + { var packageDefinition = _xmlParser.ToPackageDefinition(XElement.Parse(packageSchema.Value)); if (packageDefinition is not null) { packageDefinition.Id = packageSchema.Id; packageDefinition.Name = packageSchema.Name; packageDefinition.PackageId = packageSchema.PackageId; + packageDefinitions.Add(packageDefinition); } + } + + return packageDefinitions; + } + + public PackageDefinition? GetById(int id) + { + Sql query = new Sql(_umbracoDatabase!.SqlContext) + .Select() + .From() + .Where(x => x.Id == id); + List schemaDtos = _umbracoDatabase.Fetch(query); - return packageDefinition; + if (schemaDtos.IsCollectionEmpty()) + { + return null; } - public void Delete(int id) + CreatedPackageSchemaDto packageSchema = schemaDtos.First(); + var packageDefinition = _xmlParser.ToPackageDefinition(XElement.Parse(packageSchema.Value)); + if (packageDefinition is not null) { - // Delete package snapshot - var packageDef = GetById(id); - if (File.Exists(packageDef?.PackagePath)) - { - File.Delete(packageDef.PackagePath); - } + packageDefinition.Id = packageSchema.Id; + packageDefinition.Name = packageSchema.Name; + packageDefinition.PackageId = packageSchema.PackageId; + } - Sql query = new Sql(_umbracoDatabase!.SqlContext) - .Delete() - .Where(x => x.Id == id); + return packageDefinition; + } - _umbracoDatabase.Execute(query); + public void Delete(int id) + { + // Delete package snapshot + PackageDefinition? packageDef = GetById(id); + if (File.Exists(packageDef?.PackagePath)) + { + File.Delete(packageDef.PackagePath); } - public bool SavePackage(PackageDefinition definition) - { - if (definition == null) - { - throw new NullReferenceException("PackageDefinition cannot be null when saving"); - } + Sql query = new Sql(_umbracoDatabase!.SqlContext) + .Delete() + .Where(x => x.Id == id); - if (definition.Name == null || string.IsNullOrEmpty(definition.Name) || definition.PackagePath == null) - { - return false; - } + _umbracoDatabase.Execute(query); + } - // Ensure it's valid - ValidatePackage(definition); + public bool SavePackage(PackageDefinition definition) + { + if (definition == null) + { + throw new NullReferenceException("PackageDefinition cannot be null when saving"); + } + if (definition.Name == null || string.IsNullOrEmpty(definition.Name) || definition.PackagePath == null) + { + return false; + } + + // Ensure it's valid + ValidatePackage(definition); - if (definition.Id == default) - { - // Create dto from definition - var dto = new CreatedPackageSchemaDto() - { - Name = definition.Name, - Value = _xmlParser.ToXml(definition).ToString(), - UpdateDate = DateTime.Now, - PackageId = Guid.NewGuid() - }; - - // Set the ids, we have to save in database first to get the Id - _umbracoDatabase!.Insert(dto); - definition.Id = dto.Id; - } - // Save snapshot locally, we do this to the updated packagePath - ExportPackage(definition); + if (definition.Id == default) + { // Create dto from definition - var updatedDto = new CreatedPackageSchemaDto() + var dto = new CreatedPackageSchemaDto { Name = definition.Name, Value = _xmlParser.ToXml(definition).ToString(), - Id = definition.Id, - PackageId = definition.PackageId, - UpdateDate = DateTime.Now + UpdateDate = DateTime.Now, + PackageId = Guid.NewGuid() }; - _umbracoDatabase?.Update(updatedDto); - return true; + // Set the ids, we have to save in database first to get the Id + _umbracoDatabase!.Insert(dto); + definition.Id = dto.Id; } - public string ExportPackage(PackageDefinition definition) + // Save snapshot locally, we do this to the updated packagePath + ExportPackage(definition); + // Create dto from definition + var updatedDto = new CreatedPackageSchemaDto { - // Ensure it's valid - ValidatePackage(definition); + Name = definition.Name, + Value = _xmlParser.ToXml(definition).ToString(), + Id = definition.Id, + PackageId = definition.PackageId, + UpdateDate = DateTime.Now + }; + _umbracoDatabase?.Update(updatedDto); + + return true; + } - // Create a folder for building this package - var temporaryPath = _hostingEnvironment.MapPathContentRoot(Path.Combine(_tempFolderPath, Guid.NewGuid().ToString())); - Directory.CreateDirectory(temporaryPath); + public string ExportPackage(PackageDefinition definition) + { + // Ensure it's valid + ValidatePackage(definition); - try - { - // Init package file - XDocument compiledPackageXml = CreateCompiledPackageXml(out XElement root); - - // Info section - root.Add(GetPackageInfoXml(definition)); - - PackageDocumentsAndTags(definition, root); - PackageDocumentTypes(definition, root); - PackageMediaTypes(definition, root); - PackageTemplates(definition, root); - PackageStylesheets(definition, root); - PackageStaticFiles(definition.Scripts, root, "Scripts", "Script", _fileSystems.ScriptsFileSystem!); - PackageStaticFiles(definition.PartialViews, root, "PartialViews", "View", _fileSystems.PartialViewsFileSystem!); - PackageMacros(definition, root); - PackageDictionaryItems(definition, root); - PackageLanguages(definition, root); - PackageDataTypes(definition, root); - Dictionary mediaFiles = PackageMedia(definition, root); - - string fileName; - string tempPackagePath; - if (mediaFiles.Count > 0) + // Create a folder for building this package + var temporaryPath = + _hostingEnvironment.MapPathContentRoot(Path.Combine(_tempFolderPath, Guid.NewGuid().ToString())); + Directory.CreateDirectory(temporaryPath); + + try + { + // Init package file + XDocument compiledPackageXml = CreateCompiledPackageXml(out XElement root); + + // Info section + root.Add(GetPackageInfoXml(definition)); + + PackageDocumentsAndTags(definition, root); + PackageDocumentTypes(definition, root); + PackageMediaTypes(definition, root); + PackageTemplates(definition, root); + PackageStylesheets(definition, root); + PackageStaticFiles(definition.Scripts, root, "Scripts", "Script", _fileSystems.ScriptsFileSystem!); + PackageStaticFiles(definition.PartialViews, root, "PartialViews", "View", + _fileSystems.PartialViewsFileSystem!); + PackageMacros(definition, root); + PackageDictionaryItems(definition, root); + PackageLanguages(definition, root); + PackageDataTypes(definition, root); + Dictionary mediaFiles = PackageMedia(definition, root); + + string fileName; + string tempPackagePath; + if (mediaFiles.Count > 0) + { + fileName = "package.zip"; + tempPackagePath = Path.Combine(temporaryPath, fileName); + using (FileStream fileStream = File.OpenWrite(tempPackagePath)) + using (var archive = new ZipArchive(fileStream, ZipArchiveMode.Create, true)) { - fileName = "package.zip"; - tempPackagePath = Path.Combine(temporaryPath, fileName); - using (FileStream fileStream = File.OpenWrite(tempPackagePath)) - using (var archive = new ZipArchive(fileStream, ZipArchiveMode.Create, true)) + ZipArchiveEntry packageXmlEntry = archive.CreateEntry("package.xml"); + using (Stream entryStream = packageXmlEntry.Open()) { - ZipArchiveEntry packageXmlEntry = archive.CreateEntry("package.xml"); - using (Stream entryStream = packageXmlEntry.Open()) - { - compiledPackageXml.Save(entryStream); - } + compiledPackageXml.Save(entryStream); + } - foreach (KeyValuePair mediaFile in mediaFiles) + foreach (KeyValuePair mediaFile in mediaFiles) + { + var entryPath = $"media{mediaFile.Key.EnsureStartsWith('/')}"; + ZipArchiveEntry mediaEntry = archive.CreateEntry(entryPath); + using (Stream entryStream = mediaEntry.Open()) + using (mediaFile.Value) { - var entryPath = $"media{mediaFile.Key.EnsureStartsWith('/')}"; - ZipArchiveEntry mediaEntry = archive.CreateEntry(entryPath); - using (Stream entryStream = mediaEntry.Open()) - using (mediaFile.Value) - { - mediaFile.Value.Seek(0, SeekOrigin.Begin); - mediaFile.Value.CopyTo(entryStream); - } + mediaFile.Value.Seek(0, SeekOrigin.Begin); + mediaFile.Value.CopyTo(entryStream); } } } - else - { - fileName = "package.xml"; - tempPackagePath = Path.Combine(temporaryPath, fileName); + } + else + { + fileName = "package.xml"; + tempPackagePath = Path.Combine(temporaryPath, fileName); - using (FileStream fileStream = File.OpenWrite(tempPackagePath)) - { - compiledPackageXml.Save(fileStream); - } + using (FileStream fileStream = File.OpenWrite(tempPackagePath)) + { + compiledPackageXml.Save(fileStream); } + } - var directoryName = _hostingEnvironment.MapPathContentRoot(Path.Combine(_createdPackagesFolderPath, definition.Name.Replace(' ', '_'))); - Directory.CreateDirectory(directoryName); + var directoryName = + _hostingEnvironment.MapPathContentRoot(Path.Combine(_createdPackagesFolderPath, + definition.Name.Replace(' ', '_'))); + Directory.CreateDirectory(directoryName); - var finalPackagePath = Path.Combine(directoryName, fileName); + var finalPackagePath = Path.Combine(directoryName, fileName); - // Clean existing files - foreach (var packagePath in new[] - { - definition.PackagePath, - finalPackagePath - }) + // Clean existing files + foreach (var packagePath in new[] {definition.PackagePath, finalPackagePath}) + { + if (File.Exists(packagePath)) { - if (File.Exists(packagePath)) - { - File.Delete(packagePath); - } + File.Delete(packagePath); } + } - // Move to final package path - File.Move(tempPackagePath, finalPackagePath); + // Move to final package path + File.Move(tempPackagePath, finalPackagePath); - definition.PackagePath = finalPackagePath; + definition.PackagePath = finalPackagePath; - return finalPackagePath; - } - finally - { - // Clean up - Directory.Delete(temporaryPath, true); - } + return finalPackagePath; + } + finally + { + // Clean up + Directory.Delete(temporaryPath, true); } + } + + private XDocument CreateCompiledPackageXml(out XElement root) + { + root = new XElement("umbPackage"); + var compiledPackageXml = new XDocument(root); + return compiledPackageXml; + } - private XDocument CreateCompiledPackageXml(out XElement root) + private void ValidatePackage(PackageDefinition definition) + { + // Ensure it's valid + var context = new ValidationContext(definition, null, null); + var results = new List(); + var isValid = Validator.TryValidateObject(definition, context, results); + if (!isValid) { - root = new XElement("umbPackage"); - var compiledPackageXml = new XDocument(root); - return compiledPackageXml; + throw new InvalidOperationException("Validation failed, there is invalid data on the model: " + + string.Join(", ", results.Select(x => x.ErrorMessage))); } + } - private void ValidatePackage(PackageDefinition definition) + private void PackageDataTypes(PackageDefinition definition, XContainer root) + { + var dataTypes = new XElement("DataTypes"); + foreach (var dtId in definition.DataTypes) { - // Ensure it's valid - var context = new ValidationContext(definition, serviceProvider: null, items: null); - var results = new List(); - var isValid = Validator.TryValidateObject(definition, context, results); - if (!isValid) + if (!int.TryParse(dtId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) { - throw new InvalidOperationException("Validation failed, there is invalid data on the model: " + - string.Join(", ", results.Select(x => x.ErrorMessage))); + continue; } + + IDataType? dataType = _dataTypeService.GetDataType(outInt); + if (dataType == null) + { + continue; + } + + dataTypes.Add(_serializer.Serialize(dataType)); } - private void PackageDataTypes(PackageDefinition definition, XContainer root) + root.Add(dataTypes); + } + + private void PackageLanguages(PackageDefinition definition, XContainer root) + { + var languages = new XElement("Languages"); + foreach (var langId in definition.Languages) { - var dataTypes = new XElement("DataTypes"); - foreach (var dtId in definition.DataTypes) + if (!int.TryParse(langId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) { - if (!int.TryParse(dtId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) - { - continue; - } - - IDataType? dataType = _dataTypeService.GetDataType(outInt); - if (dataType == null) - { - continue; - } + continue; + } - dataTypes.Add(_serializer.Serialize(dataType)); + ILanguage? lang = _localizationService.GetLanguageById(outInt); + if (lang == null) + { + continue; } - root.Add(dataTypes); + languages.Add(_serializer.Serialize(lang)); } - private void PackageLanguages(PackageDefinition definition, XContainer root) + root.Add(languages); + } + + private void PackageDictionaryItems(PackageDefinition definition, XContainer root) + { + var rootDictionaryItems = new XElement("DictionaryItems"); + var items = new Dictionary(); + + foreach (var dictionaryId in definition.DictionaryItems) { - var languages = new XElement("Languages"); - foreach (var langId in definition.Languages) + if (!int.TryParse(dictionaryId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) { - if (!int.TryParse(langId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) - { - continue; - } + continue; + } - ILanguage? lang = _localizationService.GetLanguageById(outInt); - if (lang == null) - { - continue; - } + IDictionaryItem? di = _localizationService.GetDictionaryItemById(outInt); - languages.Add(_serializer.Serialize(lang)); + if (di == null) + { + continue; } - root.Add(languages); + items[di.Key] = (di, _serializer.Serialize(di, false)); } - private void PackageDictionaryItems(PackageDefinition definition, XContainer root) + // organize them in hierarchy ... + var itemCount = items.Count; + var processed = new Dictionary(); + while (processed.Count < itemCount) { - var rootDictionaryItems = new XElement("DictionaryItems"); - var items = new Dictionary(); - - foreach (var dictionaryId in definition.DictionaryItems) + foreach (Guid key in items.Keys.ToList()) { - if (!int.TryParse(dictionaryId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) - { - continue; - } + (IDictionaryItem dictionaryItem, XElement serializedDictionaryValue) = items[key]; - IDictionaryItem? di = _localizationService.GetDictionaryItemById(outInt); - - if (di == null) + if (!dictionaryItem.ParentId.HasValue) { - continue; + // if it has no parent, its definitely just at the root + AppendDictionaryElement(rootDictionaryItems, items, processed, key, serializedDictionaryValue); } - - items[di.Key] = (di, _serializer.Serialize(di, false)); - } - - // organize them in hierarchy ... - var itemCount = items.Count; - var processed = new Dictionary(); - while (processed.Count < itemCount) - { - foreach (Guid key in items.Keys.ToList()) + else { - (IDictionaryItem dictionaryItem, XElement serializedDictionaryValue) = items[key]; - - if (!dictionaryItem.ParentId.HasValue) + if (processed.ContainsKey(dictionaryItem.ParentId.Value)) { - // if it has no parent, its definitely just at the root - AppendDictionaryElement(rootDictionaryItems, items, processed, key, serializedDictionaryValue); + // we've processed this parent element already so we can just append this xml child to it + AppendDictionaryElement(processed[dictionaryItem.ParentId.Value], items, processed, key, + serializedDictionaryValue); + } + else if (items.ContainsKey(dictionaryItem.ParentId.Value)) + { + // we know the parent exists in the dictionary but + // we haven't processed it yet so we'll leave it for the next loop } else { - if (processed.ContainsKey(dictionaryItem.ParentId.Value)) - { - // we've processed this parent element already so we can just append this xml child to it - AppendDictionaryElement(processed[dictionaryItem.ParentId.Value], items, processed, key, - serializedDictionaryValue); - } - else if (items.ContainsKey(dictionaryItem.ParentId.Value)) - { - // we know the parent exists in the dictionary but - // we haven't processed it yet so we'll leave it for the next loop - continue; - } - else - { - // in this case, the parent of this item doesn't exist in our collection, we have no - // choice but to add it to the root. - AppendDictionaryElement(rootDictionaryItems, items, processed, key, - serializedDictionaryValue); - } + // in this case, the parent of this item doesn't exist in our collection, we have no + // choice but to add it to the root. + AppendDictionaryElement(rootDictionaryItems, items, processed, key, + serializedDictionaryValue); } } } + } - root.Add(rootDictionaryItems); + root.Add(rootDictionaryItems); - static void AppendDictionaryElement(XElement rootDictionaryItems, - Dictionary items, - Dictionary processed, Guid key, XElement serializedDictionaryValue) - { - // track it - processed.Add(key, serializedDictionaryValue); + static void AppendDictionaryElement(XElement rootDictionaryItems, + Dictionary items, + Dictionary processed, Guid key, XElement serializedDictionaryValue) + { + // track it + processed.Add(key, serializedDictionaryValue); - // append it - rootDictionaryItems.Add(serializedDictionaryValue); + // append it + rootDictionaryItems.Add(serializedDictionaryValue); - // remove it so its not re-processed - items.Remove(key); - } + // remove it so its not re-processed + items.Remove(key); } + } - private void PackageMacros(PackageDefinition definition, XContainer root) + private void PackageMacros(PackageDefinition definition, XContainer root) + { + var packagedMacros = new List(); + var macros = new XElement("Macros"); + foreach (var macroId in definition.Macros) { - var packagedMacros = new List(); - var macros = new XElement("Macros"); - foreach (var macroId in definition.Macros) + if (!int.TryParse(macroId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) { - if (!int.TryParse(macroId, NumberStyles.Integer, CultureInfo.InvariantCulture, out int outInt)) - { - continue; - } - - XElement? macroXml = GetMacroXml(outInt, out IMacro? macro); - if (macroXml is null) - { - continue; - } - - macros.Add(macroXml); - packagedMacros.Add(macro!); + continue; } - root.Add(macros); + XElement? macroXml = GetMacroXml(outInt, out IMacro? macro); + if (macroXml is null) + { + continue; + } - // Get the partial views for macros and package those (exclude views outside of the default directory, e.g. App_Plugins\*\Views) - IEnumerable views = packagedMacros - .Where(x => x.MacroSource.StartsWith(Constants.SystemDirectories.MacroPartials)) - .Select(x => - x.MacroSource.Substring(Constants.SystemDirectories.MacroPartials.Length).Replace('/', '\\')); - PackageStaticFiles(views, root, "MacroPartialViews", "View", _fileSystems.MacroPartialsFileSystem!); + macros.Add(macroXml); + packagedMacros.Add(macro!); } - private void PackageStylesheets(PackageDefinition definition, XContainer root) + root.Add(macros); + + // Get the partial views for macros and package those (exclude views outside of the default directory, e.g. App_Plugins\*\Views) + IEnumerable views = packagedMacros + .Where(x => x.MacroSource.StartsWith(Constants.SystemDirectories.MacroPartials)) + .Select(x => + x.MacroSource.Substring(Constants.SystemDirectories.MacroPartials.Length).Replace('/', '\\')); + PackageStaticFiles(views, root, "MacroPartialViews", "View", _fileSystems.MacroPartialsFileSystem!); + } + + private void PackageStylesheets(PackageDefinition definition, XContainer root) + { + var stylesheetsXml = new XElement("Stylesheets"); + foreach (var stylesheet in definition.Stylesheets) { - var stylesheetsXml = new XElement("Stylesheets"); - foreach (var stylesheet in definition.Stylesheets) + if (stylesheet.IsNullOrWhiteSpace()) { - if (stylesheet.IsNullOrWhiteSpace()) - { - continue; - } - - XElement? xml = GetStylesheetXml(stylesheet, true); - if (xml != null) - { - stylesheetsXml.Add(xml); - } + continue; } - root.Add(stylesheetsXml); + XElement? xml = GetStylesheetXml(stylesheet, true); + if (xml != null) + { + stylesheetsXml.Add(xml); + } } - private void PackageStaticFiles( - IEnumerable filePaths, - XContainer root, - string containerName, - string elementName, - IFileSystem fileSystem) + root.Add(stylesheetsXml); + } + + private void PackageStaticFiles( + IEnumerable filePaths, + XContainer root, + string containerName, + string elementName, + IFileSystem fileSystem) + { + var scriptsXml = new XElement(containerName); + foreach (var file in filePaths) { - var scriptsXml = new XElement(containerName); - foreach (var file in filePaths) + if (file.IsNullOrWhiteSpace()) { - if (file.IsNullOrWhiteSpace()) - { - continue; - } + continue; + } - if (!fileSystem.FileExists(file)) - { - throw new InvalidOperationException("No file found with path " + file); - } + if (!fileSystem.FileExists(file)) + { + throw new InvalidOperationException("No file found with path " + file); + } - using Stream? stream = fileSystem.OpenFile(file); - if (stream is not null) + using Stream? stream = fileSystem.OpenFile(file); + if (stream is not null) + { + using (var reader = new StreamReader(stream)) { - using (var reader = new StreamReader(stream)) - { - var fileContents = reader.ReadToEnd(); - scriptsXml.Add( - new XElement( - elementName, - new XAttribute("path", file), - new XCData(fileContents))); - } + var fileContents = reader.ReadToEnd(); + scriptsXml.Add( + new XElement( + elementName, + new XAttribute("path", file), + new XCData(fileContents))); } } - - root.Add(scriptsXml); } - private void PackageTemplates(PackageDefinition definition, XContainer root) + root.Add(scriptsXml); + } + + private void PackageTemplates(PackageDefinition definition, XContainer root) + { + var templatesXml = new XElement("Templates"); + foreach (var templateId in definition.Templates) { - var templatesXml = new XElement("Templates"); - foreach (var templateId in definition.Templates) + if (!int.TryParse(templateId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) { - if (!int.TryParse(templateId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) - { - continue; - } - - ITemplate? template = _fileService.GetTemplate(outInt); - if (template == null) - { - continue; - } + continue; + } - templatesXml.Add(_serializer.Serialize(template)); + ITemplate? template = _fileService.GetTemplate(outInt); + if (template == null) + { + continue; } - root.Add(templatesXml); + templatesXml.Add(_serializer.Serialize(template)); } - private void PackageDocumentTypes(PackageDefinition definition, XContainer root) + root.Add(templatesXml); + } + + private void PackageDocumentTypes(PackageDefinition definition, XContainer root) + { + var contentTypes = new HashSet(); + var docTypesXml = new XElement("DocumentTypes"); + foreach (var dtId in definition.DocumentTypes) { - var contentTypes = new HashSet(); - var docTypesXml = new XElement("DocumentTypes"); - foreach (var dtId in definition.DocumentTypes) + if (!int.TryParse(dtId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) { - if (!int.TryParse(dtId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) - { - continue; - } - - IContentType? contentType = _contentTypeService.Get(outInt); - if (contentType == null) - { - continue; - } - - AddDocumentType(contentType, contentTypes); + continue; } - foreach (IContentType contentType in contentTypes) + IContentType? contentType = _contentTypeService.Get(outInt); + if (contentType == null) { - docTypesXml.Add(_serializer.Serialize(contentType)); + continue; } - root.Add(docTypesXml); + AddDocumentType(contentType, contentTypes); } - private void PackageMediaTypes(PackageDefinition definition, XContainer root) + foreach (IContentType contentType in contentTypes) { - var mediaTypes = new HashSet(); - var mediaTypesXml = new XElement("MediaTypes"); - foreach (var mediaTypeId in definition.MediaTypes) - { - if (!int.TryParse(mediaTypeId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) - { - continue; - } + docTypesXml.Add(_serializer.Serialize(contentType)); + } - IMediaType? mediaType = _mediaTypeService.Get(outInt); - if (mediaType == null) - { - continue; - } + root.Add(docTypesXml); + } - AddMediaType(mediaType, mediaTypes); + private void PackageMediaTypes(PackageDefinition definition, XContainer root) + { + var mediaTypes = new HashSet(); + var mediaTypesXml = new XElement("MediaTypes"); + foreach (var mediaTypeId in definition.MediaTypes) + { + if (!int.TryParse(mediaTypeId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) + { + continue; } - foreach (IMediaType mediaType in mediaTypes) + IMediaType? mediaType = _mediaTypeService.Get(outInt); + if (mediaType == null) { - mediaTypesXml.Add(_serializer.Serialize(mediaType)); + continue; } - root.Add(mediaTypesXml); + AddMediaType(mediaType, mediaTypes); } - private void PackageDocumentsAndTags(PackageDefinition definition, XContainer root) + foreach (IMediaType mediaType in mediaTypes) { - // Documents and tags - if (string.IsNullOrEmpty(definition.ContentNodeId) == false && int.TryParse(definition.ContentNodeId, - NumberStyles.Integer, CultureInfo.InvariantCulture, out var contentNodeId)) + mediaTypesXml.Add(_serializer.Serialize(mediaType)); + } + + root.Add(mediaTypesXml); + } + + private void PackageDocumentsAndTags(PackageDefinition definition, XContainer root) + { + // Documents and tags + if (string.IsNullOrEmpty(definition.ContentNodeId) == false && int.TryParse(definition.ContentNodeId, + NumberStyles.Integer, CultureInfo.InvariantCulture, out var contentNodeId)) + { + if (contentNodeId > 0) { - if (contentNodeId > 0) + // load content from umbraco. + IContent? content = _contentService.GetById(contentNodeId); + if (content != null) { - // load content from umbraco. - IContent? content = _contentService.GetById(contentNodeId); - if (content != null) - { - var contentXml = definition.ContentLoadChildNodes - ? content.ToDeepXml(_serializer) - : content.ToXml(_serializer); + XElement contentXml = definition.ContentLoadChildNodes + ? content.ToDeepXml(_serializer) + : content.ToXml(_serializer); - // Create the Documents/DocumentSet node + // Create the Documents/DocumentSet node - root.Add( + root.Add( + new XElement( + "Documents", new XElement( - "Documents", - new XElement( - "DocumentSet", - new XAttribute("importMode", "root"), - contentXml))); - } + "DocumentSet", + new XAttribute("importMode", "root"), + contentXml))); } } } + } - private Dictionary PackageMedia(PackageDefinition definition, XElement root) - { - var mediaStreams = new Dictionary(); + private Dictionary PackageMedia(PackageDefinition definition, XElement root) + { + var mediaStreams = new Dictionary(); - // callback that occurs on each serialized media item - void OnSerializedMedia(IMedia media, XElement xmlMedia) + // callback that occurs on each serialized media item + void OnSerializedMedia(IMedia media, XElement xmlMedia) + { + // get the media file path and store that separately in the XML. + // the media file path is different from the URL and is specifically + // extracted using the property editor for this media file and the current media file system. + Stream? mediaStream = _mediaFileManager.GetFile(media, out var mediaFilePath); + if (mediaStream != null) { - // get the media file path and store that separately in the XML. - // the media file path is different from the URL and is specifically - // extracted using the property editor for this media file and the current media file system. - Stream? mediaStream = _mediaFileManager.GetFile(media, out var mediaFilePath); - if (mediaStream != null) - { - xmlMedia.Add(new XAttribute("mediaFilePath", mediaFilePath!)); + xmlMedia.Add(new XAttribute("mediaFilePath", mediaFilePath!)); - // add the stream to our outgoing stream - mediaStreams.Add(mediaFilePath!, mediaStream); - } + // add the stream to our outgoing stream + mediaStreams.Add(mediaFilePath!, mediaStream); } + } - IEnumerable medias = _mediaService.GetByIds(definition.MediaUdis); + IEnumerable medias = _mediaService.GetByIds(definition.MediaUdis); - var mediaXml = new XElement( - "MediaItems", - medias.Select(media => - { - XElement serializedMedia = _serializer.Serialize( - media, - definition.MediaLoadChildNodes, - OnSerializedMedia); + var mediaXml = new XElement( + "MediaItems", + medias.Select(media => + { + XElement serializedMedia = _serializer.Serialize( + media, + definition.MediaLoadChildNodes, + OnSerializedMedia); - return new XElement("MediaSet", serializedMedia); - })); + return new XElement("MediaSet", serializedMedia); + })); - root.Add(mediaXml); + root.Add(mediaXml); - return mediaStreams; - } + return mediaStreams; + } - /// - /// Gets a macros xml node - /// - private XElement? GetMacroXml(int macroId, out IMacro? macro) + /// + /// Gets a macros xml node + /// + private XElement? GetMacroXml(int macroId, out IMacro? macro) + { + macro = _macroService.GetById(macroId); + if (macro == null) { - macro = _macroService.GetById(macroId); - if (macro == null) - { - return null; - } - - XElement xml = _serializer.Serialize(macro); - return xml; + return null; } - /// - /// Converts a umbraco stylesheet to a package xml node - /// - /// The path of the stylesheet. - /// if set to true [include properties]. - private XElement? GetStylesheetXml(string path, bool includeProperties) - { - if (string.IsNullOrWhiteSpace(path)) - { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(path)); - } - - IStylesheet? stylesheet = _fileService.GetStylesheet(path); - if (stylesheet == null) - { - return null; - } + XElement xml = _serializer.Serialize(macro); + return xml; + } - return _serializer.Serialize(stylesheet, includeProperties); + /// + /// Converts a umbraco stylesheet to a package xml node + /// + /// The path of the stylesheet. + /// if set to true [include properties]. + private XElement? GetStylesheetXml(string path, bool includeProperties) + { + if (string.IsNullOrWhiteSpace(path)) + { + throw new ArgumentException("Value cannot be null or whitespace.", nameof(path)); } - private void AddDocumentType(IContentType dt, HashSet dtl) + IStylesheet? stylesheet = _fileService.GetStylesheet(path); + if (stylesheet == null) { - if (dt.ParentId > 0) - { - IContentType? parent = _contentTypeService.Get(dt.ParentId); - if (parent != null) - { - AddDocumentType(parent, dtl); - } - } + return null; + } + + return _serializer.Serialize(stylesheet, includeProperties); + } - if (!dtl.Contains(dt)) + private void AddDocumentType(IContentType dt, HashSet dtl) + { + if (dt.ParentId > 0) + { + IContentType? parent = _contentTypeService.Get(dt.ParentId); + if (parent != null) { - dtl.Add(dt); + AddDocumentType(parent, dtl); } } - private void AddMediaType(IMediaType mediaType, HashSet mediaTypes) + if (!dtl.Contains(dt)) { - if (mediaType.ParentId > 0) - { - IMediaType? parent = _mediaTypeService.Get(mediaType.ParentId); - if (parent != null) - { - AddMediaType(parent, mediaTypes); - } - } + dtl.Add(dt); + } + } - if (!mediaTypes.Contains(mediaType)) + private void AddMediaType(IMediaType mediaType, HashSet mediaTypes) + { + if (mediaType.ParentId > 0) + { + IMediaType? parent = _mediaTypeService.Get(mediaType.ParentId); + if (parent != null) { - mediaTypes.Add(mediaType); + AddMediaType(parent, mediaTypes); } } - private static XElement GetPackageInfoXml(PackageDefinition definition) + if (!mediaTypes.Contains(mediaType)) { - var info = new XElement("info"); - - // Package info - var package = new XElement("package"); - package.Add(new XElement("name", definition.Name)); - info.Add(package); - return info; + mediaTypes.Add(mediaType); } } + + private static XElement GetPackageInfoXml(PackageDefinition definition) + { + var info = new XElement("info"); + + // Package info + var package = new XElement("package"); + package.Add(new XElement("name", definition.Name)); + info.Add(package); + return info; + } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeContainerRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeContainerRepository.cs index f8fc9e14be0c..84c0fe093d38 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeContainerRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeContainerRepository.cs @@ -1,14 +1,16 @@ using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Infrastructure.Scoping; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class DataTypeContainerRepository : EntityContainerRepository, IDataTypeContainerRepository { - internal class DataTypeContainerRepository : EntityContainerRepository, IDataTypeContainerRepository + public DataTypeContainerRepository(IScopeAccessor scopeAccessor, AppCaches cache, + ILogger logger) + : base(scopeAccessor, cache, logger, Constants.ObjectTypes.DataTypeContainer) { - public DataTypeContainerRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger, Cms.Core.Constants.ObjectTypes.DataTypeContainer) - { } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeRepository.cs index 7ca3e8c3c3d4..85c8e28bb6ce 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeRepository.cs @@ -1,9 +1,5 @@ -using System; -using System.Collections.Generic; using System.Data; -using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -23,331 +19,326 @@ using Umbraco.Extensions; using static Umbraco.Cms.Core.Persistence.SqlExtensionsStatics; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents a repository for doing CRUD operations for +/// +internal class DataTypeRepository : EntityRepositoryBase, IDataTypeRepository { - /// - /// Represents a repository for doing CRUD operations for - /// - internal class DataTypeRepository : EntityRepositoryBase, IDataTypeRepository + private readonly ILogger _dataTypeLogger; + private readonly PropertyEditorCollection _editors; + private readonly IConfigurationEditorJsonSerializer _serializer; + + public DataTypeRepository( + IScopeAccessor scopeAccessor, + AppCaches cache, + PropertyEditorCollection editors, + ILogger logger, + ILoggerFactory loggerFactory, + IConfigurationEditorJsonSerializer serializer) + : base(scopeAccessor, cache, logger) { - private readonly PropertyEditorCollection _editors; - private readonly IConfigurationEditorJsonSerializer _serializer; - private readonly ILogger _dataTypeLogger; - - public DataTypeRepository( - IScopeAccessor scopeAccessor, - AppCaches cache, - PropertyEditorCollection editors, - ILogger logger, - ILoggerFactory loggerFactory, - IConfigurationEditorJsonSerializer serializer) - : base(scopeAccessor, cache, logger) - { - _editors = editors; - _serializer = serializer; - _dataTypeLogger = loggerFactory.CreateLogger(); - } - - #region Overrides of RepositoryBase - - protected override IDataType? PerformGet(int id) - { - return GetMany(id)?.FirstOrDefault(); - } + _editors = editors; + _serializer = serializer; + _dataTypeLogger = loggerFactory.CreateLogger(); + } - protected override IEnumerable PerformGetAll(params int[]? ids) + public IEnumerable> Move(IDataType toMove, EntityContainer? container) + { + var parentId = -1; + if (container != null) { - var dataTypeSql = GetBaseQuery(false); - - if (ids?.Any() ?? false) - { - dataTypeSql.Where("umbracoNode.id in (@ids)", new { ids }); - } - else + // Check on paths + if (string.Format(",{0},", container.Path) + .IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) { - dataTypeSql.Where(x => x.NodeObjectType == NodeObjectTypeId); + throw new DataOperationException( + MoveOperationStatusType.FailedNotAllowedByPath); } - var dtos = Database.Fetch(dataTypeSql); - return dtos.Select(x => DataTypeFactory.BuildEntity(x, _editors, _dataTypeLogger, _serializer)).ToArray(); + parentId = container.Id; } - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); + //used to track all the moved entities to be given to the event + var moveInfo = new List> {new(toMove, toMove.Path, parentId)}; - var dtos = Database.Fetch(sql); + var origPath = toMove.Path; - return dtos.Select(x => DataTypeFactory.BuildEntity(x, _editors, _dataTypeLogger, _serializer)).ToArray(); - } + //do the move to a new parent + toMove.ParentId = parentId; - #endregion + //set the updated path + toMove.Path = string.Concat(container == null ? parentId.ToInvariantString() : container.Path, ",", toMove.Id); - #region Overrides of EntityRepositoryBase + //schedule it for updating in the transaction + Save(toMove); - protected override Sql GetBaseQuery(bool isCount) - { - var sql = Sql(); - - sql = isCount - ? sql.SelectCount() - : sql.Select(r => r.Select(x => x.NodeDto)); - - sql - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId); - return sql; - } + //update all descendants from the original path, update in order of level + IEnumerable? descendants = + Get(Query().Where(type => type.Path.StartsWith(origPath + ","))); - protected override string GetBaseWhereClause() + IDataType lastParent = toMove; + if (descendants is not null) { - return "umbracoNode.id = @id"; + foreach (IDataType descendant in descendants.OrderBy(x => x.Level)) + { + moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); + + descendant.ParentId = lastParent.Id; + descendant.Path = string.Concat(lastParent.Path, ",", descendant.Id); + + //schedule it for updating in the transaction + Save(descendant); + } } - protected override IEnumerable GetDeleteClauses() + return moveInfo; + } + + public IReadOnlyDictionary> FindUsages(int id) + { + if (id == default) { - return Array.Empty(); + return new Dictionary>(); } - protected Guid NodeObjectTypeId => Cms.Core.Constants.ObjectTypes.DataType; + Sql sql = Sql() + .Select(ct => ct.Select(node => node.NodeDto)) + .AndSelect(pt => Alias(pt.Alias, "ptAlias"), pt => Alias(pt.Name, "ptName")) + .From() + .InnerJoin().On(ct => ct.NodeId, pt => pt.ContentTypeId) + .InnerJoin().On(n => n.NodeId, ct => ct.NodeId) + .Where(pt => pt.DataTypeId == id) + .OrderBy(node => node.NodeId) + .AndBy(pt => pt.Alias); + + List? dtos = + Database.FetchOneToMany(ct => ct.PropertyTypes, sql); + + return dtos.ToDictionary( + x => (Udi)new GuidUdi(ObjectTypes.GetUdiType(x.NodeDto.NodeObjectType!.Value), x.NodeDto.UniqueId) + .EnsureClosed(), + x => (IEnumerable)x.PropertyTypes.Select(p => p.Alias).ToList()); + } - #endregion + private string? EnsureUniqueNodeName(string? nodeName, int id = 0) + { + SqlTemplate template = SqlContext.Templates.Get(Constants.SqlTemplates.DataTypeRepository.EnsureUniqueNodeName, + tsql => tsql + .Select(x => Alias(x.NodeId, "id"), x => Alias(x.Text, "name")) + .From() + .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType"))); - #region Unit of Work Implementation + Sql sql = template.Sql(NodeObjectTypeId); + List? names = Database.Fetch(sql); - protected override void PersistNewItem(IDataType entity) - { - entity.AddingEntity(); - - //ensure a datatype has a unique name before creating it - entity.Name = EnsureUniqueNodeName(entity.Name)!; - - // TODO: should the below be removed? - //Cannot add a duplicate data type - var existsSql = Sql() - .SelectCount() - .From() - .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .Where(x => x.Text == entity.Name); - var exists = Database.ExecuteScalar(existsSql) > 0; - if (exists) - { - throw new DuplicateNameException("A data type with the name " + entity.Name + " already exists"); - } + return SimilarNodeName.GetUniqueName(names, id, nodeName); + } - var dto = DataTypeFactory.BuildDto(entity, _serializer); - //Logic for setting Path, Level and SortOrder - var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); - int level = parent.Level + 1; - int sortOrder = - Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoNode WHERE parentID = @ParentId AND nodeObjectType = @NodeObjectType", - new { ParentId = entity.ParentId, NodeObjectType = NodeObjectTypeId }); + [TableName(Constants.DatabaseSchema.Tables.ContentType)] + private class ContentTypeReferenceDto : ContentTypeDto + { + [ResultColumn] + [Reference(ReferenceType.Many)] + public List PropertyTypes { get; } = null!; + } - //Create the (base) node data - umbracoNode - var nodeDto = dto.NodeDto; - nodeDto.Path = parent.Path; - nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); - nodeDto.SortOrder = sortOrder; - var o = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); + [TableName(Constants.DatabaseSchema.Tables.PropertyType)] + private class PropertyTypeReferenceDto + { + [Column("ptAlias")] public string? Alias { get; set; } - //Update with new correct path - nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); - Database.Update(nodeDto); + [Column("ptName")] public string? Name { get; set; } + } - //Update entity with correct values - entity.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set - entity.Path = nodeDto.Path; - entity.SortOrder = sortOrder; - entity.Level = level; + #region Overrides of RepositoryBase - dto.NodeId = nodeDto.NodeId; - Database.Insert(dto); + protected override IDataType? PerformGet(int id) => GetMany(id)?.FirstOrDefault(); - entity.ResetDirtyProperties(); - } + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql dataTypeSql = GetBaseQuery(false); - protected override void PersistUpdatedItem(IDataType entity) + if (ids?.Any() ?? false) { + dataTypeSql.Where("umbracoNode.id in (@ids)", new {ids}); + } + else + { + dataTypeSql.Where(x => x.NodeObjectType == NodeObjectTypeId); + } - entity.Name = EnsureUniqueNodeName(entity.Name, entity.Id)!; - - //Cannot change to a duplicate alias - var existsSql = Sql() - .SelectCount() - .From() - .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .Where(x => x.Text == entity.Name && x.NodeId != entity.Id); - var exists = Database.ExecuteScalar(existsSql) > 0; - if (exists) - { - throw new DuplicateNameException("A data type with the name " + entity.Name + " already exists"); - } + List? dtos = Database.Fetch(dataTypeSql); + return dtos.Select(x => DataTypeFactory.BuildEntity(x, _editors, _dataTypeLogger, _serializer)).ToArray(); + } - //Updates Modified date - entity.UpdatingEntity(); + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); - //Look up parent to get and set the correct Path if ParentId has changed - if (entity.IsPropertyDirty("ParentId")) - { - var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); - entity.Path = string.Concat(parent.Path, ",", entity.Id); - entity.Level = parent.Level + 1; - var maxSortOrder = - Database.ExecuteScalar( - "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", - new { ParentId = entity.ParentId, NodeObjectType = NodeObjectTypeId }); - entity.SortOrder = maxSortOrder + 1; - } + List? dtos = Database.Fetch(sql); - var dto = DataTypeFactory.BuildDto(entity, _serializer); + return dtos.Select(x => DataTypeFactory.BuildEntity(x, _editors, _dataTypeLogger, _serializer)).ToArray(); + } - //Updates the (base) node data - umbracoNode - var nodeDto = dto.NodeDto; - Database.Update(nodeDto); - Database.Update(dto); + #endregion - entity.ResetDirtyProperties(); - } + #region Overrides of EntityRepositoryBase - protected override void PersistDeletedItem(IDataType entity) - { - //Remove Notifications - Database.Delete("WHERE nodeId = @Id", new { Id = entity.Id }); + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); + + sql = isCount + ? sql.SelectCount() + : sql.Select(r => r.Select(x => x.NodeDto)); + + sql + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId); + return sql; + } - //Remove Permissions - Database.Delete("WHERE nodeId = @Id", new { Id = entity.Id }); + protected override string GetBaseWhereClause() => "umbracoNode.id = @id"; - //Remove associated tags - Database.Delete("WHERE nodeId = @Id", new { Id = entity.Id }); + protected override IEnumerable GetDeleteClauses() => Array.Empty(); - //PropertyTypes containing the DataType being deleted - var propertyTypeDtos = Database.Fetch("WHERE dataTypeId = @Id", new { Id = entity.Id }); - //Go through the PropertyTypes and delete referenced PropertyData before deleting the PropertyType - foreach (var dto in propertyTypeDtos) - { - Database.Delete("WHERE propertytypeid = @Id", new { Id = dto.Id }); - Database.Delete("WHERE id = @Id", new { Id = dto.Id }); - } + protected Guid NodeObjectTypeId => Constants.ObjectTypes.DataType; - //Delete Content specific data - Database.Delete("WHERE nodeId = @Id", new { Id = entity.Id }); + #endregion - //Delete (base) node data - Database.Delete("WHERE uniqueID = @Id", new { Id = entity.Key }); + #region Unit of Work Implementation - entity.DeleteDate = DateTime.Now; + protected override void PersistNewItem(IDataType entity) + { + entity.AddingEntity(); + + //ensure a datatype has a unique name before creating it + entity.Name = EnsureUniqueNodeName(entity.Name)!; + + // TODO: should the below be removed? + //Cannot add a duplicate data type + Sql existsSql = Sql() + .SelectCount() + .From() + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .Where(x => x.Text == entity.Name); + var exists = Database.ExecuteScalar(existsSql) > 0; + if (exists) + { + throw new DuplicateNameException("A data type with the name " + entity.Name + " already exists"); } - #endregion + DataTypeDto dto = DataTypeFactory.BuildDto(entity, _serializer); + + //Logic for setting Path, Level and SortOrder + NodeDto? parent = Database.First("WHERE id = @ParentId", new {entity.ParentId}); + var level = parent.Level + 1; + var sortOrder = + Database.ExecuteScalar( + "SELECT COUNT(*) FROM umbracoNode WHERE parentID = @ParentId AND nodeObjectType = @NodeObjectType", + new {entity.ParentId, NodeObjectType = NodeObjectTypeId}); + + //Create the (base) node data - umbracoNode + NodeDto nodeDto = dto.NodeDto; + nodeDto.Path = parent.Path; + nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); + nodeDto.SortOrder = sortOrder; + var o = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); + + //Update with new correct path + nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); + Database.Update(nodeDto); + + //Update entity with correct values + entity.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set + entity.Path = nodeDto.Path; + entity.SortOrder = sortOrder; + entity.Level = level; + + dto.NodeId = nodeDto.NodeId; + Database.Insert(dto); + + entity.ResetDirtyProperties(); + } - public IEnumerable> Move(IDataType toMove, EntityContainer? container) + protected override void PersistUpdatedItem(IDataType entity) + { + entity.Name = EnsureUniqueNodeName(entity.Name, entity.Id)!; + + //Cannot change to a duplicate alias + Sql existsSql = Sql() + .SelectCount() + .From() + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .Where(x => x.Text == entity.Name && x.NodeId != entity.Id); + var exists = Database.ExecuteScalar(existsSql) > 0; + if (exists) { - var parentId = -1; - if (container != null) - { - // Check on paths - if ((string.Format(",{0},", container.Path)).IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) - { - throw new DataOperationException(MoveOperationStatusType.FailedNotAllowedByPath); - } - parentId = container.Id; - } - - //used to track all the moved entities to be given to the event - var moveInfo = new List> - { - new MoveEventInfo(toMove, toMove.Path, parentId) - }; - - var origPath = toMove.Path; - - //do the move to a new parent - toMove.ParentId = parentId; + throw new DuplicateNameException("A data type with the name " + entity.Name + " already exists"); + } - //set the updated path - toMove.Path = string.Concat(container == null ? parentId.ToInvariantString() : container.Path, ",", toMove.Id); + //Updates Modified date + entity.UpdatingEntity(); - //schedule it for updating in the transaction - Save(toMove); + //Look up parent to get and set the correct Path if ParentId has changed + if (entity.IsPropertyDirty("ParentId")) + { + NodeDto? parent = Database.First("WHERE id = @ParentId", new {entity.ParentId}); + entity.Path = string.Concat(parent.Path, ",", entity.Id); + entity.Level = parent.Level + 1; + var maxSortOrder = + Database.ExecuteScalar( + "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", + new {entity.ParentId, NodeObjectType = NodeObjectTypeId}); + entity.SortOrder = maxSortOrder + 1; + } - //update all descendants from the original path, update in order of level - var descendants = Get(Query().Where(type => type.Path.StartsWith(origPath + ","))); + DataTypeDto dto = DataTypeFactory.BuildDto(entity, _serializer); - var lastParent = toMove; - if (descendants is not null) - { - foreach (var descendant in descendants.OrderBy(x => x.Level)) - { - moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); + //Updates the (base) node data - umbracoNode + NodeDto nodeDto = dto.NodeDto; + Database.Update(nodeDto); + Database.Update(dto); - descendant.ParentId = lastParent.Id; - descendant.Path = string.Concat(lastParent.Path, ",", descendant.Id); + entity.ResetDirtyProperties(); + } - //schedule it for updating in the transaction - Save(descendant); - } - } + protected override void PersistDeletedItem(IDataType entity) + { + //Remove Notifications + Database.Delete("WHERE nodeId = @Id", new {entity.Id}); - return moveInfo; - } + //Remove Permissions + Database.Delete("WHERE nodeId = @Id", new {entity.Id}); - public IReadOnlyDictionary> FindUsages(int id) - { - if (id == default) - return new Dictionary>(); - - var sql = Sql() - .Select(ct => ct.Select(node => node.NodeDto)) - .AndSelect(pt => Alias(pt.Alias, "ptAlias"), pt => Alias(pt.Name, "ptName")) - .From() - .InnerJoin().On(ct => ct.NodeId, pt => pt.ContentTypeId) - .InnerJoin().On(n => n.NodeId, ct => ct.NodeId) - .Where(pt => pt.DataTypeId == id) - .OrderBy(node => node.NodeId) - .AndBy(pt => pt.Alias); - - var dtos = Database.FetchOneToMany(ct => ct.PropertyTypes, sql); - - return dtos.ToDictionary( - x => (Udi)new GuidUdi(ObjectTypes.GetUdiType(x.NodeDto.NodeObjectType!.Value), x.NodeDto.UniqueId).EnsureClosed(), - x => (IEnumerable)x.PropertyTypes.Select(p => p.Alias).ToList()); - } + //Remove associated tags + Database.Delete("WHERE nodeId = @Id", new {entity.Id}); - private string? EnsureUniqueNodeName(string? nodeName, int id = 0) + //PropertyTypes containing the DataType being deleted + List? propertyTypeDtos = + Database.Fetch("WHERE dataTypeId = @Id", new {entity.Id}); + //Go through the PropertyTypes and delete referenced PropertyData before deleting the PropertyType + foreach (PropertyTypeDto? dto in propertyTypeDtos) { - var template = SqlContext.Templates.Get(Cms.Core.Constants.SqlTemplates.DataTypeRepository.EnsureUniqueNodeName, tsql => tsql - .Select(x => Alias(x.NodeId, "id"), x => Alias(x.Text, "name")) - .From() - .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType"))); - - var sql = template.Sql(NodeObjectTypeId); - var names = Database.Fetch(sql); - - return SimilarNodeName.GetUniqueName(names, id, nodeName); + Database.Delete("WHERE propertytypeid = @Id", new {dto.Id}); + Database.Delete("WHERE id = @Id", new {dto.Id}); } + //Delete Content specific data + Database.Delete("WHERE nodeId = @Id", new {entity.Id}); - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.ContentType)] - private class ContentTypeReferenceDto : ContentTypeDto - { - [ResultColumn] - [Reference(ReferenceType.Many)] - public List PropertyTypes { get; set; } = null!; - } - - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType)] - private class PropertyTypeReferenceDto - { - [Column("ptAlias")] - public string? Alias { get; set; } + //Delete (base) node data + Database.Delete("WHERE uniqueID = @Id", new {Id = entity.Key}); - [Column("ptName")] - public string? Name { get; set; } - } + entity.DeleteDate = DateTime.Now; } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs index 7f39bb3ee638..6f861904407a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -15,383 +12,358 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents a repository for doing CRUD operations for +/// +internal class DictionaryRepository : EntityRepositoryBase, IDictionaryRepository { - /// - /// Represents a repository for doing CRUD operations for - /// - internal class DictionaryRepository : EntityRepositoryBase, IDictionaryRepository + private readonly ILoggerFactory _loggerFactory; + + public DictionaryRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, + ILoggerFactory loggerFactory) + : base(scopeAccessor, cache, logger) => + _loggerFactory = loggerFactory; + + public IDictionaryItem? Get(Guid uniqueId) + { + var uniqueIdRepo = new DictionaryByUniqueIdRepository(this, ScopeAccessor, AppCaches, + _loggerFactory.CreateLogger()); + return uniqueIdRepo.Get(uniqueId); + } + + public IDictionaryItem? Get(string key) + { + var keyRepo = new DictionaryByKeyRepository(this, ScopeAccessor, AppCaches, + _loggerFactory.CreateLogger()); + return keyRepo.Get(key); + } + + public Dictionary GetDictionaryItemKeyMap() + { + var columns = new[] {"key", "id"}.Select(x => (object)SqlSyntax.GetQuotedColumnName(x)).ToArray(); + Sql sql = Sql().Select(columns).From(); + return Database.Fetch(sql).ToDictionary(x => x.Key, x => x.Id); + } + + public IEnumerable GetDictionaryItemDescendants(Guid? parentId) { - private readonly ILoggerFactory _loggerFactory; + //This methods will look up children at each level, since we do not store a path for dictionary (ATM), we need to do a recursive + // lookup to get descendants. Currently this is the most efficient way to do it - public DictionaryRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, ILoggerFactory loggerFactory) - : base(scopeAccessor, cache, logger) + Func>> getItemsFromParents = guids => { - _loggerFactory = loggerFactory; - } + return guids.InGroupsOf(Constants.Sql.MaxParameterCount) + .Select(group => + { + Sql sqlClause = GetBaseQuery(false) + .Where(x => x.Parent != null) + .WhereIn(x => x.Parent, group); + + var translator = new SqlTranslator(sqlClause, Query()); + Sql sql = translator.Translate(); + sql.OrderBy(x => x.UniqueId); + + return Database + .FetchOneToMany(x => x.LanguageTextDtos, sql) + .Select(ConvertFromDto); + }); + }; + + IEnumerable> childItems = parentId.HasValue == false + ? new[] {GetRootDictionaryItems()!} + : getItemsFromParents(new[] {parentId.Value}); + + return childItems.SelectRecursive(items => getItemsFromParents(items.Select(x => x.Key).ToArray())) + .SelectMany(items => items); + } - protected override IRepositoryCachePolicy CreateCachePolicy() + protected override IRepositoryCachePolicy CreateCachePolicy() + { + var options = new RepositoryCachePolicyOptions { - var options = new RepositoryCachePolicyOptions - { - //allow zero to be cached - GetAllCacheAllowZeroCount = true - }; + //allow zero to be cached + GetAllCacheAllowZeroCount = true + }; - return new SingleItemsOnlyRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, options); - } + return new SingleItemsOnlyRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, + options); + } - #region Overrides of RepositoryBase + protected IDictionaryItem ConvertFromDto(DictionaryDto dto) + { + IDictionaryItem entity = DictionaryItemFactory.BuildEntity(dto); - protected override IDictionaryItem? PerformGet(int id) - { - var sql = GetBaseQuery(false) - .Where(GetBaseWhereClause(), new { id = id }) - .OrderBy(x => x.UniqueId); + entity.Translations = dto.LanguageTextDtos.EmptyNull() + .Where(x => x.LanguageId > 0) + .Select(x => DictionaryTranslationFactory.BuildEntity(x, dto.UniqueId)) + .ToList(); - var dto = Database - .FetchOneToMany(x => x.LanguageTextDtos, sql) - .FirstOrDefault(); + return entity; + } - if (dto == null) - return null; + private IEnumerable? GetRootDictionaryItems() + { + IQuery query = Query().Where(x => x.ParentId == null); + return Get(query); + } - var entity = ConvertFromDto(dto); + private class DictionaryItemKeyIdDto + { + public string Key { get; } = null!; + public Guid Id { get; set; } + } - // reset dirty initial properties (U4-1946) - ((EntityBase)entity).ResetDirtyProperties(false); + private class DictionaryByUniqueIdRepository : SimpleGetRepository + { + private readonly DictionaryRepository _dictionaryRepository; - return entity; - } + public DictionaryByUniqueIdRepository(DictionaryRepository dictionaryRepository, IScopeAccessor scopeAccessor, + AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) => + _dictionaryRepository = dictionaryRepository; - protected override IEnumerable PerformGetAll(params int[]? ids) - { - var sql = GetBaseQuery(false).Where(x => x.PrimaryKey > 0); - if (ids?.Any() ?? false) - { - sql.WhereIn(x => x.PrimaryKey, ids); - } + protected override IEnumerable PerformFetch(Sql sql) => + Database + .FetchOneToMany(x => x.LanguageTextDtos, sql); - return Database - .FetchOneToMany(x => x.LanguageTextDtos, sql) - .Select(ConvertFromDto); - } + protected override Sql GetBaseQuery(bool isCount) => _dictionaryRepository.GetBaseQuery(isCount); - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - sql.OrderBy(x => x.UniqueId); - - return Database - .FetchOneToMany(x => x.LanguageTextDtos, sql) - .Select(ConvertFromDto); - } + protected override string GetBaseWhereClause() => + "cmsDictionary." + SqlSyntax.GetQuotedColumnName("id") + " = @id"; + + protected override IDictionaryItem ConvertToEntity(DictionaryDto dto) => + _dictionaryRepository.ConvertFromDto(dto); - #endregion + protected override object GetBaseWhereClauseArguments(Guid id) => new {id}; - #region Overrides of EntityRepositoryBase + protected override string GetWhereInClauseForGetAll() => + "cmsDictionary." + SqlSyntax.GetQuotedColumnName("id") + " in (@ids)"; - protected override Sql GetBaseQuery(bool isCount) + protected override IRepositoryCachePolicy CreateCachePolicy() { - var sql = Sql(); - if (isCount) - { - sql.SelectCount() - .From(); - } - else + var options = new RepositoryCachePolicyOptions { - sql.SelectAll() - .From() - .LeftJoin() - .On(left => left.UniqueId, right => right.UniqueId); - } - return sql; - } + //allow zero to be cached + GetAllCacheAllowZeroCount = true + }; - protected override string GetBaseWhereClause() - { - return $"{Constants.DatabaseSchema.Tables.DictionaryEntry}.pk = @id"; + return new SingleItemsOnlyRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, + options); } + } - protected override IEnumerable GetDeleteClauses() - { - return new List(); - } + private class DictionaryByKeyRepository : SimpleGetRepository + { + private readonly DictionaryRepository _dictionaryRepository; - #endregion + public DictionaryByKeyRepository(DictionaryRepository dictionaryRepository, IScopeAccessor scopeAccessor, + AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) => + _dictionaryRepository = dictionaryRepository; - #region Unit of Work Implementation + protected override IEnumerable PerformFetch(Sql sql) => + Database + .FetchOneToMany(x => x.LanguageTextDtos, sql); - protected override void PersistNewItem(IDictionaryItem entity) - { - var dictionaryItem = ((DictionaryItem) entity); + protected override Sql GetBaseQuery(bool isCount) => _dictionaryRepository.GetBaseQuery(isCount); - dictionaryItem.AddingEntity(); + protected override string GetBaseWhereClause() => + "cmsDictionary." + SqlSyntax.GetQuotedColumnName("key") + " = @id"; - foreach (var translation in dictionaryItem.Translations) - translation.Value = translation.Value.ToValidXmlString(); + protected override IDictionaryItem ConvertToEntity(DictionaryDto dto) => + _dictionaryRepository.ConvertFromDto(dto); - var dto = DictionaryItemFactory.BuildDto(dictionaryItem); + protected override object GetBaseWhereClauseArguments(string? id) => new {id}; - var id = Convert.ToInt32(Database.Insert(dto)); - dictionaryItem.Id = id; + protected override string GetWhereInClauseForGetAll() => + "cmsDictionary." + SqlSyntax.GetQuotedColumnName("key") + " in (@ids)"; - foreach (var translation in dictionaryItem.Translations) + protected override IRepositoryCachePolicy CreateCachePolicy() + { + var options = new RepositoryCachePolicyOptions { - var textDto = DictionaryTranslationFactory.BuildDto(translation, dictionaryItem.Key); - translation.Id = Convert.ToInt32(Database.Insert(textDto)); - translation.Key = dictionaryItem.Key; - } + //allow zero to be cached + GetAllCacheAllowZeroCount = true + }; - dictionaryItem.ResetDirtyProperties(); + return new SingleItemsOnlyRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, + options); } + } - protected override void PersistUpdatedItem(IDictionaryItem entity) - { - entity.UpdatingEntity(); - - foreach (var translation in entity.Translations) - translation.Value = translation.Value.ToValidXmlString(); - - var dto = DictionaryItemFactory.BuildDto(entity); - - Database.Update(dto); + #region Overrides of RepositoryBase - foreach (var translation in entity.Translations) - { - var textDto = DictionaryTranslationFactory.BuildDto(translation, entity.Key); - if (translation.HasIdentity) - { - Database.Update(textDto); - } - else - { - translation.Id = Convert.ToInt32(Database.Insert(textDto)); - translation.Key = entity.Key; - } - } + protected override IDictionaryItem? PerformGet(int id) + { + Sql sql = GetBaseQuery(false) + .Where(GetBaseWhereClause(), new {id}) + .OrderBy(x => x.UniqueId); - entity.ResetDirtyProperties(); + DictionaryDto? dto = Database + .FetchOneToMany(x => x.LanguageTextDtos, sql) + .FirstOrDefault(); - //Clear the cache entries that exist by uniqueid/item key - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.ItemKey)); - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Key)); - } - - protected override void PersistDeletedItem(IDictionaryItem entity) + if (dto == null) { - RecursiveDelete(entity.Key); + return null; + } - Database.Delete("WHERE UniqueId = @Id", new { Id = entity.Key }); - Database.Delete("WHERE id = @Id", new { Id = entity.Key }); + IDictionaryItem entity = ConvertFromDto(dto); - //Clear the cache entries that exist by uniqueid/item key - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.ItemKey)); - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Key)); + // reset dirty initial properties (U4-1946) + ((EntityBase)entity).ResetDirtyProperties(false); - entity.DeleteDate = DateTime.Now; - } + return entity; + } - private void RecursiveDelete(Guid parentId) + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = GetBaseQuery(false).Where(x => x.PrimaryKey > 0); + if (ids?.Any() ?? false) { - var list = Database.Fetch("WHERE parent = @ParentId", new { ParentId = parentId }); - foreach (var dto in list) - { - RecursiveDelete(dto.UniqueId); - - Database.Delete("WHERE UniqueId = @Id", new { Id = dto.UniqueId }); - Database.Delete("WHERE id = @Id", new { Id = dto.UniqueId }); - - //Clear the cache entries that exist by uniqueid/item key - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(dto.Key)); - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(dto.UniqueId)); - } + sql.WhereIn(x => x.PrimaryKey, ids); } - #endregion + return Database + .FetchOneToMany(x => x.LanguageTextDtos, sql) + .Select(ConvertFromDto); + } - protected IDictionaryItem ConvertFromDto(DictionaryDto dto) - { - var entity = DictionaryItemFactory.BuildEntity(dto); + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + sql.OrderBy(x => x.UniqueId); + + return Database + .FetchOneToMany(x => x.LanguageTextDtos, sql) + .Select(ConvertFromDto); + } - entity.Translations = dto.LanguageTextDtos.EmptyNull() - .Where(x => x.LanguageId > 0) - .Select(x => DictionaryTranslationFactory.BuildEntity(x, dto.UniqueId)) - .ToList(); + #endregion - return entity; - } + #region Overrides of EntityRepositoryBase - public IDictionaryItem? Get(Guid uniqueId) + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); + if (isCount) { - var uniqueIdRepo = new DictionaryByUniqueIdRepository(this, ScopeAccessor, AppCaches, _loggerFactory.CreateLogger()); - return uniqueIdRepo.Get(uniqueId); + sql.SelectCount() + .From(); } - - public IDictionaryItem? Get(string key) + else { - var keyRepo = new DictionaryByKeyRepository(this, ScopeAccessor, AppCaches, _loggerFactory.CreateLogger()); - return keyRepo.Get(key); + sql.SelectAll() + .From() + .LeftJoin() + .On(left => left.UniqueId, right => right.UniqueId); } - private IEnumerable? GetRootDictionaryItems() - { - var query = Query().Where(x => x.ParentId == null); - return Get(query); - } + return sql; + } - public Dictionary GetDictionaryItemKeyMap() - { - var columns = new[] { "key", "id" }.Select(x => (object) SqlSyntax.GetQuotedColumnName(x)).ToArray(); - var sql = Sql().Select(columns).From(); - return Database.Fetch(sql).ToDictionary(x => x.Key, x => x.Id); - } + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.DictionaryEntry}.pk = @id"; - private class DictionaryItemKeyIdDto - { - public string Key { get; set; } = null!; - public Guid Id { get; set; } - } + protected override IEnumerable GetDeleteClauses() => new List(); - public IEnumerable GetDictionaryItemDescendants(Guid? parentId) - { - //This methods will look up children at each level, since we do not store a path for dictionary (ATM), we need to do a recursive - // lookup to get descendants. Currently this is the most efficient way to do it + #endregion - Func>> getItemsFromParents = guids => - { - return guids.InGroupsOf(Constants.Sql.MaxParameterCount) - .Select(group => - { - var sqlClause = GetBaseQuery(false) - .Where(x => x.Parent != null) - .WhereIn(x => x.Parent, group); - - var translator = new SqlTranslator(sqlClause, Query()); - var sql = translator.Translate(); - sql.OrderBy(x => x.UniqueId); - - return Database - .FetchOneToMany(x=> x.LanguageTextDtos, sql) - .Select(ConvertFromDto); - }); - }; + #region Unit of Work Implementation - var childItems = parentId.HasValue == false - ? new[] { GetRootDictionaryItems()! } - : getItemsFromParents(new[] { parentId.Value }); + protected override void PersistNewItem(IDictionaryItem entity) + { + var dictionaryItem = (DictionaryItem)entity; - return childItems.SelectRecursive(items => getItemsFromParents(items.Select(x => x.Key).ToArray())).SelectMany(items => items); + dictionaryItem.AddingEntity(); + foreach (IDictionaryTranslation translation in dictionaryItem.Translations) + { + translation.Value = translation.Value.ToValidXmlString(); } - private class DictionaryByUniqueIdRepository : SimpleGetRepository - { - private readonly DictionaryRepository _dictionaryRepository; + DictionaryDto dto = DictionaryItemFactory.BuildDto(dictionaryItem); - public DictionaryByUniqueIdRepository(DictionaryRepository dictionaryRepository, IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { - _dictionaryRepository = dictionaryRepository; - } + var id = Convert.ToInt32(Database.Insert(dto)); + dictionaryItem.Id = id; - protected override IEnumerable PerformFetch(Sql sql) - { - return Database - .FetchOneToMany(x => x.LanguageTextDtos, sql); - } + foreach (IDictionaryTranslation translation in dictionaryItem.Translations) + { + LanguageTextDto textDto = DictionaryTranslationFactory.BuildDto(translation, dictionaryItem.Key); + translation.Id = Convert.ToInt32(Database.Insert(textDto)); + translation.Key = dictionaryItem.Key; + } - protected override Sql GetBaseQuery(bool isCount) - { - return _dictionaryRepository.GetBaseQuery(isCount); - } + dictionaryItem.ResetDirtyProperties(); + } - protected override string GetBaseWhereClause() - { - return "cmsDictionary." + SqlSyntax.GetQuotedColumnName("id") + " = @id"; - } + protected override void PersistUpdatedItem(IDictionaryItem entity) + { + entity.UpdatingEntity(); - protected override IDictionaryItem ConvertToEntity(DictionaryDto dto) - { - return _dictionaryRepository.ConvertFromDto(dto); - } + foreach (IDictionaryTranslation translation in entity.Translations) + { + translation.Value = translation.Value.ToValidXmlString(); + } - protected override object GetBaseWhereClauseArguments(Guid id) - { - return new { id = id }; - } + DictionaryDto dto = DictionaryItemFactory.BuildDto(entity); - protected override string GetWhereInClauseForGetAll() + Database.Update(dto); + + foreach (IDictionaryTranslation translation in entity.Translations) + { + LanguageTextDto textDto = DictionaryTranslationFactory.BuildDto(translation, entity.Key); + if (translation.HasIdentity) { - return "cmsDictionary." + SqlSyntax.GetQuotedColumnName("id") + " in (@ids)"; + Database.Update(textDto); } - - protected override IRepositoryCachePolicy CreateCachePolicy() + else { - var options = new RepositoryCachePolicyOptions - { - //allow zero to be cached - GetAllCacheAllowZeroCount = true - }; - - return new SingleItemsOnlyRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, options); + translation.Id = Convert.ToInt32(Database.Insert(textDto)); + translation.Key = entity.Key; } } - private class DictionaryByKeyRepository : SimpleGetRepository - { - private readonly DictionaryRepository _dictionaryRepository; + entity.ResetDirtyProperties(); - public DictionaryByKeyRepository(DictionaryRepository dictionaryRepository, IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { - _dictionaryRepository = dictionaryRepository; - } - - protected override IEnumerable PerformFetch(Sql sql) - { - return Database - .FetchOneToMany(x => x.LanguageTextDtos, sql); - } + //Clear the cache entries that exist by uniqueid/item key + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.ItemKey)); + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Key)); + } - protected override Sql GetBaseQuery(bool isCount) - { - return _dictionaryRepository.GetBaseQuery(isCount); - } + protected override void PersistDeletedItem(IDictionaryItem entity) + { + RecursiveDelete(entity.Key); - protected override string GetBaseWhereClause() - { - return "cmsDictionary." + SqlSyntax.GetQuotedColumnName("key") + " = @id"; - } + Database.Delete("WHERE UniqueId = @Id", new {Id = entity.Key}); + Database.Delete("WHERE id = @Id", new {Id = entity.Key}); - protected override IDictionaryItem ConvertToEntity(DictionaryDto dto) - { - return _dictionaryRepository.ConvertFromDto(dto); - } + //Clear the cache entries that exist by uniqueid/item key + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.ItemKey)); + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Key)); - protected override object GetBaseWhereClauseArguments(string? id) - { - return new { id = id }; - } + entity.DeleteDate = DateTime.Now; + } - protected override string GetWhereInClauseForGetAll() - { - return "cmsDictionary." + SqlSyntax.GetQuotedColumnName("key") + " in (@ids)"; - } + private void RecursiveDelete(Guid parentId) + { + List? list = + Database.Fetch("WHERE parent = @ParentId", new {ParentId = parentId}); + foreach (DictionaryDto? dto in list) + { + RecursiveDelete(dto.UniqueId); - protected override IRepositoryCachePolicy CreateCachePolicy() - { - var options = new RepositoryCachePolicyOptions - { - //allow zero to be cached - GetAllCacheAllowZeroCount = true - }; + Database.Delete("WHERE UniqueId = @Id", new {Id = dto.UniqueId}); + Database.Delete("WHERE id = @Id", new {Id = dto.UniqueId}); - return new SingleItemsOnlyRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, options); - } + //Clear the cache entries that exist by uniqueid/item key + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(dto.Key)); + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(dto.UniqueId)); } } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs index f97aec0917e3..9105079d21b2 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs @@ -1,5 +1,5 @@ -using System; using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Persistence.Repositories; @@ -8,43 +8,43 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Scoping; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Override the base content repository so we can change the node object type +/// +/// +/// It would be nicer if we could separate most of this down into a smaller version of the ContentRepository class, +/// however to do that +/// requires quite a lot of work since we'd need to re-organize the inheritance quite a lot or create a helper class to +/// perform a lot of the underlying logic. +/// TODO: Create a helper method to contain most of the underlying logic for the ContentRepository +/// +internal class DocumentBlueprintRepository : DocumentRepository, IDocumentBlueprintRepository { - /// - /// Override the base content repository so we can change the node object type - /// - /// - /// It would be nicer if we could separate most of this down into a smaller version of the ContentRepository class, however to do that - /// requires quite a lot of work since we'd need to re-organize the inheritance quite a lot or create a helper class to perform a lot of the underlying logic. - /// - /// TODO: Create a helper method to contain most of the underlying logic for the ContentRepository - /// - internal class DocumentBlueprintRepository : DocumentRepository, IDocumentBlueprintRepository + public DocumentBlueprintRepository( + IScopeAccessor scopeAccessor, + AppCaches appCaches, + ILogger logger, + ILoggerFactory loggerFactory, + IContentTypeRepository contentTypeRepository, + ITemplateRepository templateRepository, + ITagRepository tagRepository, + ILanguageRepository languageRepository, + IRelationRepository relationRepository, + IRelationTypeRepository relationTypeRepository, + PropertyEditorCollection propertyEditorCollection, + IDataTypeService dataTypeService, + DataValueReferenceFactoryCollection dataValueReferenceFactories, + IJsonSerializer serializer, + IEventAggregator eventAggregator) + : base(scopeAccessor, appCaches, logger, loggerFactory, contentTypeRepository, templateRepository, + tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection, + dataValueReferenceFactories, dataTypeService, serializer, eventAggregator) { - public DocumentBlueprintRepository( - IScopeAccessor scopeAccessor, - AppCaches appCaches, - ILogger logger, - ILoggerFactory loggerFactory, - IContentTypeRepository contentTypeRepository, - ITemplateRepository templateRepository, - ITagRepository tagRepository, - ILanguageRepository languageRepository, - IRelationRepository relationRepository, - IRelationTypeRepository relationTypeRepository, - PropertyEditorCollection propertyEditorCollection, - IDataTypeService dataTypeService, - DataValueReferenceFactoryCollection dataValueReferenceFactories, - IJsonSerializer serializer, - IEventAggregator eventAggregator) - : base(scopeAccessor, appCaches, logger, loggerFactory, contentTypeRepository, templateRepository, - tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection, - dataValueReferenceFactories, dataTypeService, serializer, eventAggregator) - { - } + } - protected override bool EnsureUniqueNaming => false; // duplicates are allowed + protected override bool EnsureUniqueNaming => false; // duplicates are allowed - protected override Guid NodeObjectTypeId => Cms.Core.Constants.ObjectTypes.DocumentBlueprint; - } + protected override Guid NodeObjectTypeId => Constants.ObjectTypes.DocumentBlueprint; } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs index 39084aa5d9e7..0caafbcf79c2 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -14,7 +11,6 @@ using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.PropertyEditors; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence.Dtos; @@ -24,1603 +20,1708 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents a repository for doing CRUD operations for . +/// +public class DocumentRepository : ContentRepositoryBase, IDocumentRepository { + private readonly AppCaches _appCaches; + private readonly ContentByGuidReadRepository _contentByGuidReadRepository; + private readonly IContentTypeRepository _contentTypeRepository; + private readonly ILoggerFactory _loggerFactory; + private readonly IScopeAccessor _scopeAccessor; + private readonly IJsonSerializer _serializer; + private readonly ITagRepository _tagRepository; + private readonly ITemplateRepository _templateRepository; + private PermissionRepository? _permissionRepository; + /// - /// Represents a repository for doing CRUD operations for . + /// Constructor /// - public class DocumentRepository : ContentRepositoryBase, IDocumentRepository + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// Lazy property value collection - must be lazy because we have a circular dependency since some property editors + /// require services, yet these services require property editors + /// + public DocumentRepository( + IScopeAccessor scopeAccessor, + AppCaches appCaches, + ILogger logger, + ILoggerFactory loggerFactory, + IContentTypeRepository contentTypeRepository, + ITemplateRepository templateRepository, + ITagRepository tagRepository, + ILanguageRepository languageRepository, + IRelationRepository relationRepository, + IRelationTypeRepository relationTypeRepository, + PropertyEditorCollection propertyEditors, + DataValueReferenceFactoryCollection dataValueReferenceFactories, + IDataTypeService dataTypeService, + IJsonSerializer serializer, + IEventAggregator eventAggregator) + : base(scopeAccessor, appCaches, logger, languageRepository, relationRepository, relationTypeRepository, + propertyEditors, dataValueReferenceFactories, dataTypeService, eventAggregator) { - private readonly IContentTypeRepository _contentTypeRepository; - private readonly ITemplateRepository _templateRepository; - private readonly ITagRepository _tagRepository; - private readonly IJsonSerializer _serializer; - private readonly AppCaches _appCaches; - private readonly ILoggerFactory _loggerFactory; - private PermissionRepository? _permissionRepository; - private readonly ContentByGuidReadRepository _contentByGuidReadRepository; - private readonly IScopeAccessor _scopeAccessor; - - /// - /// Constructor - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// Lazy property value collection - must be lazy because we have a circular dependency since some property editors require services, yet these services require property editors - /// - public DocumentRepository( - IScopeAccessor scopeAccessor, - AppCaches appCaches, - ILogger logger, - ILoggerFactory loggerFactory, - IContentTypeRepository contentTypeRepository, - ITemplateRepository templateRepository, - ITagRepository tagRepository, - ILanguageRepository languageRepository, - IRelationRepository relationRepository, - IRelationTypeRepository relationTypeRepository, - PropertyEditorCollection propertyEditors, - DataValueReferenceFactoryCollection dataValueReferenceFactories, - IDataTypeService dataTypeService, - IJsonSerializer serializer, - IEventAggregator eventAggregator) - : base(scopeAccessor, appCaches, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferenceFactories, dataTypeService, eventAggregator) - { - _contentTypeRepository = contentTypeRepository ?? throw new ArgumentNullException(nameof(contentTypeRepository)); - _templateRepository = templateRepository ?? throw new ArgumentNullException(nameof(templateRepository)); - _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); - _serializer = serializer; - _appCaches = appCaches; - _loggerFactory = loggerFactory; - _scopeAccessor = scopeAccessor; - _contentByGuidReadRepository = new ContentByGuidReadRepository(this, scopeAccessor, appCaches, loggerFactory.CreateLogger()); - } - - protected override DocumentRepository This => this; - - /// - /// Default is to always ensure all documents have unique names - /// - protected virtual bool EnsureUniqueNaming { get; } = true; + _contentTypeRepository = + contentTypeRepository ?? throw new ArgumentNullException(nameof(contentTypeRepository)); + _templateRepository = templateRepository ?? throw new ArgumentNullException(nameof(templateRepository)); + _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); + _serializer = serializer; + _appCaches = appCaches; + _loggerFactory = loggerFactory; + _scopeAccessor = scopeAccessor; + _contentByGuidReadRepository = new ContentByGuidReadRepository(this, scopeAccessor, appCaches, + loggerFactory.CreateLogger()); + } - // note: is ok to 'new' the repo here as it's a sub-repo really - private PermissionRepository PermissionRepository => _permissionRepository - ?? (_permissionRepository = new PermissionRepository(_scopeAccessor, _appCaches, _loggerFactory.CreateLogger>())); + protected override DocumentRepository This => this; - #region Repository Base + /// + /// Default is to always ensure all documents have unique names + /// + protected virtual bool EnsureUniqueNaming { get; } = true; + + // note: is ok to 'new' the repo here as it's a sub-repo really + private PermissionRepository PermissionRepository => _permissionRepository + ?? (_permissionRepository = + new PermissionRepository( + _scopeAccessor, _appCaches, + _loggerFactory + .CreateLogger< + PermissionRepository>())); + + /// + public ContentScheduleCollection GetContentSchedule(int contentId) + { + var result = new ContentScheduleCollection(); - protected override Guid NodeObjectTypeId => Cms.Core.Constants.ObjectTypes.Document; + List? scheduleDtos = Database.Fetch(Sql() + .Select() + .From() + .Where(x => x.NodeId == contentId)); - protected override IContent? PerformGet(int id) + foreach (ContentScheduleDto? scheduleDto in scheduleDtos) { - var sql = GetBaseQuery(QueryType.Single) - .Where(x => x.NodeId == id) - .SelectTop(1); - - var dto = Database.Fetch(sql).FirstOrDefault(); - return dto == null - ? null - : MapDtoToContent(dto); + result.Add(new ContentSchedule(scheduleDto.Id, + LanguageRepository.GetIsoCodeById(scheduleDto.LanguageId) ?? string.Empty, + scheduleDto.Date, + scheduleDto.Action == ContentScheduleAction.Release.ToString() + ? ContentScheduleAction.Release + : ContentScheduleAction.Expire)); } - protected override IEnumerable PerformGetAll(params int[]? ids) + return result; + } + + protected override string ApplySystemOrdering(ref Sql sql, Ordering ordering) + { + // note: 'updater' is the user who created the latest draft version, + // we don't have an 'updater' per culture (should we?) + if (ordering.OrderBy.InvariantEquals("updater")) { - var sql = GetBaseQuery(QueryType.Many); + Sql joins = Sql() + .InnerJoin("updaterUser") + .On((version, user) => version.UserId == user.Id, + aliasRight: "updaterUser"); - if (ids?.Any() ?? false) - sql.WhereIn(x => x.NodeId, ids); + // see notes in ApplyOrdering: the field MUST be selected + aliased + sql = Sql( + InsertBefore(sql, "FROM", + ", " + SqlSyntax.GetFieldName(x => x.UserName, "updaterUser") + " AS ordering "), + sql.Arguments); - return MapDtosToContent(Database.Fetch(sql)); + sql = InsertJoins(sql, joins); + + return "ordering"; } - protected override IEnumerable PerformGetByQuery(IQuery query) + if (ordering.OrderBy.InvariantEquals("published")) { - var sqlClause = GetBaseQuery(QueryType.Many); + // no culture = can only work on the global 'published' flag + if (ordering.Culture.IsNullOrWhiteSpace()) + { + // see notes in ApplyOrdering: the field MUST be selected + aliased, and we cannot have + // the whole CASE fragment in ORDER BY due to it not being detected by NPoco + sql = Sql(InsertBefore(sql, "FROM", ", (CASE WHEN pcv.id IS NULL THEN 0 ELSE 1 END) AS ordering "), + sql.Arguments); + return "ordering"; + } - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); + // invariant: left join will yield NULL and we must use pcv to determine published + // variant: left join may yield NULL or something, and that determines published - AddGetByQueryOrderBy(sql); - return MapDtosToContent(Database.Fetch(sql)); - } + Sql joins = Sql() + .InnerJoin("ctype").On( + (content, contentType) => content.ContentTypeId == contentType.NodeId, aliasRight: "ctype") + // left join on optional culture variation + //the magic "[[[ISOCODE]]]" parameter value will be replaced in ContentRepositoryBase.GetPage() by the actual ISO code + .LeftJoin(nested => + nested.InnerJoin("langp").On( + (ccv, lang) => ccv.LanguageId == lang.Id && lang.IsoCode == "[[[ISOCODE]]]", "ccvp", + "langp"), + "ccvp") + .On((version, ccv) => version.Id == ccv.VersionId, + "pcv", "ccvp"); - private void AddGetByQueryOrderBy(Sql sql) - { - sql - .OrderBy(x => x.Level) - .OrderBy(x => x.SortOrder); - } + sql = InsertJoins(sql, joins); - protected override Sql GetBaseQuery(QueryType queryType) - { - return GetBaseQuery(queryType, true); + // see notes in ApplyOrdering: the field MUST be selected + aliased, and we cannot have + // the whole CASE fragment in ORDER BY due to it not being detected by NPoco + var sqlText = InsertBefore(sql.SQL, "FROM", + + // when invariant, ie 'variations' does not have the culture flag (value 1), use the global 'published' flag on pcv.id, + // otherwise check if there's a version culture variation for the lang, via ccv.id + ", (CASE WHEN (ctype.variations & 1) = 0 THEN (CASE WHEN pcv.id IS NULL THEN 0 ELSE 1 END) ELSE (CASE WHEN ccvp.id IS NULL THEN 0 ELSE 1 END) END) AS ordering "); // trailing space is important! + + sql = Sql(sqlText, sql.Arguments); + + return "ordering"; } - // gets the COALESCE expression for variant/invariant name - private string VariantNameSqlExpression - => SqlContext.VisitDto((ccv, node) => ccv.Name ?? node.Text, "ccv").Sql; + return base.ApplySystemOrdering(ref sql, ordering); + } + + private IEnumerable MapDtosToContent(List dtos, + bool withCache = false, + bool loadProperties = true, + bool loadTemplates = true, + bool loadVariants = true) + { + var temps = new List>(); + var contentTypes = new Dictionary(); + var templateIds = new List(); - protected Sql GetBaseQuery(QueryType queryType, bool current) + var content = new Content[dtos.Count]; + + for (var i = 0; i < dtos.Count; i++) { - var sql = SqlContext.Sql(); + DocumentDto dto = dtos[i]; - switch (queryType) + if (withCache) { - case QueryType.Count: - sql = sql.SelectCount(); - break; - case QueryType.Ids: - sql = sql.Select(x => x.NodeId); - break; - case QueryType.Single: - case QueryType.Many: - // R# may flag this ambiguous and red-squiggle it, but it is not - sql = sql.Select(r => - r.Select(documentDto => documentDto.ContentDto, r1 => - r1.Select(contentDto => contentDto.NodeDto)) - .Select(documentDto => documentDto.DocumentVersionDto, r1 => - r1.Select(documentVersionDto => documentVersionDto.ContentVersionDto)) - .Select(documentDto => documentDto.PublishedVersionDto, "pdv", r1 => - r1.Select(documentVersionDto => documentVersionDto!.ContentVersionDto, "pcv"))) - - // select the variant name, coalesce to the invariant name, as "variantName" - .AndSelect(VariantNameSqlExpression + " AS variantName"); - break; + // if the cache contains the (proper version of the) item, use it + IContent? cached = + IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); + if (cached != null && cached.VersionId == dto.DocumentVersionDto.ContentVersionDto.Id) + { + content[i] = (Content)cached; + continue; + } } - sql - .From() - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - - // inner join on mandatory edited version - .InnerJoin() - .On((left, right) => left.NodeId == right.NodeId) - .InnerJoin() - .On((left, right) => left.Id == right.Id) + // else, need to build it - // left join on optional published version - .LeftJoin(nested => - nested.InnerJoin("pdv") - .On((left, right) => left.Id == right.Id && right.Published, "pcv", "pdv"), "pcv") - .On((left, right) => left.NodeId == right.NodeId, aliasRight: "pcv") + // get the content type - the repository is full cache *but* still deep-clones + // whatever comes out of it, so use our own local index here to avoid this + var contentTypeId = dto.ContentDto.ContentTypeId; + if (contentTypes.TryGetValue(contentTypeId, out IContentType? contentType) == false) + { + contentTypes[contentTypeId] = contentType = _contentTypeRepository.Get(contentTypeId); + } - // TODO: should we be joining this when the query type is not single/many? - // left join on optional culture variation - //the magic "[[[ISOCODE]]]" parameter value will be replaced in ContentRepositoryBase.GetPage() by the actual ISO code - .LeftJoin(nested => - nested.InnerJoin("lang").On((ccv, lang) => ccv.LanguageId == lang.Id && lang.IsoCode == "[[[ISOCODE]]]", "ccv", "lang"), "ccv") - .On((version, ccv) => version.Id == ccv.VersionId, aliasRight: "ccv"); + Content c = content[i] = ContentBaseFactory.BuildEntity(dto, contentType); - sql - .Where(x => x.NodeObjectType == NodeObjectTypeId); + if (loadTemplates) + { + // need templates + var templateId = dto.DocumentVersionDto.TemplateId; + if (templateId.HasValue) + { + templateIds.Add(templateId.Value); + } - // this would ensure we don't get the published version - keep for reference - //sql - // .WhereAny( - // x => x.Where((x1, x2) => x1.Id != x2.Id, alias2: "pcv"), - // x => x.WhereNull(x1 => x1.Id, "pcv") - // ); + if (dto.Published) + { + templateId = dto.PublishedVersionDto.TemplateId; + if (templateId.HasValue) + { + templateIds.Add(templateId.Value); + } + } + } - if (current) - sql.Where(x => x.Current); // always get the current version + // need temps, for properties, templates and variations + var versionId = dto.DocumentVersionDto.Id; + var publishedVersionId = dto.Published ? dto.PublishedVersionDto.Id : 0; + var temp = new TempContent(dto.NodeId, versionId, publishedVersionId, contentType, c) + { + Template1Id = dto.DocumentVersionDto.TemplateId + }; + if (dto.Published) + { + temp.Template2Id = dto.PublishedVersionDto.TemplateId; + } - return sql; + temps.Add(temp); } - protected override Sql GetBaseQuery(bool isCount) + Dictionary? templates = null; + if (loadTemplates) { - return GetBaseQuery(isCount ? QueryType.Count : QueryType.Single); + // load all required templates in 1 query, and index + templates = _templateRepository.GetMany(templateIds.ToArray())? + .ToDictionary(x => x.Id, x => x); } - // ah maybe not, that what's used for eg Exists in base repo - protected override string GetBaseWhereClause() + IDictionary? properties = null; + if (loadProperties) { - return $"{Cms.Core.Constants.DatabaseSchema.Tables.Node}.id = @id"; + // load all properties for all documents from database in 1 query - indexed by version id + properties = GetPropertyCollections(temps); } - protected override IEnumerable GetDeleteClauses() + // assign templates and properties + foreach (TempContent temp in temps) { - var list = new List + if (loadTemplates) { - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentSchedule + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.RedirectUrl + " WHERE contentKey IN (SELECT uniqueId FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Node + " WHERE id = @id)", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.User2NodeNotify + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.UserGroup2Node + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.UserGroup2NodePermission + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.UserStartNode + " WHERE startNode = @id", - "UPDATE " + Cms.Core.Constants.DatabaseSchema.Tables.UserGroup + " SET startContentId = NULL WHERE startContentId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Relation + " WHERE parentId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Relation + " WHERE childId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.TagRelationship + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Domain + " WHERE domainRootStructureID = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Document + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.DocumentCultureVariation + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.DocumentVersion + " WHERE id IN (SELECT id FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.PropertyData + " WHERE versionId IN (SELECT id FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentVersionCultureVariation + " WHERE versionId IN (SELECT id FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Content + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.AccessRule + " WHERE accessId IN (SELECT id FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Access + " WHERE nodeId = @id OR loginNodeId = @id OR noAccessNodeId = @id)", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Access + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Access + " WHERE loginNodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Access + " WHERE noAccessNodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Node + " WHERE id = @id" - }; - return list; - } - - #endregion + // set the template ID if it matches an existing template + if (temp.Template1Id.HasValue && (templates?.ContainsKey(temp.Template1Id.Value) ?? false)) + { + temp.Content!.TemplateId = temp.Template1Id; + } - #region Versions + if (temp.Template2Id.HasValue && (templates?.ContainsKey(temp.Template2Id.Value) ?? false)) + { + temp.Content!.PublishTemplateId = temp.Template2Id; + } + } - public override IEnumerable GetAllVersions(int nodeId) - { - var sql = GetBaseQuery(QueryType.Many, false) - .Where(x => x.NodeId == nodeId) - .OrderByDescending(x => x.Current) - .AndByDescending(x => x.VersionDate); - return MapDtosToContent(Database.Fetch(sql), true); + // set properties + if (loadProperties) + { + if (properties?.ContainsKey(temp.VersionId) ?? false) + { + temp.Content!.Properties = properties[temp.VersionId]; + } + else + { + throw new InvalidOperationException($"No property data found for version: '{temp.VersionId}'."); + } + } } - // TODO: This method needs to return a readonly version of IContent! The content returned - // from this method does not contain all of the data required to re-persist it and if that - // is attempted some odd things will occur. - // Either we create an IContentReadOnly (which ultimately we should for vNext so we can - // differentiate between methods that return entities that can be re-persisted or not), or - // in the meantime to not break API compatibility, we can add a property to IContentBase - // (or go further and have it on IUmbracoEntity): "IsReadOnly" and if that is true we throw - // an exception if that entity is passed to a Save method. - // Ideally we return "Slim" versions of content for all sorts of methods here and in ContentService. - // Perhaps another non-breaking alternative is to have new services like IContentServiceReadOnly - // which can return IContentReadOnly. - // We have the ability with `MapDtosToContent` to reduce the amount of data looked up for a - // content item. Ideally for paged data that populates list views, these would be ultra slim - // content items, there's no reason to populate those with really anything apart from property data, - // but until we do something like the above, we can't do that since it would be breaking and unclear. - public override IEnumerable GetAllVersionsSlim(int nodeId, int skip, int take) + if (loadVariants) { - var sql = GetBaseQuery(QueryType.Many, false) - .Where(x => x.NodeId == nodeId) - .OrderByDescending(x => x.Current) - .AndByDescending(x => x.VersionDate); + // set variations, if varying + temps = temps.Where(x => x.ContentType?.VariesByCulture() ?? false).ToList(); + if (temps.Count > 0) + { + // load all variations for all documents from database, in one query + IDictionary> contentVariations = GetContentVariations(temps); + IDictionary> documentVariations = GetDocumentVariations(temps); + foreach (TempContent temp in temps) + { + SetVariations(temp.Content, contentVariations, documentVariations); + } + } + } - var pageIndex = skip / take; - return MapDtosToContent(Database.Page(pageIndex+1, take, sql).Items, true, - // load bare minimum, need variants though since this is used to rollback with variants - false, false, true); + foreach (Content c in content) + { + c.ResetDirtyProperties(false); // reset dirty initial properties (U4-1946) } - public override IContent? GetVersion(int versionId) + return content; + } + + private IContent MapDtoToContent(DocumentDto dto) + { + IContentType? contentType = _contentTypeRepository.Get(dto.ContentDto.ContentTypeId); + Content content = ContentBaseFactory.BuildEntity(dto, contentType); + + try { - var sql = GetBaseQuery(QueryType.Single, false) - .Where(x => x.Id == versionId); + content.DisableChangeTracking(); - var dto = Database.Fetch(sql).FirstOrDefault(); - return dto == null ? null : MapDtoToContent(dto); - } + // get template + if (dto.DocumentVersionDto.TemplateId.HasValue) + { + content.TemplateId = dto.DocumentVersionDto.TemplateId; + } + + // get properties - indexed by version id + var versionId = dto.DocumentVersionDto.Id; + + // TODO: shall we get published properties or not? + //var publishedVersionId = dto.Published ? dto.PublishedVersionDto.Id : 0; + var publishedVersionId = dto.PublishedVersionDto?.Id ?? 0; + + var temp = new TempContent(dto.NodeId, versionId, publishedVersionId, contentType); + var ltemp = new List> {temp}; + IDictionary properties = GetPropertyCollections(ltemp); + content.Properties = properties[dto.DocumentVersionDto.Id]; - // deletes a specific version - public override void DeleteVersion(int versionId) + // set variations, if varying + if (contentType?.VariesByCulture() ?? false) + { + IDictionary> contentVariations = GetContentVariations(ltemp); + IDictionary> documentVariations = GetDocumentVariations(ltemp); + SetVariations(content, contentVariations, documentVariations); + } + + // reset dirty initial properties (U4-1946) + content.ResetDirtyProperties(false); + return content; + } + finally { - // TODO: test object node type? - - // get the version we want to delete - var template = SqlContext.Templates.Get("Umbraco.Core.DocumentRepository.GetVersion", tsql => - tsql.Select() - .AndSelect() - .From() - .InnerJoin() - .On((c, d) => c.Id == d.Id) - .Where(x => x.Id == SqlTemplate.Arg("versionId")) - ); - var versionDto = Database.Fetch(template.Sql(new { versionId })).FirstOrDefault(); - - // nothing to delete - if (versionDto == null) - return; - - // don't delete the current or published version - if (versionDto.ContentVersionDto.Current) - throw new InvalidOperationException("Cannot delete the current version."); - else if (versionDto.Published) - throw new InvalidOperationException("Cannot delete the published version."); - - PerformDeleteVersion(versionDto.ContentVersionDto.NodeId, versionId); + content.EnableChangeTracking(); } + } - // deletes all versions of an entity, older than a date. - public override void DeleteVersions(int nodeId, DateTime versionDate) + private void SetVariations(Content? content, IDictionary> contentVariations, + IDictionary> documentVariations) + { + if (content is null) { - // TODO: test object node type? - - // get the versions we want to delete, excluding the current one - var template = SqlContext.Templates.Get("Umbraco.Core.DocumentRepository.GetVersions", tsql => - tsql.Select() - .From() - .InnerJoin() - .On((c, d) => c.Id == d.Id) - .Where(x => x.NodeId == SqlTemplate.Arg("nodeId") && !x.Current && x.VersionDate < SqlTemplate.Arg("versionDate")) - .Where(x => !x.Published) - ); - var versionDtos = Database.Fetch(template.Sql(new { nodeId, versionDate })); - foreach (var versionDto in versionDtos) - PerformDeleteVersion(versionDto.NodeId, versionDto.Id); + return; } - protected override void PerformDeleteVersion(int id, int versionId) + if (contentVariations.TryGetValue(content.VersionId, out List? contentVariation)) { - Database.Delete("WHERE versionId = @versionId", new { versionId }); - Database.Delete("WHERE versionId = @versionId", new { versionId }); - Database.Delete("WHERE id = @versionId", new { versionId }); - Database.Delete("WHERE id = @versionId", new { versionId }); + foreach (ContentVariation v in contentVariation) + { + content.SetCultureInfo(v.Culture, v.Name, v.Date); + } } - #endregion - - #region Persist + if (content.PublishedVersionId > 0 && + contentVariations.TryGetValue(content.PublishedVersionId, out contentVariation)) + { + foreach (ContentVariation v in contentVariation) + { + content.SetPublishInfo(v.Culture, v.Name, v.Date); + } + } - protected override void PersistNewItem(IContent entity) + if (documentVariations.TryGetValue(content.Id, out List? documentVariation)) { - entity.AddingEntity(); + content.SetCultureEdited(documentVariation.Where(x => x.Edited).Select(x => x.Culture)); + } + } - var publishing = entity.PublishedState == PublishedState.Publishing; + private IDictionary> GetContentVariations(List> temps) + where T : class, IContentBase + { + var versions = new List(); + foreach (TempContent temp in temps) + { + versions.Add(temp.VersionId); + if (temp.PublishedVersionId > 0) + { + versions.Add(temp.PublishedVersionId); + } + } - // ensure that the default template is assigned - if (entity.TemplateId.HasValue == false) - entity.TemplateId = entity.ContentType.DefaultTemplate?.Id; + if (versions.Count == 0) + { + return new Dictionary>(); + } - // sanitize names - SanitizeNames(entity, publishing); + IEnumerable dtos = + Database.FetchByGroups(versions, Constants.Sql.MaxParameterCount, + batch + => Sql() + .Select() + .From() + .WhereIn(x => x.VersionId, batch)); - // ensure that strings don't contain characters that are invalid in xml - // TODO: do we really want to keep doing this here? - entity.SanitizeEntityPropertiesForXmlStorage(); + var variations = new Dictionary>(); - // create the dto - var dto = ContentBaseFactory.BuildDto(entity, NodeObjectTypeId); + foreach (ContentVersionCultureVariationDto dto in dtos) + { + if (!variations.TryGetValue(dto.VersionId, out List? variation)) + { + variations[dto.VersionId] = variation = new List(); + } - // derive path and level from parent - var parent = GetParentNodeDto(entity.ParentId); - var level = parent.Level + 1; + variation.Add(new ContentVariation + { + Culture = LanguageRepository.GetIsoCodeById(dto.LanguageId), Name = dto.Name, Date = dto.UpdateDate + }); + } - // get sort order - var sortOrder = GetNewChildSortOrder(entity.ParentId, 0); + return variations; + } - // persist the node dto - var nodeDto = dto.ContentDto.NodeDto; - nodeDto.Path = parent.Path; - nodeDto.Level = Convert.ToInt16(level); - nodeDto.SortOrder = sortOrder; + private IDictionary> GetDocumentVariations(List> temps) + where T : class, IContentBase + { + IEnumerable ids = temps.Select(x => x.Id); - // see if there's a reserved identifier for this unique id - // and then either update or insert the node dto - var id = GetReservedId(nodeDto.UniqueId); - if (id > 0) - nodeDto.NodeId = id; - else - Database.Insert(nodeDto); - - nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); - nodeDto.ValidatePathWithException(); - Database.Update(nodeDto); - - // update entity - entity.Id = nodeDto.NodeId; - entity.Path = nodeDto.Path; - entity.SortOrder = sortOrder; - entity.Level = level; - - // persist the content dto - var contentDto = dto.ContentDto; - contentDto.NodeId = nodeDto.NodeId; - Database.Insert(contentDto); - - // persist the content version dto - var contentVersionDto = dto.DocumentVersionDto.ContentVersionDto; - contentVersionDto.NodeId = nodeDto.NodeId; - contentVersionDto.Current = !publishing; - Database.Insert(contentVersionDto); - entity.VersionId = contentVersionDto.Id; + IEnumerable dtos = Database.FetchByGroups(ids, + Constants.Sql.MaxParameterCount, batch => + Sql() + .Select() + .From() + .WhereIn(x => x.NodeId, batch)); - // persist the document version dto - var documentVersionDto = dto.DocumentVersionDto; - documentVersionDto.Id = entity.VersionId; - if (publishing) - documentVersionDto.Published = true; - Database.Insert(documentVersionDto); + var variations = new Dictionary>(); - // and again in case we're publishing immediately - if (publishing) + foreach (DocumentCultureVariationDto dto in dtos) + { + if (!variations.TryGetValue(dto.NodeId, out List? variation)) { - entity.PublishedVersionId = entity.VersionId; - contentVersionDto.Id = 0; - contentVersionDto.Current = true; - contentVersionDto.Text = entity.Name; - Database.Insert(contentVersionDto); - entity.VersionId = contentVersionDto.Id; - - documentVersionDto.Id = entity.VersionId; - documentVersionDto.Published = false; - Database.Insert(documentVersionDto); + variations[dto.NodeId] = variation = new List(); } - // persist the property data - IEnumerable propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, entity.PublishedVersionId, entity.Properties, LanguageRepository, out var edited, out HashSet? editedCultures); - foreach (PropertyDataDto propertyDataDto in propertyDataDtos) + variation.Add(new DocumentVariation { - Database.Insert(propertyDataDto); - } + Culture = LanguageRepository.GetIsoCodeById(dto.LanguageId), Edited = dto.Edited + }); + } - // if !publishing, we may have a new name != current publish name, - // also impacts 'edited' - if (!publishing && entity.PublishName != entity.Name) - { - edited = true; - } + return variations; + } - // persist the document dto - // at that point, when publishing, the entity still has its old Published value - // so we need to explicitly update the dto to persist the correct value - if (entity.PublishedState == PublishedState.Publishing) + private IEnumerable GetContentVariationDtos(IContent content, bool publishing) + { + if (content.CultureInfos is not null) + { + // create dtos for the 'current' (non-published) version, all cultures + // ReSharper disable once UseDeconstruction + foreach (ContentCultureInfos cultureInfo in content.CultureInfos) { - dto.Published = true; + yield return new ContentVersionCultureVariationDto + { + VersionId = content.VersionId, + LanguageId = + LanguageRepository.GetIdByIsoCode(cultureInfo.Culture) ?? + throw new InvalidOperationException("Not a valid culture."), + Culture = cultureInfo.Culture, + Name = cultureInfo.Name, + UpdateDate = + content.GetUpdateDate(cultureInfo.Culture) ?? DateTime.MinValue // we *know* there is a value + }; } + } - dto.NodeId = nodeDto.NodeId; - entity.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited - Database.Insert(dto); + // if not publishing, we're just updating the 'current' (non-published) version, + // so there are no DTOs to create for the 'published' version which remains unchanged + if (!publishing) + { + yield break; + } - // persist the variations - if (entity.ContentType.VariesByCulture()) + if (content.PublishCultureInfos is not null) + { + // create dtos for the 'published' version, for published cultures (those having a name) + // ReSharper disable once UseDeconstruction + foreach (ContentCultureInfos cultureInfo in content.PublishCultureInfos) { - // names also impact 'edited' - // ReSharper disable once UseDeconstruction - foreach (ContentCultureInfos cultureInfo in entity.CultureInfos!) + yield return new ContentVersionCultureVariationDto { - if (cultureInfo.Name != entity.GetPublishName(cultureInfo.Culture)) - { - (editedCultures ??= new HashSet(StringComparer.OrdinalIgnoreCase)).Add(cultureInfo.Culture); - } - } - - // refresh content - entity.SetCultureEdited(editedCultures!); + VersionId = content.PublishedVersionId, + LanguageId = + LanguageRepository.GetIdByIsoCode(cultureInfo.Culture) ?? + throw new InvalidOperationException("Not a valid culture."), + Culture = cultureInfo.Culture, + Name = cultureInfo.Name, + UpdateDate = + content.GetPublishDate(cultureInfo.Culture) ?? DateTime.MinValue // we *know* there is a value + }; + } + } + } - // bump dates to align cultures to version - entity.AdjustDates(contentVersionDto.VersionDate, publishing); + private IEnumerable GetDocumentVariationDtos(IContent content, + HashSet editedCultures) + { + IEnumerable + allCultures = content.AvailableCultures.Union(content.PublishedCultures); // union = distinct + foreach (var culture in allCultures) + { + var dto = new DocumentCultureVariationDto + { + NodeId = content.Id, + LanguageId = + LanguageRepository.GetIdByIsoCode(culture) ?? + throw new InvalidOperationException("Not a valid culture."), + Culture = culture, + Name = content.GetCultureName(culture) ?? content.GetPublishName(culture), + Available = content.IsCultureAvailable(culture), + Published = content.IsCulturePublished(culture), + // note: can't use IsCultureEdited at that point - hasn't been updated yet - see PersistUpdatedItem + Edited = content.IsCultureAvailable(culture) && + (!content.IsCulturePublished(culture) || + (editedCultures != null && editedCultures.Contains(culture))) + }; - // insert content variations - Database.BulkInsertRecords(GetContentVariationDtos(entity, publishing)); + yield return dto; + } + } - // insert document variations - Database.BulkInsertRecords(GetDocumentVariationDtos(entity, editedCultures!)); - } + private class ContentVariation + { + public string? Culture { get; set; } + public string? Name { get; set; } + public DateTime Date { get; set; } + } - // trigger here, before we reset Published etc - OnUowRefreshedEntity(new ContentRefreshNotification(entity, new EventMessages())); + private class DocumentVariation + { + public string? Culture { get; set; } + public bool Edited { get; set; } + } - // flip the entity's published property - // this also flips its published state - // note: what depends on variations (eg PublishNames) is managed directly by the content - if (entity.PublishedState == PublishedState.Publishing) - { - entity.Published = true; - entity.PublishTemplateId = entity.TemplateId; - entity.PublisherId = entity.WriterId; - entity.PublishName = entity.Name; - entity.PublishDate = entity.UpdateDate; + #region Repository Base - SetEntityTags(entity, _tagRepository, _serializer); - } - else if (entity.PublishedState == PublishedState.Unpublishing) - { - entity.Published = false; - entity.PublishTemplateId = null; - entity.PublisherId = null; - entity.PublishName = null; - entity.PublishDate = null; + protected override Guid NodeObjectTypeId => Constants.ObjectTypes.Document; - ClearEntityTags(entity, _tagRepository); - } + protected override IContent? PerformGet(int id) + { + Sql sql = GetBaseQuery(QueryType.Single) + .Where(x => x.NodeId == id) + .SelectTop(1); + + DocumentDto? dto = Database.Fetch(sql).FirstOrDefault(); + return dto == null + ? null + : MapDtoToContent(dto); + } - PersistRelations(entity); + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = GetBaseQuery(QueryType.Many); - entity.ResetDirtyProperties(); - - // troubleshooting - //if (Database.ExecuteScalar($"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.DocumentVersion} JOIN {Constants.DatabaseSchema.Tables.ContentVersion} ON {Constants.DatabaseSchema.Tables.DocumentVersion}.id={Constants.DatabaseSchema.Tables.ContentVersion}.id WHERE published=1 AND nodeId=" + content.Id) > 1) - //{ - // Debugger.Break(); - // throw new Exception("oops"); - //} - //if (Database.ExecuteScalar($"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.DocumentVersion} JOIN {Constants.DatabaseSchema.Tables.ContentVersion} ON {Constants.DatabaseSchema.Tables.DocumentVersion}.id={Constants.DatabaseSchema.Tables.ContentVersion}.id WHERE [current]=1 AND nodeId=" + content.Id) > 1) - //{ - // Debugger.Break(); - // throw new Exception("oops"); - //} + if (ids?.Any() ?? false) + { + sql.WhereIn(x => x.NodeId, ids); } - protected override void PersistUpdatedItem(IContent entity) - { - var isEntityDirty = entity.IsDirty(); - var editedSnapshot = entity.Edited; + return MapDtosToContent(Database.Fetch(sql)); + } - // check if we need to make any database changes at all - if ((entity.PublishedState == PublishedState.Published || entity.PublishedState == PublishedState.Unpublished) - && !isEntityDirty && !entity.IsAnyUserPropertyDirty()) - { - return; // no change to save, do nothing, don't even update dates - } + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(QueryType.Many); - // whatever we do, we must check that we are saving the current version - var version = Database.Fetch(SqlContext.Sql().Select().From().Where(x => x.Id == entity.VersionId)).FirstOrDefault(); - if (version == null || !version.Current) - throw new InvalidOperationException("Cannot save a non-current version."); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); - // update - entity.UpdatingEntity(); + AddGetByQueryOrderBy(sql); - // Check if this entity is being moved as a descendant as part of a bulk moving operations. - // In this case we can bypass a lot of the below operations which will make this whole operation go much faster. - // When moving we don't need to create new versions, etc... because we cannot roll this operation back anyways. - var isMoving = entity.IsMoving(); - // TODO: I'm sure we can also detect a "Copy" (of a descendant) operation and probably perform similar checks below. - // There is probably more stuff that would be required for copying but I'm sure not all of this logic would be, we could more than likely boost - // copy performance by 95% just like we did for Move + return MapDtosToContent(Database.Fetch(sql)); + } + private void AddGetByQueryOrderBy(Sql sql) => + sql + .OrderBy(x => x.Level) + .OrderBy(x => x.SortOrder); - var publishing = entity.PublishedState == PublishedState.Publishing; + protected override Sql GetBaseQuery(QueryType queryType) => GetBaseQuery(queryType, true); - if (!isMoving) - { - // check if we need to create a new version - if (publishing && entity.PublishedVersionId > 0) - { - // published version is not published anymore - Database.Execute(Sql().Update(u => u.Set(x => x.Published, false)).Where(x => x.Id == entity.PublishedVersionId)); - } + // gets the COALESCE expression for variant/invariant name + private string VariantNameSqlExpression + => SqlContext.VisitDto((ccv, node) => ccv.Name ?? node.Text, "ccv") + .Sql; - // sanitize names - SanitizeNames(entity, publishing); + protected Sql GetBaseQuery(QueryType queryType, bool current) + { + Sql sql = SqlContext.Sql(); - // ensure that strings don't contain characters that are invalid in xml - // TODO: do we really want to keep doing this here? - entity.SanitizeEntityPropertiesForXmlStorage(); + switch (queryType) + { + case QueryType.Count: + sql = sql.SelectCount(); + break; + case QueryType.Ids: + sql = sql.Select(x => x.NodeId); + break; + case QueryType.Single: + case QueryType.Many: + // R# may flag this ambiguous and red-squiggle it, but it is not + sql = sql.Select(r => + r.Select(documentDto => documentDto.ContentDto, r1 => + r1.Select(contentDto => contentDto.NodeDto)) + .Select(documentDto => documentDto.DocumentVersionDto, r1 => + r1.Select(documentVersionDto => documentVersionDto.ContentVersionDto)) + .Select(documentDto => documentDto.PublishedVersionDto, "pdv", r1 => + r1.Select(documentVersionDto => documentVersionDto!.ContentVersionDto, "pcv"))) + + // select the variant name, coalesce to the invariant name, as "variantName" + .AndSelect(VariantNameSqlExpression + " AS variantName"); + break; + } - // if parent has changed, get path, level and sort order - if (entity.IsPropertyDirty("ParentId")) - { - var parent = GetParentNodeDto(entity.ParentId); - entity.Path = string.Concat(parent.Path, ",", entity.Id); - entity.Level = parent.Level + 1; - entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0); - } - } + sql + .From() + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + + // inner join on mandatory edited version + .InnerJoin() + .On((left, right) => left.NodeId == right.NodeId) + .InnerJoin() + .On((left, right) => left.Id == right.Id) + + // left join on optional published version + .LeftJoin(nested => + nested.InnerJoin("pdv") + .On((left, right) => left.Id == right.Id && right.Published, + "pcv", "pdv"), "pcv") + .On((left, right) => left.NodeId == right.NodeId, aliasRight: "pcv") + + // TODO: should we be joining this when the query type is not single/many? + // left join on optional culture variation + //the magic "[[[ISOCODE]]]" parameter value will be replaced in ContentRepositoryBase.GetPage() by the actual ISO code + .LeftJoin(nested => + nested.InnerJoin("lang").On( + (ccv, lang) => ccv.LanguageId == lang.Id && lang.IsoCode == "[[[ISOCODE]]]", "ccv", "lang"), "ccv") + .On((version, ccv) => version.Id == ccv.VersionId, + aliasRight: "ccv"); + + sql + .Where(x => x.NodeObjectType == NodeObjectTypeId); + + // this would ensure we don't get the published version - keep for reference + //sql + // .WhereAny( + // x => x.Where((x1, x2) => x1.Id != x2.Id, alias2: "pcv"), + // x => x.WhereNull(x1 => x1.Id, "pcv") + // ); + + if (current) + { + sql.Where(x => x.Current); // always get the current version + } - // create the dto - var dto = ContentBaseFactory.BuildDto(entity, NodeObjectTypeId); + return sql; + } - // update the node dto - var nodeDto = dto.ContentDto.NodeDto; - nodeDto.ValidatePathWithException(); - Database.Update(nodeDto); + protected override Sql GetBaseQuery(bool isCount) => + GetBaseQuery(isCount ? QueryType.Count : QueryType.Single); - if (!isMoving) - { - // update the content dto - Database.Update(dto.ContentDto); + // ah maybe not, that what's used for eg Exists in base repo + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Node}.id = @id"; - // update the content & document version dtos - var contentVersionDto = dto.DocumentVersionDto.ContentVersionDto; - var documentVersionDto = dto.DocumentVersionDto; - if (publishing) - { - documentVersionDto.Published = true; // now published - contentVersionDto.Current = false; // no more current - } + protected override IEnumerable GetDeleteClauses() + { + var list = new List + { + "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentSchedule + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.RedirectUrl + + " WHERE contentKey IN (SELECT uniqueId FROM " + Constants.DatabaseSchema.Tables.Node + + " WHERE id = @id)", + "DELETE FROM " + Constants.DatabaseSchema.Tables.User2NodeNotify + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.UserGroup2Node + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.UserGroup2NodePermission + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.UserStartNode + " WHERE startNode = @id", + "UPDATE " + Constants.DatabaseSchema.Tables.UserGroup + + " SET startContentId = NULL WHERE startContentId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Relation + " WHERE parentId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Relation + " WHERE childId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.TagRelationship + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Domain + " WHERE domainRootStructureID = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Document + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.DocumentCultureVariation + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.DocumentVersion + " WHERE id IN (SELECT id FROM " + + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", + "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyData + " WHERE versionId IN (SELECT id FROM " + + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", + "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentVersionCultureVariation + + " WHERE versionId IN (SELECT id FROM " + Constants.DatabaseSchema.Tables.ContentVersion + + " WHERE nodeId = @id)", + "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Content + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.AccessRule + " WHERE accessId IN (SELECT id FROM " + + Constants.DatabaseSchema.Tables.Access + + " WHERE nodeId = @id OR loginNodeId = @id OR noAccessNodeId = @id)", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Access + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Access + " WHERE loginNodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Access + " WHERE noAccessNodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Node + " WHERE id = @id" + }; + return list; + } - // Ensure existing version retains current preventCleanup flag (both saving and publishing). - contentVersionDto.PreventCleanup = version.PreventCleanup; + #endregion - Database.Update(contentVersionDto); - Database.Update(documentVersionDto); + #region Versions - // and, if publishing, insert new content & document version dtos - if (publishing) - { - entity.PublishedVersionId = entity.VersionId; + public override IEnumerable GetAllVersions(int nodeId) + { + Sql sql = GetBaseQuery(QueryType.Many, false) + .Where(x => x.NodeId == nodeId) + .OrderByDescending(x => x.Current) + .AndByDescending(x => x.VersionDate); - contentVersionDto.Id = 0; // want a new id - contentVersionDto.Current = true; // current version - contentVersionDto.Text = entity.Name; - contentVersionDto.PreventCleanup = false; // new draft version disregards prevent cleanup flag - Database.Insert(contentVersionDto); - entity.VersionId = documentVersionDto.Id = contentVersionDto.Id; // get the new id + return MapDtosToContent(Database.Fetch(sql), true); + } - documentVersionDto.Published = false; // non-published version - Database.Insert(documentVersionDto); - } + // TODO: This method needs to return a readonly version of IContent! The content returned + // from this method does not contain all of the data required to re-persist it and if that + // is attempted some odd things will occur. + // Either we create an IContentReadOnly (which ultimately we should for vNext so we can + // differentiate between methods that return entities that can be re-persisted or not), or + // in the meantime to not break API compatibility, we can add a property to IContentBase + // (or go further and have it on IUmbracoEntity): "IsReadOnly" and if that is true we throw + // an exception if that entity is passed to a Save method. + // Ideally we return "Slim" versions of content for all sorts of methods here and in ContentService. + // Perhaps another non-breaking alternative is to have new services like IContentServiceReadOnly + // which can return IContentReadOnly. + // We have the ability with `MapDtosToContent` to reduce the amount of data looked up for a + // content item. Ideally for paged data that populates list views, these would be ultra slim + // content items, there's no reason to populate those with really anything apart from property data, + // but until we do something like the above, we can't do that since it would be breaking and unclear. + public override IEnumerable GetAllVersionsSlim(int nodeId, int skip, int take) + { + Sql sql = GetBaseQuery(QueryType.Many, false) + .Where(x => x.NodeId == nodeId) + .OrderByDescending(x => x.Current) + .AndByDescending(x => x.VersionDate); - // replace the property data (rather than updating) - // only need to delete for the version that existed, the new version (if any) has no property data yet - var versionToDelete = publishing ? entity.PublishedVersionId : entity.VersionId; + var pageIndex = skip / take; - // insert property data - ReplacePropertyValues(entity, versionToDelete, publishing ? entity.PublishedVersionId : 0, out var edited, out HashSet? editedCultures); + return MapDtosToContent(Database.Page(pageIndex + 1, take, sql).Items, true, + // load bare minimum, need variants though since this is used to rollback with variants + false, false); + } - // if !publishing, we may have a new name != current publish name, - // also impacts 'edited' - if (!publishing && entity.PublishName != entity.Name) - { - edited = true; - } + public override IContent? GetVersion(int versionId) + { + Sql sql = GetBaseQuery(QueryType.Single, false) + .Where(x => x.Id == versionId); - // To establish the new value of "edited" we compare all properties publishedValue to editedValue and look - // for differences. - // - // If we SaveAndPublish but the publish fails (e.g. already scheduled for release) - // we have lost the publishedValue on IContent (in memory vs database) so we cannot correctly make that comparison. - // - // This is a slight change to behaviour, historically a publish, followed by change & save, followed by undo change & save - // would change edited back to false. - if (!publishing && editedSnapshot) - { - edited = true; - } + DocumentDto? dto = Database.Fetch(sql).FirstOrDefault(); + return dto == null ? null : MapDtoToContent(dto); + } - if (entity.ContentType.VariesByCulture()) - { - // names also impact 'edited' - // ReSharper disable once UseDeconstruction - foreach (var cultureInfo in entity.CultureInfos!) - { - if (cultureInfo.Name != entity.GetPublishName(cultureInfo.Culture)) - { - edited = true; - (editedCultures ??= new HashSet(StringComparer.OrdinalIgnoreCase)).Add(cultureInfo.Culture); - - // TODO: change tracking - // at the moment, we don't do any dirty tracking on property values, so we don't know whether the - // culture has just been edited or not, so we don't update its update date - that date only changes - // when the name is set, and it all works because the controller does it - but, if someone uses a - // service to change a property value and save (without setting name), the update date does not change. - } - } + // deletes a specific version + public override void DeleteVersion(int versionId) + { + // TODO: test object node type? - // refresh content - entity.SetCultureEdited(editedCultures!); + // get the version we want to delete + SqlTemplate template = SqlContext.Templates.Get("Umbraco.Core.DocumentRepository.GetVersion", tsql => + tsql.Select() + .AndSelect() + .From() + .InnerJoin() + .On((c, d) => c.Id == d.Id) + .Where(x => x.Id == SqlTemplate.Arg("versionId")) + ); + DocumentVersionDto? versionDto = + Database.Fetch(template.Sql(new {versionId})).FirstOrDefault(); + + // nothing to delete + if (versionDto == null) + { + return; + } - // bump dates to align cultures to version - entity.AdjustDates(contentVersionDto.VersionDate, publishing); + // don't delete the current or published version + if (versionDto.ContentVersionDto.Current) + { + throw new InvalidOperationException("Cannot delete the current version."); + } - // replace the content version variations (rather than updating) - // only need to delete for the version that existed, the new version (if any) has no property data yet - var deleteContentVariations = Sql().Delete().Where(x => x.VersionId == versionToDelete); - Database.Execute(deleteContentVariations); + if (versionDto.Published) + { + throw new InvalidOperationException("Cannot delete the published version."); + } - // replace the document version variations (rather than updating) - var deleteDocumentVariations = Sql().Delete().Where(x => x.NodeId == entity.Id); - Database.Execute(deleteDocumentVariations); + PerformDeleteVersion(versionDto.ContentVersionDto.NodeId, versionId); + } - // TODO: NPoco InsertBulk issue? - // we should use the native NPoco InsertBulk here but it causes problems (not sure exactly all scenarios) - // but by using SQL Server and updating a variants name will cause: Unable to cast object of type - // 'Umbraco.Core.Persistence.FaultHandling.RetryDbConnection' to type 'System.Data.SqlClient.SqlConnection'. - // (same in PersistNewItem above) + // deletes all versions of an entity, older than a date. + public override void DeleteVersions(int nodeId, DateTime versionDate) + { + // TODO: test object node type? - // insert content variations - Database.BulkInsertRecords(GetContentVariationDtos(entity, publishing)); + // get the versions we want to delete, excluding the current one + SqlTemplate template = SqlContext.Templates.Get("Umbraco.Core.DocumentRepository.GetVersions", tsql => + tsql.Select() + .From() + .InnerJoin() + .On((c, d) => c.Id == d.Id) + .Where(x => + x.NodeId == SqlTemplate.Arg("nodeId") && !x.Current && + x.VersionDate < SqlTemplate.Arg("versionDate")) + .Where(x => !x.Published) + ); + List? versionDtos = + Database.Fetch(template.Sql(new {nodeId, versionDate})); + foreach (ContentVersionDto? versionDto in versionDtos) + { + PerformDeleteVersion(versionDto.NodeId, versionDto.Id); + } + } - // insert document variations - Database.BulkInsertRecords(GetDocumentVariationDtos(entity, editedCultures!)); - } + protected override void PerformDeleteVersion(int id, int versionId) + { + Database.Delete("WHERE versionId = @versionId", new {versionId}); + Database.Delete("WHERE versionId = @versionId", new {versionId}); + Database.Delete("WHERE id = @versionId", new {versionId}); + Database.Delete("WHERE id = @versionId", new {versionId}); + } - // update the document dto - // at that point, when un/publishing, the entity still has its old Published value - // so we need to explicitly update the dto to persist the correct value - if (entity.PublishedState == PublishedState.Publishing) - { - dto.Published = true; - } - else if (entity.PublishedState == PublishedState.Unpublishing) - { - dto.Published = false; - } + #endregion - entity.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited - Database.Update(dto); + #region Persist - // if entity is publishing, update tags, else leave tags there - // means that implicitly unpublished, or trashed, entities *still* have tags in db - if (entity.PublishedState == PublishedState.Publishing) - { - SetEntityTags(entity, _tagRepository, _serializer); - } - } + protected override void PersistNewItem(IContent entity) + { + entity.AddingEntity(); - // trigger here, before we reset Published etc - OnUowRefreshedEntity(new ContentRefreshNotification(entity, new EventMessages())); + var publishing = entity.PublishedState == PublishedState.Publishing; - if (!isMoving) - { - // flip the entity's published property - // this also flips its published state - if (entity.PublishedState == PublishedState.Publishing) - { - entity.Published = true; - entity.PublishTemplateId = entity.TemplateId; - entity.PublisherId = entity.WriterId; - entity.PublishName = entity.Name; - entity.PublishDate = entity.UpdateDate; + // ensure that the default template is assigned + if (entity.TemplateId.HasValue == false) + { + entity.TemplateId = entity.ContentType.DefaultTemplate?.Id; + } - SetEntityTags(entity, _tagRepository, _serializer); - } - else if (entity.PublishedState == PublishedState.Unpublishing) - { - entity.Published = false; - entity.PublishTemplateId = null; - entity.PublisherId = null; - entity.PublishName = null; - entity.PublishDate = null; + // sanitize names + SanitizeNames(entity, publishing); - ClearEntityTags(entity, _tagRepository); - } + // ensure that strings don't contain characters that are invalid in xml + // TODO: do we really want to keep doing this here? + entity.SanitizeEntityPropertiesForXmlStorage(); - PersistRelations(entity); + // create the dto + DocumentDto dto = ContentBaseFactory.BuildDto(entity, NodeObjectTypeId); - // TODO: note re. tags: explicitly unpublished entities have cleared tags, but masked or trashed entities *still* have tags in the db - so what? - } + // derive path and level from parent + NodeDto parent = GetParentNodeDto(entity.ParentId); + var level = parent.Level + 1; + + // get sort order + var sortOrder = GetNewChildSortOrder(entity.ParentId, 0); - entity.ResetDirtyProperties(); - - // troubleshooting - //if (Database.ExecuteScalar($"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.DocumentVersion} JOIN {Constants.DatabaseSchema.Tables.ContentVersion} ON {Constants.DatabaseSchema.Tables.DocumentVersion}.id={Constants.DatabaseSchema.Tables.ContentVersion}.id WHERE published=1 AND nodeId=" + content.Id) > 1) - //{ - // Debugger.Break(); - // throw new Exception("oops"); - //} - //if (Database.ExecuteScalar($"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.DocumentVersion} JOIN {Constants.DatabaseSchema.Tables.ContentVersion} ON {Constants.DatabaseSchema.Tables.DocumentVersion}.id={Constants.DatabaseSchema.Tables.ContentVersion}.id WHERE [current]=1 AND nodeId=" + content.Id) > 1) - //{ - // Debugger.Break(); - // throw new Exception("oops"); - //} + // persist the node dto + NodeDto nodeDto = dto.ContentDto.NodeDto; + nodeDto.Path = parent.Path; + nodeDto.Level = Convert.ToInt16(level); + nodeDto.SortOrder = sortOrder; + + // see if there's a reserved identifier for this unique id + // and then either update or insert the node dto + var id = GetReservedId(nodeDto.UniqueId); + if (id > 0) + { + nodeDto.NodeId = id; + } + else + { + Database.Insert(nodeDto); } - /// - public void PersistContentSchedule(IContent content, ContentScheduleCollection contentSchedule) + nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); + nodeDto.ValidatePathWithException(); + Database.Update(nodeDto); + + // update entity + entity.Id = nodeDto.NodeId; + entity.Path = nodeDto.Path; + entity.SortOrder = sortOrder; + entity.Level = level; + + // persist the content dto + ContentDto contentDto = dto.ContentDto; + contentDto.NodeId = nodeDto.NodeId; + Database.Insert(contentDto); + + // persist the content version dto + ContentVersionDto contentVersionDto = dto.DocumentVersionDto.ContentVersionDto; + contentVersionDto.NodeId = nodeDto.NodeId; + contentVersionDto.Current = !publishing; + Database.Insert(contentVersionDto); + entity.VersionId = contentVersionDto.Id; + + // persist the document version dto + DocumentVersionDto documentVersionDto = dto.DocumentVersionDto; + documentVersionDto.Id = entity.VersionId; + if (publishing) { - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } + documentVersionDto.Published = true; + } - if (contentSchedule == null) - { - throw new ArgumentNullException(nameof(contentSchedule)); - } + Database.Insert(documentVersionDto); - var schedules = ContentBaseFactory.BuildScheduleDto(content, contentSchedule, LanguageRepository).ToList(); + // and again in case we're publishing immediately + if (publishing) + { + entity.PublishedVersionId = entity.VersionId; + contentVersionDto.Id = 0; + contentVersionDto.Current = true; + contentVersionDto.Text = entity.Name; + Database.Insert(contentVersionDto); + entity.VersionId = contentVersionDto.Id; - //remove any that no longer exist - var ids = schedules.Where(x => x.Model.Id != Guid.Empty).Select(x => x.Model.Id).Distinct(); - Database.Execute(Sql() - .Delete() - .Where(x => x.NodeId == content.Id) - .WhereNotIn(x => x.Id, ids)); + documentVersionDto.Id = entity.VersionId; + documentVersionDto.Published = false; + Database.Insert(documentVersionDto); + } - //add/update the rest - foreach (var schedule in schedules) - { - if (schedule.Model.Id == Guid.Empty) - { - schedule.Model.Id = schedule.Dto.Id = Guid.NewGuid(); - Database.Insert(schedule.Dto); - } - else - { - Database.Update(schedule.Dto); - } - } + // persist the property data + IEnumerable propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, + entity.VersionId, entity.PublishedVersionId, entity.Properties, LanguageRepository, out var edited, + out HashSet? editedCultures); + foreach (PropertyDataDto propertyDataDto in propertyDataDtos) + { + Database.Insert(propertyDataDto); } - protected override void PersistDeletedItem(IContent entity) + // if !publishing, we may have a new name != current publish name, + // also impacts 'edited' + if (!publishing && entity.PublishName != entity.Name) { - // Raise event first else potential FK issues - OnUowRemovingEntity(entity); - - //We need to clear out all access rules but we need to do this in a manual way since - // nothing in that table is joined to a content id - var subQuery = SqlContext.Sql() - .Select(x => x.AccessId) - .From() - .InnerJoin() - .On(left => left.AccessId, right => right.Id) - .Where(dto => dto.NodeId == entity.Id); - Database.Execute(SqlContext.SqlSyntax.GetDeleteSubquery("umbracoAccessRule", "accessId", subQuery)); - - //now let the normal delete clauses take care of everything else - base.PersistDeletedItem(entity); + edited = true; } - #endregion + // persist the document dto + // at that point, when publishing, the entity still has its old Published value + // so we need to explicitly update the dto to persist the correct value + if (entity.PublishedState == PublishedState.Publishing) + { + dto.Published = true; + } - #region Content Repository + dto.NodeId = nodeDto.NodeId; + entity.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited + Database.Insert(dto); - public int CountPublished(string? contentTypeAlias = null) + // persist the variations + if (entity.ContentType.VariesByCulture()) { - var sql = SqlContext.Sql(); - if (contentTypeAlias.IsNullOrWhiteSpace()) - { - sql.SelectCount() - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId && x.Trashed == false) - .Where(x => x.Published); - } - else + // names also impact 'edited' + // ReSharper disable once UseDeconstruction + foreach (ContentCultureInfos cultureInfo in entity.CultureInfos!) { - sql.SelectCount() - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.ContentTypeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId && x.Trashed == false) - .Where(x => x.Alias == contentTypeAlias) - .Where(x => x.Published); + if (cultureInfo.Name != entity.GetPublishName(cultureInfo.Culture)) + { + (editedCultures ??= new HashSet(StringComparer.OrdinalIgnoreCase)).Add(cultureInfo.Culture); + } } - return Database.ExecuteScalar(sql); - } + // refresh content + entity.SetCultureEdited(editedCultures!); - public void ReplaceContentPermissions(EntityPermissionSet permissionSet) - { - PermissionRepository.ReplaceEntityPermissions(permissionSet); + // bump dates to align cultures to version + entity.AdjustDates(contentVersionDto.VersionDate, publishing); + + // insert content variations + Database.BulkInsertRecords(GetContentVariationDtos(entity, publishing)); + + // insert document variations + Database.BulkInsertRecords(GetDocumentVariationDtos(entity, editedCultures!)); } - /// - /// Assigns a single permission to the current content item for the specified group ids - /// - /// - /// - /// - public void AssignEntityPermission(IContent entity, char permission, IEnumerable groupIds) + // trigger here, before we reset Published etc + OnUowRefreshedEntity(new ContentRefreshNotification(entity, new EventMessages())); + + // flip the entity's published property + // this also flips its published state + // note: what depends on variations (eg PublishNames) is managed directly by the content + if (entity.PublishedState == PublishedState.Publishing) { - PermissionRepository.AssignEntityPermission(entity, permission, groupIds); - } + entity.Published = true; + entity.PublishTemplateId = entity.TemplateId; + entity.PublisherId = entity.WriterId; + entity.PublishName = entity.Name; + entity.PublishDate = entity.UpdateDate; - public EntityPermissionCollection GetPermissionsForEntity(int entityId) + SetEntityTags(entity, _tagRepository, _serializer); + } + else if (entity.PublishedState == PublishedState.Unpublishing) { - return PermissionRepository.GetPermissionsForEntity(entityId); + entity.Published = false; + entity.PublishTemplateId = null; + entity.PublisherId = null; + entity.PublishName = null; + entity.PublishDate = null; + + ClearEntityTags(entity, _tagRepository); } - /// - /// Used to add/update a permission for a content item - /// - /// - public void AddOrUpdatePermissions(ContentPermissionSet permission) + PersistRelations(entity); + + entity.ResetDirtyProperties(); + + // troubleshooting + //if (Database.ExecuteScalar($"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.DocumentVersion} JOIN {Constants.DatabaseSchema.Tables.ContentVersion} ON {Constants.DatabaseSchema.Tables.DocumentVersion}.id={Constants.DatabaseSchema.Tables.ContentVersion}.id WHERE published=1 AND nodeId=" + content.Id) > 1) + //{ + // Debugger.Break(); + // throw new Exception("oops"); + //} + //if (Database.ExecuteScalar($"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.DocumentVersion} JOIN {Constants.DatabaseSchema.Tables.ContentVersion} ON {Constants.DatabaseSchema.Tables.DocumentVersion}.id={Constants.DatabaseSchema.Tables.ContentVersion}.id WHERE [current]=1 AND nodeId=" + content.Id) > 1) + //{ + // Debugger.Break(); + // throw new Exception("oops"); + //} + } + + protected override void PersistUpdatedItem(IContent entity) + { + var isEntityDirty = entity.IsDirty(); + var editedSnapshot = entity.Edited; + + // check if we need to make any database changes at all + if ((entity.PublishedState == PublishedState.Published || entity.PublishedState == PublishedState.Unpublished) + && !isEntityDirty && !entity.IsAnyUserPropertyDirty()) { - PermissionRepository.Save(permission); + return; // no change to save, do nothing, don't even update dates } - /// - public override IEnumerable GetPage(IQuery? query, - long pageIndex, int pageSize, out long totalRecords, - IQuery? filter, Ordering? ordering) + // whatever we do, we must check that we are saving the current version + ContentVersionDto? version = Database.Fetch(SqlContext.Sql().Select() + .From().Where(x => x.Id == entity.VersionId)).FirstOrDefault(); + if (version == null || !version.Current) { - Sql? filterSql = null; + throw new InvalidOperationException("Cannot save a non-current version."); + } - // if we have a filter, map its clauses to an Sql statement - if (filter != null) - { - // if the clause works on "name", we need to swap the field and use the variantName instead, - // so that querying also works on variant content (for instance when searching a listview). + // update + entity.UpdatingEntity(); - // figure out how the "name" field is going to look like - so we can look for it - var nameField = SqlContext.VisitModelField(x => x.Name); + // Check if this entity is being moved as a descendant as part of a bulk moving operations. + // In this case we can bypass a lot of the below operations which will make this whole operation go much faster. + // When moving we don't need to create new versions, etc... because we cannot roll this operation back anyways. + var isMoving = entity.IsMoving(); + // TODO: I'm sure we can also detect a "Copy" (of a descendant) operation and probably perform similar checks below. + // There is probably more stuff that would be required for copying but I'm sure not all of this logic would be, we could more than likely boost + // copy performance by 95% just like we did for Move - filterSql = Sql(); - foreach (var filterClause in filter.GetWhereClauses()) - { - var clauseSql = filterClause.Item1; - var clauseArgs = filterClause.Item2; - // replace the name field - // we cannot reference an aliased field in a WHERE clause, so have to repeat the expression here - clauseSql = clauseSql.Replace(nameField, VariantNameSqlExpression); + var publishing = entity.PublishedState == PublishedState.Publishing; - // append the clause - filterSql.Append($"AND ({clauseSql})", clauseArgs); - } + if (!isMoving) + { + // check if we need to create a new version + if (publishing && entity.PublishedVersionId > 0) + { + // published version is not published anymore + Database.Execute(Sql().Update(u => u.Set(x => x.Published, false)) + .Where(x => x.Id == entity.PublishedVersionId)); } - return GetPage(query, pageIndex, pageSize, out totalRecords, - x => MapDtosToContent(x), - filterSql, - ordering); - } + // sanitize names + SanitizeNames(entity, publishing); - public bool IsPathPublished(IContent? content) - { - // fail fast - if (content?.Path.StartsWith("-1,-20,") ?? false) - return false; + // ensure that strings don't contain characters that are invalid in xml + // TODO: do we really want to keep doing this here? + entity.SanitizeEntityPropertiesForXmlStorage(); - // succeed fast - if (content?.ParentId == -1) - return content.Published; + // if parent has changed, get path, level and sort order + if (entity.IsPropertyDirty("ParentId")) + { + NodeDto parent = GetParentNodeDto(entity.ParentId); + entity.Path = string.Concat(parent.Path, ",", entity.Id); + entity.Level = parent.Level + 1; + entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0); + } + } - var ids = content?.Path.Split(Constants.CharArrays.Comma).Skip(1).Select(s => int.Parse(s, CultureInfo.InvariantCulture)); + // create the dto + DocumentDto dto = ContentBaseFactory.BuildDto(entity, NodeObjectTypeId); - var sql = SqlContext.Sql() - .SelectCount(x => x.NodeId) - .From() - .InnerJoin().On((n, d) => n.NodeId == d.NodeId && d.Published) - .WhereIn(x => x.NodeId, ids); + // update the node dto + NodeDto nodeDto = dto.ContentDto.NodeDto; + nodeDto.ValidatePathWithException(); + Database.Update(nodeDto); - var count = Database.ExecuteScalar(sql); - return count == content?.Level; - } + if (!isMoving) + { + // update the content dto + Database.Update(dto.ContentDto); - #endregion + // update the content & document version dtos + ContentVersionDto contentVersionDto = dto.DocumentVersionDto.ContentVersionDto; + DocumentVersionDto documentVersionDto = dto.DocumentVersionDto; + if (publishing) + { + documentVersionDto.Published = true; // now published + contentVersionDto.Current = false; // no more current + } - #region Recycle Bin + // Ensure existing version retains current preventCleanup flag (both saving and publishing). + contentVersionDto.PreventCleanup = version.PreventCleanup; - public override int RecycleBinId => Cms.Core.Constants.System.RecycleBinContent; + Database.Update(contentVersionDto); + Database.Update(documentVersionDto); - public bool RecycleBinSmells() - { - var cache = _appCaches.RuntimeCache; - var cacheKey = CacheKeys.ContentRecycleBinCacheKey; + // and, if publishing, insert new content & document version dtos + if (publishing) + { + entity.PublishedVersionId = entity.VersionId; - // always cache either true or false - return cache.GetCacheItem(cacheKey, () => CountChildren(RecycleBinId) > 0); - } + contentVersionDto.Id = 0; // want a new id + contentVersionDto.Current = true; // current version + contentVersionDto.Text = entity.Name; + contentVersionDto.PreventCleanup = false; // new draft version disregards prevent cleanup flag + Database.Insert(contentVersionDto); + entity.VersionId = documentVersionDto.Id = contentVersionDto.Id; // get the new id - #endregion + documentVersionDto.Published = false; // non-published version + Database.Insert(documentVersionDto); + } - #region Read Repository implementation for Guid keys + // replace the property data (rather than updating) + // only need to delete for the version that existed, the new version (if any) has no property data yet + var versionToDelete = publishing ? entity.PublishedVersionId : entity.VersionId; - public IContent? Get(Guid id) - { - return _contentByGuidReadRepository.Get(id); - } + // insert property data + ReplacePropertyValues(entity, versionToDelete, publishing ? entity.PublishedVersionId : 0, out var edited, + out HashSet? editedCultures); - IEnumerable IReadRepository.GetMany(params Guid[]? ids) - { - return _contentByGuidReadRepository.GetMany(ids); - } + // if !publishing, we may have a new name != current publish name, + // also impacts 'edited' + if (!publishing && entity.PublishName != entity.Name) + { + edited = true; + } - public bool Exists(Guid id) - { - return _contentByGuidReadRepository.Exists(id); - } + // To establish the new value of "edited" we compare all properties publishedValue to editedValue and look + // for differences. + // + // If we SaveAndPublish but the publish fails (e.g. already scheduled for release) + // we have lost the publishedValue on IContent (in memory vs database) so we cannot correctly make that comparison. + // + // This is a slight change to behaviour, historically a publish, followed by change & save, followed by undo change & save + // would change edited back to false. + if (!publishing && editedSnapshot) + { + edited = true; + } + + if (entity.ContentType.VariesByCulture()) + { + // names also impact 'edited' + // ReSharper disable once UseDeconstruction + foreach (ContentCultureInfos cultureInfo in entity.CultureInfos!) + { + if (cultureInfo.Name != entity.GetPublishName(cultureInfo.Culture)) + { + edited = true; + (editedCultures ??= new HashSet(StringComparer.OrdinalIgnoreCase)).Add(cultureInfo + .Culture); + + // TODO: change tracking + // at the moment, we don't do any dirty tracking on property values, so we don't know whether the + // culture has just been edited or not, so we don't update its update date - that date only changes + // when the name is set, and it all works because the controller does it - but, if someone uses a + // service to change a property value and save (without setting name), the update date does not change. + } + } - // reading repository purely for looking up by GUID - // TODO: ugly and to fix we need to decouple the IRepositoryQueryable -> IRepository -> IReadRepository which should all be separate things! - // This sub-repository pattern is super old and totally unecessary anymore, caching can be handled in much nicer ways without this - private class ContentByGuidReadRepository : EntityRepositoryBase - { - private readonly DocumentRepository _outerRepo; + // refresh content + entity.SetCultureEdited(editedCultures!); - public ContentByGuidReadRepository(DocumentRepository outerRepo, IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { - _outerRepo = outerRepo; - } + // bump dates to align cultures to version + entity.AdjustDates(contentVersionDto.VersionDate, publishing); - protected override IContent? PerformGet(Guid id) - { - var sql = _outerRepo.GetBaseQuery(QueryType.Single) - .Where(x => x.UniqueId == id); + // replace the content version variations (rather than updating) + // only need to delete for the version that existed, the new version (if any) has no property data yet + Sql deleteContentVariations = Sql().Delete() + .Where(x => x.VersionId == versionToDelete); + Database.Execute(deleteContentVariations); - var dto = Database.Fetch(sql.SelectTop(1)).FirstOrDefault(); + // replace the document version variations (rather than updating) + Sql deleteDocumentVariations = Sql().Delete() + .Where(x => x.NodeId == entity.Id); + Database.Execute(deleteDocumentVariations); - if (dto == null) - return null; + // TODO: NPoco InsertBulk issue? + // we should use the native NPoco InsertBulk here but it causes problems (not sure exactly all scenarios) + // but by using SQL Server and updating a variants name will cause: Unable to cast object of type + // 'Umbraco.Core.Persistence.FaultHandling.RetryDbConnection' to type 'System.Data.SqlClient.SqlConnection'. + // (same in PersistNewItem above) - var content = _outerRepo.MapDtoToContent(dto); + // insert content variations + Database.BulkInsertRecords(GetContentVariationDtos(entity, publishing)); - return content; + // insert document variations + Database.BulkInsertRecords(GetDocumentVariationDtos(entity, editedCultures!)); } - protected override IEnumerable PerformGetAll(params Guid[]? ids) + // update the document dto + // at that point, when un/publishing, the entity still has its old Published value + // so we need to explicitly update the dto to persist the correct value + if (entity.PublishedState == PublishedState.Publishing) { - var sql = _outerRepo.GetBaseQuery(QueryType.Many); - if (ids?.Length > 0) - sql.WhereIn(x => x.UniqueId, ids); - - return _outerRepo.MapDtosToContent(Database.Fetch(sql)); + dto.Published = true; } - - protected override IEnumerable PerformGetByQuery(IQuery query) + else if (entity.PublishedState == PublishedState.Unpublishing) { - throw new InvalidOperationException("This method won't be implemented."); + dto.Published = false; } - protected override IEnumerable GetDeleteClauses() - { - throw new InvalidOperationException("This method won't be implemented."); - } + entity.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited + Database.Update(dto); - protected override void PersistNewItem(IContent entity) + // if entity is publishing, update tags, else leave tags there + // means that implicitly unpublished, or trashed, entities *still* have tags in db + if (entity.PublishedState == PublishedState.Publishing) { - throw new InvalidOperationException("This method won't be implemented."); + SetEntityTags(entity, _tagRepository, _serializer); } + } - protected override void PersistUpdatedItem(IContent entity) - { - throw new InvalidOperationException("This method won't be implemented."); - } + // trigger here, before we reset Published etc + OnUowRefreshedEntity(new ContentRefreshNotification(entity, new EventMessages())); - protected override Sql GetBaseQuery(bool isCount) + if (!isMoving) + { + // flip the entity's published property + // this also flips its published state + if (entity.PublishedState == PublishedState.Publishing) { - throw new InvalidOperationException("This method won't be implemented."); - } + entity.Published = true; + entity.PublishTemplateId = entity.TemplateId; + entity.PublisherId = entity.WriterId; + entity.PublishName = entity.Name; + entity.PublishDate = entity.UpdateDate; - protected override string GetBaseWhereClause() - { - throw new InvalidOperationException("This method won't be implemented."); + SetEntityTags(entity, _tagRepository, _serializer); } - } + else if (entity.PublishedState == PublishedState.Unpublishing) + { + entity.Published = false; + entity.PublishTemplateId = null; + entity.PublisherId = null; + entity.PublishName = null; + entity.PublishDate = null; - #endregion + ClearEntityTags(entity, _tagRepository); + } - #region Schedule + PersistRelations(entity); - /// - public void ClearSchedule(DateTime date) - { - var sql = Sql().Delete().Where(x => x.Date <= date); - Database.Execute(sql); + // TODO: note re. tags: explicitly unpublished entities have cleared tags, but masked or trashed entities *still* have tags in the db - so what? } - /// - public void ClearSchedule(DateTime date, ContentScheduleAction action) - { - var a = action.ToString(); - var sql = Sql().Delete().Where(x => x.Date <= date && x.Action == a); - Database.Execute(sql); - } + entity.ResetDirtyProperties(); + + // troubleshooting + //if (Database.ExecuteScalar($"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.DocumentVersion} JOIN {Constants.DatabaseSchema.Tables.ContentVersion} ON {Constants.DatabaseSchema.Tables.DocumentVersion}.id={Constants.DatabaseSchema.Tables.ContentVersion}.id WHERE published=1 AND nodeId=" + content.Id) > 1) + //{ + // Debugger.Break(); + // throw new Exception("oops"); + //} + //if (Database.ExecuteScalar($"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.DocumentVersion} JOIN {Constants.DatabaseSchema.Tables.ContentVersion} ON {Constants.DatabaseSchema.Tables.DocumentVersion}.id={Constants.DatabaseSchema.Tables.ContentVersion}.id WHERE [current]=1 AND nodeId=" + content.Id) > 1) + //{ + // Debugger.Break(); + // throw new Exception("oops"); + //} + } - private Sql GetSqlForHasScheduling(ContentScheduleAction action, DateTime date) + /// + public void PersistContentSchedule(IContent content, ContentScheduleCollection contentSchedule) + { + if (content == null) { - var template = SqlContext.Templates.Get("Umbraco.Core.DocumentRepository.GetSqlForHasScheduling", tsql => tsql - .SelectCount() - .From() - .Where(x => x.Action == SqlTemplate.Arg("action") && x.Date <= SqlTemplate.Arg("date"))); - - var sql = template.Sql(action.ToString(), date); - return sql; + throw new ArgumentNullException(nameof(content)); } - public bool HasContentForExpiration(DateTime date) + if (contentSchedule == null) { - var sql = GetSqlForHasScheduling(ContentScheduleAction.Expire, date); - return Database.ExecuteScalar(sql) > 0; + throw new ArgumentNullException(nameof(contentSchedule)); } - public bool HasContentForRelease(DateTime date) + var schedules = ContentBaseFactory.BuildScheduleDto(content, contentSchedule, LanguageRepository).ToList(); + + //remove any that no longer exist + IEnumerable ids = schedules.Where(x => x.Model.Id != Guid.Empty).Select(x => x.Model.Id).Distinct(); + Database.Execute(Sql() + .Delete() + .Where(x => x.NodeId == content.Id) + .WhereNotIn(x => x.Id, ids)); + + //add/update the rest + foreach ((ContentSchedule Model, ContentScheduleDto Dto) schedule in schedules) { - var sql = GetSqlForHasScheduling(ContentScheduleAction.Release, date); - return Database.ExecuteScalar(sql) > 0; + if (schedule.Model.Id == Guid.Empty) + { + schedule.Model.Id = schedule.Dto.Id = Guid.NewGuid(); + Database.Insert(schedule.Dto); + } + else + { + Database.Update(schedule.Dto); + } } + } - /// - public IEnumerable GetContentForRelease(DateTime date) - { - var action = ContentScheduleAction.Release.ToString(); + protected override void PersistDeletedItem(IContent entity) + { + // Raise event first else potential FK issues + OnUowRemovingEntity(entity); + + //We need to clear out all access rules but we need to do this in a manual way since + // nothing in that table is joined to a content id + Sql subQuery = SqlContext.Sql() + .Select(x => x.AccessId) + .From() + .InnerJoin() + .On(left => left.AccessId, right => right.Id) + .Where(dto => dto.NodeId == entity.Id); + Database.Execute(SqlContext.SqlSyntax.GetDeleteSubquery("umbracoAccessRule", "accessId", subQuery)); + + //now let the normal delete clauses take care of everything else + base.PersistDeletedItem(entity); + } - var sql = GetBaseQuery(QueryType.Many) - .WhereIn(x => x.NodeId, Sql() - .Select(x => x.NodeId) - .From() - .Where(x => x.Action == action && x.Date <= date)); + #endregion - AddGetByQueryOrderBy(sql); + #region Content Repository - return MapDtosToContent(Database.Fetch(sql)); + public int CountPublished(string? contentTypeAlias = null) + { + Sql sql = SqlContext.Sql(); + if (contentTypeAlias.IsNullOrWhiteSpace()) + { + sql.SelectCount() + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId && x.Trashed == false) + .Where(x => x.Published); } - - /// - public IEnumerable GetContentForExpiration(DateTime date) + else { - var action = ContentScheduleAction.Expire.ToString(); + sql.SelectCount() + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.ContentTypeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId && x.Trashed == false) + .Where(x => x.Alias == contentTypeAlias) + .Where(x => x.Published); + } - var sql = GetBaseQuery(QueryType.Many) - .WhereIn(x => x.NodeId, Sql() - .Select(x => x.NodeId) - .From() - .Where(x => x.Action == action && x.Date <= date)); + return Database.ExecuteScalar(sql); + } - AddGetByQueryOrderBy(sql); + public void ReplaceContentPermissions(EntityPermissionSet permissionSet) => + PermissionRepository.ReplaceEntityPermissions(permissionSet); - return MapDtosToContent(Database.Fetch(sql)); - } + /// + /// Assigns a single permission to the current content item for the specified group ids + /// + /// + /// + /// + public void AssignEntityPermission(IContent entity, char permission, IEnumerable groupIds) => + PermissionRepository.AssignEntityPermission(entity, permission, groupIds); - #endregion + public EntityPermissionCollection GetPermissionsForEntity(int entityId) => + PermissionRepository.GetPermissionsForEntity(entityId); - protected override string ApplySystemOrdering(ref Sql sql, Ordering ordering) - { - // note: 'updater' is the user who created the latest draft version, - // we don't have an 'updater' per culture (should we?) - if (ordering.OrderBy.InvariantEquals("updater")) - { - var joins = Sql() - .InnerJoin("updaterUser").On((version, user) => version.UserId == user.Id, aliasRight: "updaterUser"); + /// + /// Used to add/update a permission for a content item + /// + /// + public void AddOrUpdatePermissions(ContentPermissionSet permission) => PermissionRepository.Save(permission); - // see notes in ApplyOrdering: the field MUST be selected + aliased - sql = Sql(InsertBefore(sql, "FROM", ", " + SqlSyntax.GetFieldName(x => x.UserName, "updaterUser") + " AS ordering "), sql.Arguments); + /// + public override IEnumerable GetPage(IQuery? query, + long pageIndex, int pageSize, out long totalRecords, + IQuery? filter, Ordering? ordering) + { + Sql? filterSql = null; - sql = InsertJoins(sql, joins); + // if we have a filter, map its clauses to an Sql statement + if (filter != null) + { + // if the clause works on "name", we need to swap the field and use the variantName instead, + // so that querying also works on variant content (for instance when searching a listview). - return "ordering"; - } + // figure out how the "name" field is going to look like - so we can look for it + var nameField = SqlContext.VisitModelField(x => x.Name); - if (ordering.OrderBy.InvariantEquals("published")) + filterSql = Sql(); + foreach (Tuple filterClause in filter.GetWhereClauses()) { - // no culture = can only work on the global 'published' flag - if (ordering.Culture.IsNullOrWhiteSpace()) - { - // see notes in ApplyOrdering: the field MUST be selected + aliased, and we cannot have - // the whole CASE fragment in ORDER BY due to it not being detected by NPoco - sql = Sql(InsertBefore(sql, "FROM", ", (CASE WHEN pcv.id IS NULL THEN 0 ELSE 1 END) AS ordering "), sql.Arguments); - return "ordering"; - } - - // invariant: left join will yield NULL and we must use pcv to determine published - // variant: left join may yield NULL or something, and that determines published + var clauseSql = filterClause.Item1; + var clauseArgs = filterClause.Item2; + // replace the name field + // we cannot reference an aliased field in a WHERE clause, so have to repeat the expression here + clauseSql = clauseSql.Replace(nameField, VariantNameSqlExpression); - var joins = Sql() - .InnerJoin("ctype").On((content, contentType) => content.ContentTypeId == contentType.NodeId, aliasRight: "ctype") - // left join on optional culture variation - //the magic "[[[ISOCODE]]]" parameter value will be replaced in ContentRepositoryBase.GetPage() by the actual ISO code - .LeftJoin(nested => - nested.InnerJoin("langp").On((ccv, lang) => ccv.LanguageId == lang.Id && lang.IsoCode == "[[[ISOCODE]]]", "ccvp", "langp"), "ccvp") - .On((version, ccv) => version.Id == ccv.VersionId, aliasLeft: "pcv", aliasRight: "ccvp"); + // append the clause + filterSql.Append($"AND ({clauseSql})", clauseArgs); + } + } - sql = InsertJoins(sql, joins); + return GetPage(query, pageIndex, pageSize, out totalRecords, + x => MapDtosToContent(x), + filterSql, + ordering); + } - // see notes in ApplyOrdering: the field MUST be selected + aliased, and we cannot have - // the whole CASE fragment in ORDER BY due to it not being detected by NPoco - var sqlText = InsertBefore(sql.SQL, "FROM", + public bool IsPathPublished(IContent? content) + { + // fail fast + if (content?.Path.StartsWith("-1,-20,") ?? false) + { + return false; + } - // when invariant, ie 'variations' does not have the culture flag (value 1), use the global 'published' flag on pcv.id, - // otherwise check if there's a version culture variation for the lang, via ccv.id - ", (CASE WHEN (ctype.variations & 1) = 0 THEN (CASE WHEN pcv.id IS NULL THEN 0 ELSE 1 END) ELSE (CASE WHEN ccvp.id IS NULL THEN 0 ELSE 1 END) END) AS ordering "); // trailing space is important! + // succeed fast + if (content?.ParentId == -1) + { + return content.Published; + } - sql = Sql(sqlText, sql.Arguments); + IEnumerable? ids = content?.Path.Split(Constants.CharArrays.Comma).Skip(1) + .Select(s => int.Parse(s, CultureInfo.InvariantCulture)); - return "ordering"; - } + Sql sql = SqlContext.Sql() + .SelectCount(x => x.NodeId) + .From() + .InnerJoin().On((n, d) => n.NodeId == d.NodeId && d.Published) + .WhereIn(x => x.NodeId, ids); - return base.ApplySystemOrdering(ref sql, ordering); - } + var count = Database.ExecuteScalar(sql); + return count == content?.Level; + } - private IEnumerable MapDtosToContent(List dtos, - bool withCache = false, - bool loadProperties = true, - bool loadTemplates = true, - bool loadVariants = true) - { - var temps = new List>(); - var contentTypes = new Dictionary(); - var templateIds = new List(); + #endregion - var content = new Content[dtos.Count]; + #region Recycle Bin - for (var i = 0; i < dtos.Count; i++) - { - var dto = dtos[i]; + public override int RecycleBinId => Constants.System.RecycleBinContent; - if (withCache) - { - // if the cache contains the (proper version of the) item, use it - var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); - if (cached != null && cached.VersionId == dto.DocumentVersionDto.ContentVersionDto.Id) - { - content[i] = (Content)cached; - continue; - } - } + public bool RecycleBinSmells() + { + IAppPolicyCache cache = _appCaches.RuntimeCache; + var cacheKey = CacheKeys.ContentRecycleBinCacheKey; - // else, need to build it + // always cache either true or false + return cache.GetCacheItem(cacheKey, () => CountChildren(RecycleBinId) > 0); + } - // get the content type - the repository is full cache *but* still deep-clones - // whatever comes out of it, so use our own local index here to avoid this - var contentTypeId = dto.ContentDto.ContentTypeId; - if (contentTypes.TryGetValue(contentTypeId, out var contentType) == false) - contentTypes[contentTypeId] = contentType = _contentTypeRepository.Get(contentTypeId); + #endregion - var c = content[i] = ContentBaseFactory.BuildEntity(dto, contentType); + #region Read Repository implementation for Guid keys - if (loadTemplates) - { - // need templates - var templateId = dto.DocumentVersionDto.TemplateId; - if (templateId.HasValue) - templateIds.Add(templateId.Value); - if (dto.Published) - { - templateId = dto.PublishedVersionDto.TemplateId; - if (templateId.HasValue) - templateIds.Add(templateId.Value); - } - } + public IContent? Get(Guid id) => _contentByGuidReadRepository.Get(id); - // need temps, for properties, templates and variations - var versionId = dto.DocumentVersionDto.Id; - var publishedVersionId = dto.Published ? dto.PublishedVersionDto.Id : 0; - var temp = new TempContent(dto.NodeId, versionId, publishedVersionId, contentType, c) - { - Template1Id = dto.DocumentVersionDto.TemplateId - }; - if (dto.Published) - temp.Template2Id = dto.PublishedVersionDto.TemplateId; - temps.Add(temp); - } + IEnumerable IReadRepository.GetMany(params Guid[]? ids) => + _contentByGuidReadRepository.GetMany(ids); - Dictionary? templates = null; - if (loadTemplates) - { - // load all required templates in 1 query, and index - templates = _templateRepository.GetMany(templateIds.ToArray())? - .ToDictionary(x => x.Id, x => x); - } + public bool Exists(Guid id) => _contentByGuidReadRepository.Exists(id); - IDictionary? properties = null; - if (loadProperties) - { - // load all properties for all documents from database in 1 query - indexed by version id - properties = GetPropertyCollections(temps); - } + // reading repository purely for looking up by GUID + // TODO: ugly and to fix we need to decouple the IRepositoryQueryable -> IRepository -> IReadRepository which should all be separate things! + // This sub-repository pattern is super old and totally unecessary anymore, caching can be handled in much nicer ways without this + private class ContentByGuidReadRepository : EntityRepositoryBase + { + private readonly DocumentRepository _outerRepo; - // assign templates and properties - foreach (var temp in temps) - { - if (loadTemplates) - { - // set the template ID if it matches an existing template - if (temp.Template1Id.HasValue && (templates?.ContainsKey(temp.Template1Id.Value) ?? false)) - temp.Content!.TemplateId = temp.Template1Id; - if (temp.Template2Id.HasValue && (templates?.ContainsKey(temp.Template2Id.Value) ?? false)) - temp.Content!.PublishTemplateId = temp.Template2Id; - } + public ContentByGuidReadRepository(DocumentRepository outerRepo, IScopeAccessor scopeAccessor, AppCaches cache, + ILogger logger) + : base(scopeAccessor, cache, logger) => + _outerRepo = outerRepo; + protected override IContent? PerformGet(Guid id) + { + Sql sql = _outerRepo.GetBaseQuery(QueryType.Single) + .Where(x => x.UniqueId == id); - // set properties - if (loadProperties) - { - if (properties?.ContainsKey(temp.VersionId) ?? false) - temp.Content!.Properties = properties[temp.VersionId]; - else - throw new InvalidOperationException($"No property data found for version: '{temp.VersionId}'."); - } - } + DocumentDto? dto = Database.Fetch(sql.SelectTop(1)).FirstOrDefault(); - if (loadVariants) + if (dto == null) { - // set variations, if varying - temps = temps.Where(x => x.ContentType?.VariesByCulture() ?? false).ToList(); - if (temps.Count > 0) - { - // load all variations for all documents from database, in one query - var contentVariations = GetContentVariations(temps); - var documentVariations = GetDocumentVariations(temps); - foreach (var temp in temps) - SetVariations(temp.Content, contentVariations, documentVariations); - } + return null; } - - - foreach (var c in content) - c.ResetDirtyProperties(false); // reset dirty initial properties (U4-1946) + IContent content = _outerRepo.MapDtoToContent(dto); return content; } - private IContent MapDtoToContent(DocumentDto dto) + protected override IEnumerable PerformGetAll(params Guid[]? ids) { - var contentType = _contentTypeRepository.Get(dto.ContentDto.ContentTypeId); - var content = ContentBaseFactory.BuildEntity(dto, contentType); - - try + Sql sql = _outerRepo.GetBaseQuery(QueryType.Many); + if (ids?.Length > 0) { - content.DisableChangeTracking(); - - // get template - if (dto.DocumentVersionDto.TemplateId.HasValue) - content.TemplateId = dto.DocumentVersionDto.TemplateId; - - // get properties - indexed by version id - var versionId = dto.DocumentVersionDto.Id; + sql.WhereIn(x => x.UniqueId, ids); + } - // TODO: shall we get published properties or not? - //var publishedVersionId = dto.Published ? dto.PublishedVersionDto.Id : 0; - var publishedVersionId = dto.PublishedVersionDto?.Id ?? 0; + return _outerRepo.MapDtosToContent(Database.Fetch(sql)); + } - var temp = new TempContent(dto.NodeId, versionId, publishedVersionId, contentType); - var ltemp = new List> { temp }; - var properties = GetPropertyCollections(ltemp); - content.Properties = properties[dto.DocumentVersionDto.Id]; + protected override IEnumerable PerformGetByQuery(IQuery query) => + throw new InvalidOperationException("This method won't be implemented."); - // set variations, if varying - if (contentType?.VariesByCulture() ?? false) - { - var contentVariations = GetContentVariations(ltemp); - var documentVariations = GetDocumentVariations(ltemp); - SetVariations(content, contentVariations, documentVariations); - } + protected override IEnumerable GetDeleteClauses() => + throw new InvalidOperationException("This method won't be implemented."); - // reset dirty initial properties (U4-1946) - content.ResetDirtyProperties(false); - return content; - } - finally - { - content.EnableChangeTracking(); - } - } + protected override void PersistNewItem(IContent entity) => + throw new InvalidOperationException("This method won't be implemented."); - /// - public ContentScheduleCollection GetContentSchedule(int contentId) - { - var result = new ContentScheduleCollection(); + protected override void PersistUpdatedItem(IContent entity) => + throw new InvalidOperationException("This method won't be implemented."); - var scheduleDtos = Database.Fetch(Sql() - .Select() - .From() - .Where(x => x.NodeId == contentId )); + protected override Sql GetBaseQuery(bool isCount) => + throw new InvalidOperationException("This method won't be implemented."); - foreach (var scheduleDto in scheduleDtos) - { - result.Add(new ContentSchedule(scheduleDto.Id, - LanguageRepository.GetIsoCodeById(scheduleDto.LanguageId) ?? string.Empty, - scheduleDto.Date, - scheduleDto.Action == ContentScheduleAction.Release.ToString() - ? ContentScheduleAction.Release - : ContentScheduleAction.Expire)); - } + protected override string GetBaseWhereClause() => + throw new InvalidOperationException("This method won't be implemented."); + } - return result; - } + #endregion - private void SetVariations(Content? content, IDictionary> contentVariations, IDictionary> documentVariations) - { - if (content is null) - { - return; - } - if (contentVariations.TryGetValue(content.VersionId, out var contentVariation)) - foreach (var v in contentVariation) - content.SetCultureInfo(v.Culture, v.Name, v.Date); + #region Schedule - if (content.PublishedVersionId > 0 && contentVariations.TryGetValue(content.PublishedVersionId, out contentVariation)) - { - foreach (var v in contentVariation) - content.SetPublishInfo(v.Culture, v.Name, v.Date); - } + /// + public void ClearSchedule(DateTime date) + { + Sql sql = Sql().Delete().Where(x => x.Date <= date); + Database.Execute(sql); + } - if (documentVariations.TryGetValue(content.Id, out var documentVariation)) - content.SetCultureEdited(documentVariation.Where(x => x.Edited).Select(x => x.Culture)); - } + /// + public void ClearSchedule(DateTime date, ContentScheduleAction action) + { + var a = action.ToString(); + Sql sql = Sql().Delete() + .Where(x => x.Date <= date && x.Action == a); + Database.Execute(sql); + } - private IDictionary> GetContentVariations(List> temps) - where T : class, IContentBase - { - var versions = new List(); - foreach (var temp in temps) - { - versions.Add(temp.VersionId); - if (temp.PublishedVersionId > 0) - versions.Add(temp.PublishedVersionId); - } - if (versions.Count == 0) - return new Dictionary>(); + private Sql GetSqlForHasScheduling(ContentScheduleAction action, DateTime date) + { + SqlTemplate template = SqlContext.Templates.Get("Umbraco.Core.DocumentRepository.GetSqlForHasScheduling", + tsql => tsql + .SelectCount() + .From() + .Where(x => + x.Action == SqlTemplate.Arg("action") && x.Date <= SqlTemplate.Arg("date"))); - var dtos = Database.FetchByGroups(versions, Constants.Sql.MaxParameterCount, batch - => Sql() - .Select() - .From() - .WhereIn(x => x.VersionId, batch)); + Sql sql = template.Sql(action.ToString(), date); + return sql; + } - var variations = new Dictionary>(); + public bool HasContentForExpiration(DateTime date) + { + Sql sql = GetSqlForHasScheduling(ContentScheduleAction.Expire, date); + return Database.ExecuteScalar(sql) > 0; + } - foreach (var dto in dtos) - { - if (!variations.TryGetValue(dto.VersionId, out var variation)) - variations[dto.VersionId] = variation = new List(); + public bool HasContentForRelease(DateTime date) + { + Sql sql = GetSqlForHasScheduling(ContentScheduleAction.Release, date); + return Database.ExecuteScalar(sql) > 0; + } - variation.Add(new ContentVariation - { - Culture = LanguageRepository.GetIsoCodeById(dto.LanguageId), - Name = dto.Name, - Date = dto.UpdateDate - }); - } + /// + public IEnumerable GetContentForRelease(DateTime date) + { + var action = ContentScheduleAction.Release.ToString(); - return variations; - } + Sql sql = GetBaseQuery(QueryType.Many) + .WhereIn(x => x.NodeId, Sql() + .Select(x => x.NodeId) + .From() + .Where(x => x.Action == action && x.Date <= date)); - private IDictionary> GetDocumentVariations(List> temps) - where T : class, IContentBase - { - var ids = temps.Select(x => x.Id); + AddGetByQueryOrderBy(sql); - var dtos = Database.FetchByGroups(ids, Constants.Sql.MaxParameterCount, batch => - Sql() - .Select() - .From() - .WhereIn(x => x.NodeId, batch)); + return MapDtosToContent(Database.Fetch(sql)); + } - var variations = new Dictionary>(); + /// + public IEnumerable GetContentForExpiration(DateTime date) + { + var action = ContentScheduleAction.Expire.ToString(); - foreach (var dto in dtos) - { - if (!variations.TryGetValue(dto.NodeId, out var variation)) - variations[dto.NodeId] = variation = new List(); + Sql sql = GetBaseQuery(QueryType.Many) + .WhereIn(x => x.NodeId, Sql() + .Select(x => x.NodeId) + .From() + .Where(x => x.Action == action && x.Date <= date)); - variation.Add(new DocumentVariation - { - Culture = LanguageRepository.GetIsoCodeById(dto.LanguageId), - Edited = dto.Edited - }); - } + AddGetByQueryOrderBy(sql); - return variations; - } + return MapDtosToContent(Database.Fetch(sql)); + } - private IEnumerable GetContentVariationDtos(IContent content, bool publishing) - { - if (content.CultureInfos is not null) - { - // create dtos for the 'current' (non-published) version, all cultures - // ReSharper disable once UseDeconstruction - foreach (var cultureInfo in content.CultureInfos) - yield return new ContentVersionCultureVariationDto - { - VersionId = content.VersionId, - LanguageId = LanguageRepository.GetIdByIsoCode(cultureInfo.Culture) ?? throw new InvalidOperationException("Not a valid culture."), - Culture = cultureInfo.Culture, - Name = cultureInfo.Name, - UpdateDate = content.GetUpdateDate(cultureInfo.Culture) ?? DateTime.MinValue // we *know* there is a value - }; - } + #endregion - // if not publishing, we're just updating the 'current' (non-published) version, - // so there are no DTOs to create for the 'published' version which remains unchanged - if (!publishing) - yield break; + #region Utilities - if (content.PublishCultureInfos is not null) - { - // create dtos for the 'published' version, for published cultures (those having a name) - // ReSharper disable once UseDeconstruction - foreach (var cultureInfo in content.PublishCultureInfos) - yield return new ContentVersionCultureVariationDto - { - VersionId = content.PublishedVersionId, - LanguageId = LanguageRepository.GetIdByIsoCode(cultureInfo.Culture) ?? throw new InvalidOperationException("Not a valid culture."), - Culture = cultureInfo.Culture, - Name = cultureInfo.Name, - UpdateDate = content.GetPublishDate(cultureInfo.Culture) ?? DateTime.MinValue // we *know* there is a value - }; - } - } + private void SanitizeNames(IContent content, bool publishing) + { + // a content item *must* have an invariant name, and invariant published name + // else we just cannot write the invariant rows (node, content version...) to the database + + // ensure that we have an invariant name + // invariant content = must be there already, else throw + // variant content = update with default culture or anything really + EnsureInvariantNameExists(content); + + // ensure that invariant name is unique + EnsureInvariantNameIsUnique(content); + + // and finally, + // ensure that each culture has a unique node name + // no published name = not published + // else, it needs to be unique + EnsureVariantNamesAreUnique(content, publishing); + } - private IEnumerable GetDocumentVariationDtos(IContent content, HashSet editedCultures) + private void EnsureInvariantNameExists(IContent content) + { + if (content.ContentType.VariesByCulture()) { - var allCultures = content.AvailableCultures.Union(content.PublishedCultures); // union = distinct - foreach (var culture in allCultures) + // content varies by culture + // then it must have at least a variant name, else it makes no sense + if (content.CultureInfos?.Count == 0) { - var dto = new DocumentCultureVariationDto - { - NodeId = content.Id, - LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."), - Culture = culture, - - Name = content.GetCultureName(culture) ?? content.GetPublishName(culture), - Available = content.IsCultureAvailable(culture), - Published = content.IsCulturePublished(culture), - // note: can't use IsCultureEdited at that point - hasn't been updated yet - see PersistUpdatedItem - Edited = content.IsCultureAvailable(culture) && - (!content.IsCulturePublished(culture) || (editedCultures != null && editedCultures.Contains(culture))) - }; - - yield return dto; + throw new InvalidOperationException("Cannot save content with an empty name."); } + // and then, we need to set the invariant name implicitly, + // using the default culture if it has a name, otherwise anything we can + var defaultCulture = LanguageRepository.GetDefaultIsoCode(); + content.Name = defaultCulture != null && + (content.CultureInfos?.TryGetValue(defaultCulture, out ContentCultureInfos cultureName) ?? + false) + ? cultureName.Name! + : content.CultureInfos![0].Name!; } - - private class ContentVariation - { - public string? Culture { get; set; } - public string? Name { get; set; } - public DateTime Date { get; set; } - } - - private class DocumentVariation - { - public string? Culture { get; set; } - public bool Edited { get; set; } - } - - #region Utilities - - private void SanitizeNames(IContent content, bool publishing) - { - // a content item *must* have an invariant name, and invariant published name - // else we just cannot write the invariant rows (node, content version...) to the database - - // ensure that we have an invariant name - // invariant content = must be there already, else throw - // variant content = update with default culture or anything really - EnsureInvariantNameExists(content); - - // ensure that invariant name is unique - EnsureInvariantNameIsUnique(content); - - // and finally, - // ensure that each culture has a unique node name - // no published name = not published - // else, it needs to be unique - EnsureVariantNamesAreUnique(content, publishing); - } - - private void EnsureInvariantNameExists(IContent content) + else { - if (content.ContentType.VariesByCulture()) - { - // content varies by culture - // then it must have at least a variant name, else it makes no sense - if (content.CultureInfos?.Count == 0) - throw new InvalidOperationException("Cannot save content with an empty name."); - - // and then, we need to set the invariant name implicitly, - // using the default culture if it has a name, otherwise anything we can - var defaultCulture = LanguageRepository.GetDefaultIsoCode(); - content.Name = defaultCulture != null && (content.CultureInfos?.TryGetValue(defaultCulture, out var cultureName) ?? false) - ? cultureName.Name! - : content.CultureInfos![0].Name!; - } - else + // content is invariant, and invariant content must have an explicit invariant name + if (string.IsNullOrWhiteSpace(content.Name)) { - // content is invariant, and invariant content must have an explicit invariant name - if (string.IsNullOrWhiteSpace(content.Name)) - throw new InvalidOperationException("Cannot save content with an empty name."); + throw new InvalidOperationException("Cannot save content with an empty name."); } } + } - private void EnsureInvariantNameIsUnique(IContent content) - { - content.Name = EnsureUniqueNodeName(content.ParentId, content.Name, content.Id); - } + private void EnsureInvariantNameIsUnique(IContent content) => + content.Name = EnsureUniqueNodeName(content.ParentId, content.Name, content.Id); - protected override string? EnsureUniqueNodeName(int parentId, string? nodeName, int id = 0) - { - return EnsureUniqueNaming == false ? nodeName : base.EnsureUniqueNodeName(parentId, nodeName, id); - } + protected override string? EnsureUniqueNodeName(int parentId, string? nodeName, int id = 0) => + EnsureUniqueNaming == false ? nodeName : base.EnsureUniqueNodeName(parentId, nodeName, id); - private SqlTemplate SqlEnsureVariantNamesAreUnique => SqlContext.Templates.Get("Umbraco.Core.DomainRepository.EnsureVariantNamesAreUnique", tsql => tsql + private SqlTemplate SqlEnsureVariantNamesAreUnique => SqlContext.Templates.Get( + "Umbraco.Core.DomainRepository.EnsureVariantNamesAreUnique", tsql => tsql .Select(x => x.Id, x => x.Name, x => x.LanguageId) .From() - .InnerJoin().On(x => x.Id, x => x.VersionId) + .InnerJoin() + .On(x => x.Id, x => x.VersionId) .InnerJoin().On(x => x.NodeId, x => x.NodeId) .Where(x => x.Current == SqlTemplate.Arg("current")) .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType") && @@ -1628,58 +1729,73 @@ private void EnsureInvariantNameIsUnique(IContent content) x.NodeId != SqlTemplate.Arg("id")) .OrderBy(x => x.LanguageId)); - private void EnsureVariantNamesAreUnique(IContent content, bool publishing) + private void EnsureVariantNamesAreUnique(IContent content, bool publishing) + { + if (!EnsureUniqueNaming || !content.ContentType.VariesByCulture() || content.CultureInfos?.Count == 0) { - if (!EnsureUniqueNaming || !content.ContentType.VariesByCulture() || content.CultureInfos?.Count == 0) - return; + return; + } - // get names per culture, at same level (ie all siblings) - var sql = SqlEnsureVariantNamesAreUnique.Sql(true, NodeObjectTypeId, content.ParentId, content.Id); - var names = Database.Fetch(sql) - .GroupBy(x => x.LanguageId) - .ToDictionary(x => x.Key, x => x); + // get names per culture, at same level (ie all siblings) + Sql sql = SqlEnsureVariantNamesAreUnique.Sql(true, NodeObjectTypeId, content.ParentId, content.Id); + var names = Database.Fetch(sql) + .GroupBy(x => x.LanguageId) + .ToDictionary(x => x.Key, x => x); - if (names.Count == 0) - return; + if (names.Count == 0) + { + return; + } - // note: the code below means we are going to unique-ify every culture names, regardless - // of whether the name has changed (ie the culture has been updated) - some saving culture - // fr-FR could cause culture en-UK name to change - not sure that is clean + // note: the code below means we are going to unique-ify every culture names, regardless + // of whether the name has changed (ie the culture has been updated) - some saving culture + // fr-FR could cause culture en-UK name to change - not sure that is clean + + if (content.CultureInfos is null) + { + return; + } - if (content.CultureInfos is null) + foreach (ContentCultureInfos cultureInfo in content.CultureInfos) + { + var langId = LanguageRepository.GetIdByIsoCode(cultureInfo.Culture); + if (!langId.HasValue) { - return; + continue; } - foreach (var cultureInfo in content.CultureInfos) - { - var langId = LanguageRepository.GetIdByIsoCode(cultureInfo.Culture); - if (!langId.HasValue) - continue; - if (!names.TryGetValue(langId.Value, out var cultureNames)) - continue; - // get a unique name - var otherNames = cultureNames.Select(x => new SimilarNodeName { Id = x.Id, Name = x.Name }); - var uniqueName = SimilarNodeName.GetUniqueName(otherNames, 0, cultureInfo.Name); + if (!names.TryGetValue(langId.Value, out IGrouping? cultureNames)) + { + continue; + } - if (uniqueName == content.GetCultureName(cultureInfo.Culture)) - continue; + // get a unique name + IEnumerable otherNames = + cultureNames.Select(x => new SimilarNodeName {Id = x.Id, Name = x.Name}); + var uniqueName = SimilarNodeName.GetUniqueName(otherNames, 0, cultureInfo.Name); - // update the name, and the publish name if published - content.SetCultureName(uniqueName, cultureInfo.Culture); - if (publishing && (content.PublishCultureInfos?.ContainsKey(cultureInfo.Culture) ?? false)) - content.SetPublishInfo(cultureInfo.Culture, uniqueName, DateTime.Now); //TODO: This is weird, this call will have already been made in the SetCultureName + if (uniqueName == content.GetCultureName(cultureInfo.Culture)) + { + continue; } - } - // ReSharper disable once ClassNeverInstantiated.Local - private class CultureNodeName - { - public int Id { get; set; } - public string? Name { get; set; } - public int LanguageId { get; set; } + // update the name, and the publish name if published + content.SetCultureName(uniqueName, cultureInfo.Culture); + if (publishing && (content.PublishCultureInfos?.ContainsKey(cultureInfo.Culture) ?? false)) + { + content.SetPublishInfo(cultureInfo.Culture, uniqueName, + DateTime.Now); //TODO: This is weird, this call will have already been made in the SetCultureName + } } + } - #endregion + // ReSharper disable once ClassNeverInstantiated.Local + private class CultureNodeName + { + public int Id { get; set; } + public string? Name { get; set; } + public int LanguageId { get; set; } } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentTypeContainerRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentTypeContainerRepository.cs index f37886fee2bf..c3356cbe91c7 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentTypeContainerRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentTypeContainerRepository.cs @@ -1,14 +1,16 @@ using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Infrastructure.Scoping; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class DocumentTypeContainerRepository : EntityContainerRepository, IDocumentTypeContainerRepository { - internal class DocumentTypeContainerRepository : EntityContainerRepository, IDocumentTypeContainerRepository + public DocumentTypeContainerRepository(IScopeAccessor scopeAccessor, AppCaches cache, + ILogger logger) + : base(scopeAccessor, cache, logger, Constants.ObjectTypes.DocumentTypeContainer) { - public DocumentTypeContainerRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger, Cms.Core.Constants.ObjectTypes.DocumentTypeContainer) - { } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentVersionRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentVersionRepository.cs index aff71feb63c3..59cfc12ee075 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentVersionRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentVersionRepository.cs @@ -1,35 +1,31 @@ -using System; -using System.Collections.Generic; -using System.Linq; using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class DocumentVersionRepository : IDocumentVersionRepository { - internal class DocumentVersionRepository : IDocumentVersionRepository + private readonly IScopeAccessor _scopeAccessor; + + public DocumentVersionRepository(IScopeAccessor scopeAccessor) => + _scopeAccessor = scopeAccessor ?? throw new ArgumentNullException(nameof(scopeAccessor)); + + /// + /// + /// Never includes current draft version.
+ /// Never includes current published version.
+ /// Never includes versions marked as "preventCleanup".
+ ///
+ public IReadOnlyCollection? GetDocumentVersionsEligibleForCleanup() { - private readonly IScopeAccessor _scopeAccessor; - - public DocumentVersionRepository(IScopeAccessor scopeAccessor) => - _scopeAccessor = scopeAccessor ?? throw new ArgumentNullException(nameof(scopeAccessor)); - - /// - /// - /// Never includes current draft version.
- /// Never includes current published version.
- /// Never includes versions marked as "preventCleanup".
- ///
- public IReadOnlyCollection? GetDocumentVersionsEligibleForCleanup() - { - Sql? query = _scopeAccessor.AmbientScope?.SqlContext.Sql(); + Sql? query = _scopeAccessor.AmbientScope?.SqlContext.Sql(); - query?.Select(@"umbracoDocument.nodeId as contentId, + query?.Select(@"umbracoDocument.nodeId as contentId, umbracoContent.contentTypeId as contentTypeId, umbracoContentVersion.id as versionId, umbracoContentVersion.userId as userId, @@ -38,39 +34,40 @@ public DocumentVersionRepository(IScopeAccessor scopeAccessor) => umbracoContentVersion.[current] as currentDraftVersion, umbracoContentVersion.preventCleanup as preventCleanup, umbracoUser.userName as username") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.Id, right => right.Id) - .LeftJoin() - .On(left => left.Id, right => right.UserId) - .Where(x => !x.Current) // Never delete current draft version - .Where(x => !x.PreventCleanup) // Never delete "pinned" versions - .Where(x => !x.Published); // Never delete published version - - return _scopeAccessor.AmbientScope?.Database.Fetch(query); - } + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.Id, right => right.Id) + .LeftJoin() + .On(left => left.Id, right => right.UserId) + .Where(x => !x.Current) // Never delete current draft version + .Where(x => !x.PreventCleanup) // Never delete "pinned" versions + .Where(x => !x.Published); // Never delete published version + + return _scopeAccessor.AmbientScope?.Database.Fetch(query); + } - /// - public IReadOnlyCollection? GetCleanupPolicies() - { - Sql? query = _scopeAccessor.AmbientScope?.SqlContext.Sql(); + /// + public IReadOnlyCollection? GetCleanupPolicies() + { + Sql? query = _scopeAccessor.AmbientScope?.SqlContext.Sql(); - query?.Select() - .From(); + query?.Select() + .From(); - return _scopeAccessor.AmbientScope?.Database.Fetch(query); - } + return _scopeAccessor.AmbientScope?.Database.Fetch(query); + } - /// - public IEnumerable? GetPagedItemsByContentId(int contentId, long pageIndex, int pageSize, out long totalRecords, int? languageId = null) - { - Sql? query = _scopeAccessor.AmbientScope?.SqlContext.Sql(); + /// + public IEnumerable? GetPagedItemsByContentId(int contentId, long pageIndex, int pageSize, + out long totalRecords, int? languageId = null) + { + Sql? query = _scopeAccessor.AmbientScope?.SqlContext.Sql(); - query?.Select(@"umbracoDocument.nodeId as contentId, + query?.Select(@"umbracoDocument.nodeId as contentId, umbracoContent.contentTypeId as contentTypeId, umbracoContentVersion.id as versionId, umbracoContentVersion.userId as userId, @@ -79,86 +76,87 @@ public DocumentVersionRepository(IScopeAccessor scopeAccessor) => umbracoContentVersion.[current] as currentDraftVersion, umbracoContentVersion.preventCleanup as preventCleanup, umbracoUser.userName as username") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.Id, right => right.Id) - .LeftJoin() - .On(left => left.Id, right => right.UserId) - .LeftJoin() - .On(left => left.VersionId, right => right.Id) - .Where(x => x.NodeId == contentId); - - // TODO: If there's not a better way to write this then we need a better way to write this. - query = languageId.HasValue - ? query?.Where(x => x.LanguageId == languageId.Value) - : query?.Where("umbracoContentVersionCultureVariation.languageId is null"); - - query = query?.OrderByDescending(x => x.Id); - - Page? page = _scopeAccessor.AmbientScope?.Database.Page(pageIndex + 1, pageSize, query); - - totalRecords = page?.TotalItems ?? 0; - - return page?.Items; - } + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.Id, right => right.Id) + .LeftJoin() + .On(left => left.Id, right => right.UserId) + .LeftJoin() + .On(left => left.VersionId, right => right.Id) + .Where(x => x.NodeId == contentId); + + // TODO: If there's not a better way to write this then we need a better way to write this. + query = languageId.HasValue + ? query?.Where(x => x.LanguageId == languageId.Value) + : query?.Where("umbracoContentVersionCultureVariation.languageId is null"); + + query = query?.OrderByDescending(x => x.Id); + + Page? page = + _scopeAccessor.AmbientScope?.Database.Page(pageIndex + 1, pageSize, query); + + totalRecords = page?.TotalItems ?? 0; + + return page?.Items; + } - /// - /// - /// Deletes in batches of - /// - public void DeleteVersions(IEnumerable versionIds) + /// + /// + /// Deletes in batches of + /// + public void DeleteVersions(IEnumerable versionIds) + { + foreach (IEnumerable group in versionIds.InGroupsOf(Constants.Sql.MaxParameterCount)) { - foreach (IEnumerable group in versionIds.InGroupsOf(Constants.Sql.MaxParameterCount)) - { - var groupedVersionIds = group.ToList(); - - /* Note: We had discussed doing this in a single SQL Command. - * If you can work out how to make that work with SQL CE, let me know! - * Can use test PerformContentVersionCleanup_WithNoKeepPeriods_DeletesEverythingExceptActive to try things out. - */ - - Sql? query = _scopeAccessor.AmbientScope?.SqlContext.Sql() - .Delete() - .WhereIn(x => x.VersionId, groupedVersionIds); - _scopeAccessor.AmbientScope?.Database.Execute(query); - - query = _scopeAccessor.AmbientScope?.SqlContext.Sql() - .Delete() - .WhereIn(x => x.VersionId, groupedVersionIds); - _scopeAccessor.AmbientScope?.Database.Execute(query); - - query = _scopeAccessor.AmbientScope?.SqlContext.Sql() - .Delete() - .WhereIn(x => x.Id, groupedVersionIds); - _scopeAccessor.AmbientScope?.Database.Execute(query); - - query = _scopeAccessor.AmbientScope?.SqlContext.Sql() - .Delete() - .WhereIn(x => x.Id, groupedVersionIds); - _scopeAccessor.AmbientScope?.Database.Execute(query); - } - } + var groupedVersionIds = group.ToList(); + + /* Note: We had discussed doing this in a single SQL Command. + * If you can work out how to make that work with SQL CE, let me know! + * Can use test PerformContentVersionCleanup_WithNoKeepPeriods_DeletesEverythingExceptActive to try things out. + */ - /// - public void SetPreventCleanup(int versionId, bool preventCleanup) - { Sql? query = _scopeAccessor.AmbientScope?.SqlContext.Sql() - .Update(x => x.Set(y => y.PreventCleanup, preventCleanup)) - .Where(x => x.Id == versionId); + .Delete() + .WhereIn(x => x.VersionId, groupedVersionIds); + _scopeAccessor.AmbientScope?.Database.Execute(query); + + query = _scopeAccessor.AmbientScope?.SqlContext.Sql() + .Delete() + .WhereIn(x => x.VersionId, groupedVersionIds); + _scopeAccessor.AmbientScope?.Database.Execute(query); + query = _scopeAccessor.AmbientScope?.SqlContext.Sql() + .Delete() + .WhereIn(x => x.Id, groupedVersionIds); + _scopeAccessor.AmbientScope?.Database.Execute(query); + + query = _scopeAccessor.AmbientScope?.SqlContext.Sql() + .Delete() + .WhereIn(x => x.Id, groupedVersionIds); _scopeAccessor.AmbientScope?.Database.Execute(query); } + } - /// - public ContentVersionMeta? Get(int versionId) - { - Sql? query = _scopeAccessor.AmbientScope?.SqlContext.Sql(); + /// + public void SetPreventCleanup(int versionId, bool preventCleanup) + { + Sql? query = _scopeAccessor.AmbientScope?.SqlContext.Sql() + .Update(x => x.Set(y => y.PreventCleanup, preventCleanup)) + .Where(x => x.Id == versionId); + + _scopeAccessor.AmbientScope?.Database.Execute(query); + } - query?.Select(@"umbracoDocument.nodeId as contentId, + /// + public ContentVersionMeta? Get(int versionId) + { + Sql? query = _scopeAccessor.AmbientScope?.SqlContext.Sql(); + + query?.Select(@"umbracoDocument.nodeId as contentId, umbracoContent.contentTypeId as contentTypeId, umbracoContentVersion.id as versionId, umbracoContentVersion.userId as userId, @@ -167,18 +165,17 @@ public void SetPreventCleanup(int versionId, bool preventCleanup) umbracoContentVersion.[current] as currentDraftVersion, umbracoContentVersion.preventCleanup as preventCleanup, umbracoUser.userName as username") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.Id, right => right.Id) - .LeftJoin() - .On(left => left.Id, right => right.UserId) - .Where(x => x.Id == versionId); - - return _scopeAccessor.AmbientScope?.Database.Single(query); - } + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.Id, right => right.Id) + .LeftJoin() + .On(left => left.Id, right => right.UserId) + .Where(x => x.Id == versionId); + + return _scopeAccessor.AmbientScope?.Database.Single(query); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DomainRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DomainRepository.cs index 0cc0bc44adbe..f3eff84f438f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DomainRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DomainRepository.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Data; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -9,199 +6,210 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement -{ - // TODO: We need to get a readonly ISO code for the domain assigned +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +// TODO: We need to get a readonly ISO code for the domain assigned - internal class DomainRepository : EntityRepositoryBase, IDomainRepository +internal class DomainRepository : EntityRepositoryBase, IDomainRepository +{ + public DomainRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) { - public DomainRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { } + } - protected override IRepositoryCachePolicy CreateCachePolicy() - { - return new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ false); - } + public IDomain? GetByName(string domainName) => + GetMany()?.FirstOrDefault(x => x.DomainName.InvariantEquals(domainName)); - protected override IDomain? PerformGet(int id) - { - //use the underlying GetAll which will force cache all domains - return GetMany()?.FirstOrDefault(x => x.Id == id); - } + public bool Exists(string domainName) => GetMany()?.Any(x => x.DomainName.InvariantEquals(domainName)) ?? false; - protected override IEnumerable PerformGetAll(params int[]? ids) - { - var sql = GetBaseQuery(false).Where(x => x.Id > 0); - if (ids?.Any() ?? false) - { - sql.WhereIn(x => x.Id, ids); - } + public IEnumerable GetAll(bool includeWildcards) => + GetMany().Where(x => includeWildcards || x.IsWildcard == false); - return Database.Fetch(sql).Select(ConvertFromDto); - } + public IEnumerable GetAssignedDomains(int contentId, bool includeWildcards) => + GetMany() + .Where(x => x.RootContentId == contentId) + .Where(x => includeWildcards || x.IsWildcard == false); + + protected override IRepositoryCachePolicy CreateCachePolicy() => + new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ + false); - protected override IEnumerable PerformGetByQuery(IQuery query) + protected override IDomain? PerformGet(int id) => + //use the underlying GetAll which will force cache all domains + GetMany()?.FirstOrDefault(x => x.Id == id); + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = GetBaseQuery(false).Where(x => x.Id > 0); + if (ids?.Any() ?? false) { - throw new NotSupportedException("This repository does not support this method"); + sql.WhereIn(x => x.Id, ids); } - protected override Sql GetBaseQuery(bool isCount) - { - var sql = Sql(); - if (isCount) - { - sql.SelectCount().From(); - } - else - { - sql.Select("umbracoDomain.*, umbracoLanguage.languageISOCode") - .From() - .LeftJoin() - .On(dto => dto.DefaultLanguage, dto => dto.Id); - } + return Database.Fetch(sql).Select(ConvertFromDto); + } - return sql; - } + protected override IEnumerable PerformGetByQuery(IQuery query) => + throw new NotSupportedException("This repository does not support this method"); - protected override string GetBaseWhereClause() + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); + if (isCount) { - return $"{Constants.DatabaseSchema.Tables.Domain}.id = @id"; + sql.SelectCount().From(); } - - protected override IEnumerable GetDeleteClauses() + else { - var list = new List - { - "DELETE FROM umbracoDomain WHERE id = @id" - }; - return list; + sql.Select("umbracoDomain.*, umbracoLanguage.languageISOCode") + .From() + .LeftJoin() + .On(dto => dto.DefaultLanguage, dto => dto.Id); } - protected override void PersistNewItem(IDomain entity) - { - var exists = Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoDomain WHERE domainName = @domainName", new { domainName = entity.DomainName }); - if (exists > 0) throw new DuplicateNameException(string.Format("The domain name {0} is already assigned", entity.DomainName)); - - if (entity.RootContentId.HasValue) - { - var contentExists = Database.ExecuteScalar($"SELECT COUNT(*) FROM {Cms.Core.Constants.DatabaseSchema.Tables.Content} WHERE nodeId = @id", new { id = entity.RootContentId.Value }); - if (contentExists == 0) throw new NullReferenceException("No content exists with id " + entity.RootContentId.Value); - } - - if (entity.LanguageId.HasValue) - { - var languageExists = Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoLanguage WHERE id = @id", new { id = entity.LanguageId.Value }); - if (languageExists == 0) throw new NullReferenceException("No language exists with id " + entity.LanguageId.Value); - } - - entity.AddingEntity(); - - var factory = new DomainModelFactory(); - var dto = factory.BuildDto(entity); + return sql; + } - var id = Convert.ToInt32(Database.Insert(dto)); - entity.Id = id; + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Domain}.id = @id"; - //if the language changed, we need to resolve the ISO code! - if (entity.LanguageId.HasValue) - { - ((UmbracoDomain)entity).LanguageIsoCode = Database.ExecuteScalar("SELECT languageISOCode FROM umbracoLanguage WHERE id=@langId", new { langId = entity.LanguageId }); - } + protected override IEnumerable GetDeleteClauses() + { + var list = new List {"DELETE FROM umbracoDomain WHERE id = @id"}; + return list; + } - entity.ResetDirtyProperties(); + protected override void PersistNewItem(IDomain entity) + { + var exists = Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoDomain WHERE domainName = @domainName", + new {domainName = entity.DomainName}); + if (exists > 0) + { + throw new DuplicateNameException( + string.Format("The domain name {0} is already assigned", entity.DomainName)); } - protected override void PersistUpdatedItem(IDomain entity) + if (entity.RootContentId.HasValue) { - entity.UpdatingEntity(); - - var exists = Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoDomain WHERE domainName = @domainName AND umbracoDomain.id <> @id", - new { domainName = entity.DomainName, id = entity.Id }); - //ensure there is no other domain with the same name on another entity - if (exists > 0) throw new DuplicateNameException(string.Format("The domain name {0} is already assigned", entity.DomainName)); - - if (entity.RootContentId.HasValue) + var contentExists = Database.ExecuteScalar( + $"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.Content} WHERE nodeId = @id", + new {id = entity.RootContentId.Value}); + if (contentExists == 0) { - var contentExists = Database.ExecuteScalar($"SELECT COUNT(*) FROM {Cms.Core.Constants.DatabaseSchema.Tables.Content} WHERE nodeId = @id", new { id = entity.RootContentId.Value }); - if (contentExists == 0) throw new NullReferenceException("No content exists with id " + entity.RootContentId.Value); + throw new NullReferenceException("No content exists with id " + entity.RootContentId.Value); } + } - if (entity.LanguageId.HasValue) + if (entity.LanguageId.HasValue) + { + var languageExists = Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoLanguage WHERE id = @id", + new {id = entity.LanguageId.Value}); + if (languageExists == 0) { - var languageExists = Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoLanguage WHERE id = @id", new { id = entity.LanguageId.Value }); - if (languageExists == 0) throw new NullReferenceException("No language exists with id " + entity.LanguageId.Value); + throw new NullReferenceException("No language exists with id " + entity.LanguageId.Value); } + } - var factory = new DomainModelFactory(); - var dto = factory.BuildDto(entity); - - Database.Update(dto); + entity.AddingEntity(); - //if the language changed, we need to resolve the ISO code! - if (entity.WasPropertyDirty("LanguageId")) - { - ((UmbracoDomain)entity).LanguageIsoCode = Database.ExecuteScalar("SELECT languageISOCode FROM umbracoLanguage WHERE id=@langId", new {langId = entity.LanguageId}); - } + var factory = new DomainModelFactory(); + DomainDto dto = factory.BuildDto(entity); - entity.ResetDirtyProperties(); - } + var id = Convert.ToInt32(Database.Insert(dto)); + entity.Id = id; - public IDomain? GetByName(string domainName) + //if the language changed, we need to resolve the ISO code! + if (entity.LanguageId.HasValue) { - return GetMany()?.FirstOrDefault(x => x.DomainName.InvariantEquals(domainName)); + ((UmbracoDomain)entity).LanguageIsoCode = Database.ExecuteScalar( + "SELECT languageISOCode FROM umbracoLanguage WHERE id=@langId", new {langId = entity.LanguageId}); } - public bool Exists(string domainName) + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IDomain entity) + { + entity.UpdatingEntity(); + + var exists = Database.ExecuteScalar( + "SELECT COUNT(*) FROM umbracoDomain WHERE domainName = @domainName AND umbracoDomain.id <> @id", + new {domainName = entity.DomainName, id = entity.Id}); + //ensure there is no other domain with the same name on another entity + if (exists > 0) { - return GetMany()?.Any(x => x.DomainName.InvariantEquals(domainName)) ?? false; + throw new DuplicateNameException( + string.Format("The domain name {0} is already assigned", entity.DomainName)); } - public IEnumerable GetAll(bool includeWildcards) + if (entity.RootContentId.HasValue) { - return GetMany().Where(x => includeWildcards || x.IsWildcard == false); + var contentExists = Database.ExecuteScalar( + $"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.Content} WHERE nodeId = @id", + new {id = entity.RootContentId.Value}); + if (contentExists == 0) + { + throw new NullReferenceException("No content exists with id " + entity.RootContentId.Value); + } } - public IEnumerable GetAssignedDomains(int contentId, bool includeWildcards) + if (entity.LanguageId.HasValue) { - return GetMany() - .Where(x => x.RootContentId == contentId) - .Where(x => includeWildcards || x.IsWildcard == false); + var languageExists = Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoLanguage WHERE id = @id", + new {id = entity.LanguageId.Value}); + if (languageExists == 0) + { + throw new NullReferenceException("No language exists with id " + entity.LanguageId.Value); + } } - private IDomain ConvertFromDto(DomainDto dto) + var factory = new DomainModelFactory(); + DomainDto dto = factory.BuildDto(entity); + + Database.Update(dto); + + //if the language changed, we need to resolve the ISO code! + if (entity.WasPropertyDirty("LanguageId")) { - var factory = new DomainModelFactory(); - var entity = factory.BuildEntity(dto); - return entity; + ((UmbracoDomain)entity).LanguageIsoCode = Database.ExecuteScalar( + "SELECT languageISOCode FROM umbracoLanguage WHERE id=@langId", new {langId = entity.LanguageId}); } - internal class DomainModelFactory - { + entity.ResetDirtyProperties(); + } - public IDomain BuildEntity(DomainDto dto) + private IDomain ConvertFromDto(DomainDto dto) + { + var factory = new DomainModelFactory(); + IDomain entity = factory.BuildEntity(dto); + return entity; + } + + internal class DomainModelFactory + { + public IDomain BuildEntity(DomainDto dto) + { + var domain = new UmbracoDomain(dto.DomainName, dto.IsoCode) { - var domain = new UmbracoDomain(dto.DomainName, dto.IsoCode) - { - Id = dto.Id, - LanguageId = dto.DefaultLanguage, - RootContentId = dto.RootStructureId - }; - // reset dirty initial properties (U4-1946) - domain.ResetDirtyProperties(false); - return domain; - } + Id = dto.Id, LanguageId = dto.DefaultLanguage, RootContentId = dto.RootStructureId + }; + // reset dirty initial properties (U4-1946) + domain.ResetDirtyProperties(false); + return domain; + } - public DomainDto BuildDto(IDomain entity) + public DomainDto BuildDto(IDomain entity) + { + var dto = new DomainDto { - var dto = new DomainDto { DefaultLanguage = entity.LanguageId, DomainName = entity.DomainName, Id = entity.Id, RootStructureId = entity.RootContentId }; - return dto; - } + DefaultLanguage = entity.LanguageId, + DomainName = entity.DomainName, + Id = entity.Id, + RootStructureId = entity.RootContentId + }; + return dto; } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityContainerRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityContainerRepository.cs index 468b83062cc8..025354031f02 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityContainerRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityContainerRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -12,336 +9,335 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// An internal repository for managing entity containers such as doc type, media type, data type containers. +/// +internal class EntityContainerRepository : EntityRepositoryBase, IEntityContainerRepository { - /// - /// An internal repository for managing entity containers such as doc type, media type, data type containers. - /// - internal class EntityContainerRepository : EntityRepositoryBase, IEntityContainerRepository + public EntityContainerRepository(IScopeAccessor scopeAccessor, AppCaches cache, + ILogger logger, Guid containerObjectType) + : base(scopeAccessor, cache, logger) { - public EntityContainerRepository(IScopeAccessor scopeAccessor, AppCaches cache, - ILogger logger, Guid containerObjectType) - : base(scopeAccessor, cache, logger) + Guid[] allowedContainers = { - Guid[] allowedContainers = new[] - { - Constants.ObjectTypes.DocumentTypeContainer, Constants.ObjectTypes.MediaTypeContainer, - Constants.ObjectTypes.DataTypeContainer - }; - NodeObjectTypeId = containerObjectType; - if (allowedContainers.Contains(NodeObjectTypeId) == false) - { - throw new InvalidOperationException("No container type exists with ID: " + NodeObjectTypeId); - } + Constants.ObjectTypes.DocumentTypeContainer, Constants.ObjectTypes.MediaTypeContainer, + Constants.ObjectTypes.DataTypeContainer + }; + NodeObjectTypeId = containerObjectType; + if (allowedContainers.Contains(NodeObjectTypeId) == false) + { + throw new InvalidOperationException("No container type exists with ID: " + NodeObjectTypeId); } + } - protected Guid NodeObjectTypeId { get; } + protected Guid NodeObjectTypeId { get; } - // never cache - protected override IRepositoryCachePolicy CreateCachePolicy() => - NoCacheRepositoryCachePolicy.Instance; + // temp - so we don't have to implement GetByQuery + public EntityContainer? Get(Guid id) + { + Sql sql = GetBaseQuery(false).Where("UniqueId=@uniqueId", new {uniqueId = id}); - protected override EntityContainer? PerformGet(int id) - { - Sql sql = GetBaseQuery(false) - .Where(GetBaseWhereClause(), new { id, NodeObjectType = NodeObjectTypeId }); + NodeDto? nodeDto = Database.Fetch(sql).FirstOrDefault(); + return nodeDto == null ? null : CreateEntity(nodeDto); + } - NodeDto? nodeDto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); - return nodeDto == null ? null : CreateEntity(nodeDto); - } + public IEnumerable Get(string name, int level) + { + Sql sql = GetBaseQuery(false) + .Where("text=@name AND level=@level AND nodeObjectType=@umbracoObjectTypeId", + new {name, level, umbracoObjectTypeId = NodeObjectTypeId}); + return Database.Fetch(sql).Select(CreateEntity); + } - // temp - so we don't have to implement GetByQuery - public EntityContainer? Get(Guid id) - { - Sql sql = GetBaseQuery(false).Where("UniqueId=@uniqueId", new { uniqueId = id }); + // never cache + protected override IRepositoryCachePolicy CreateCachePolicy() => + NoCacheRepositoryCachePolicy.Instance; - NodeDto? nodeDto = Database.Fetch(sql).FirstOrDefault(); - return nodeDto == null ? null : CreateEntity(nodeDto); - } + protected override EntityContainer? PerformGet(int id) + { + Sql sql = GetBaseQuery(false) + .Where(GetBaseWhereClause(), new {id, NodeObjectType = NodeObjectTypeId}); - public IEnumerable Get(string name, int level) - { - Sql sql = GetBaseQuery(false) - .Where("text=@name AND level=@level AND nodeObjectType=@umbracoObjectTypeId", - new { name, level, umbracoObjectTypeId = NodeObjectTypeId }); - return Database.Fetch(sql).Select(CreateEntity); - } + NodeDto? nodeDto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); + return nodeDto == null ? null : CreateEntity(nodeDto); + } - protected override IEnumerable PerformGetAll(params int[]? ids) + protected override IEnumerable PerformGetAll(params int[]? ids) + { + if (ids?.Any() ?? false) { - if (ids?.Any() ?? false) - { - return Database.FetchByGroups(ids, Constants.Sql.MaxParameterCount, batch => - GetBaseQuery(false) - .Where(x => x.NodeObjectType == NodeObjectTypeId) - .WhereIn(x => x.NodeId, batch)) - .Select(CreateEntity); - } + return Database.FetchByGroups(ids, Constants.Sql.MaxParameterCount, batch => + GetBaseQuery(false) + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .WhereIn(x => x.NodeId, batch)) + .Select(CreateEntity); + } - // else + // else - Sql sql = GetBaseQuery(false) - .Where("nodeObjectType=@umbracoObjectTypeId", new { umbracoObjectTypeId = NodeObjectTypeId }) - .OrderBy(x => x.Level); + Sql sql = GetBaseQuery(false) + .Where("nodeObjectType=@umbracoObjectTypeId", new {umbracoObjectTypeId = NodeObjectTypeId}) + .OrderBy(x => x.Level); - return Database.Fetch(sql).Select(CreateEntity); - } + return Database.Fetch(sql).Select(CreateEntity); + } - protected override IEnumerable PerformGetByQuery(IQuery query) => - throw new NotImplementedException(); + protected override IEnumerable PerformGetByQuery(IQuery query) => + throw new NotImplementedException(); - private static EntityContainer CreateEntity(NodeDto nodeDto) + private static EntityContainer CreateEntity(NodeDto nodeDto) + { + if (nodeDto.NodeObjectType.HasValue == false) { - if (nodeDto.NodeObjectType.HasValue == false) - { - throw new InvalidOperationException("Node with id " + nodeDto.NodeId + " has no object type."); - } + throw new InvalidOperationException("Node with id " + nodeDto.NodeId + " has no object type."); + } - // throws if node is not a container - Guid containedObjectType = EntityContainer.GetContainedObjectType(nodeDto.NodeObjectType.Value); + // throws if node is not a container + Guid containedObjectType = EntityContainer.GetContainedObjectType(nodeDto.NodeObjectType.Value); - var entity = new EntityContainer(nodeDto.NodeId, nodeDto.UniqueId, - nodeDto.ParentId, nodeDto.Path, nodeDto.Level, nodeDto.SortOrder, - containedObjectType, - nodeDto.Text, nodeDto.UserId ?? Constants.Security.UnknownUserId); + var entity = new EntityContainer(nodeDto.NodeId, nodeDto.UniqueId, + nodeDto.ParentId, nodeDto.Path, nodeDto.Level, nodeDto.SortOrder, + containedObjectType, + nodeDto.Text, nodeDto.UserId ?? Constants.Security.UnknownUserId); - // reset dirty initial properties (U4-1946) - entity.ResetDirtyProperties(false); + // reset dirty initial properties (U4-1946) + entity.ResetDirtyProperties(false); - return entity; - } + return entity; + } - protected override Sql GetBaseQuery(bool isCount) + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); + if (isCount) { - Sql sql = Sql(); - if (isCount) - { - sql.SelectCount(); - } - else - { - sql.SelectAll(); - } - - sql.From(); - return sql; + sql.SelectCount(); + } + else + { + sql.SelectAll(); } - protected override string GetBaseWhereClause() => "umbracoNode.id = @id and nodeObjectType = @NodeObjectType"; + sql.From(); + return sql; + } + + protected override string GetBaseWhereClause() => "umbracoNode.id = @id and nodeObjectType = @NodeObjectType"; - protected override IEnumerable GetDeleteClauses() => throw new NotImplementedException(); + protected override IEnumerable GetDeleteClauses() => throw new NotImplementedException(); - protected override void PersistDeletedItem(EntityContainer entity) + protected override void PersistDeletedItem(EntityContainer entity) + { + if (entity == null) { - if (entity == null) - { - throw new ArgumentNullException(nameof(entity)); - } + throw new ArgumentNullException(nameof(entity)); + } - EnsureContainerType(entity); + EnsureContainerType(entity); - NodeDto nodeDto = Database.FirstOrDefault(Sql().SelectAll() - .From() - .Where(dto => dto.NodeId == entity.Id && dto.NodeObjectType == entity.ContainerObjectType)); + NodeDto nodeDto = Database.FirstOrDefault(Sql().SelectAll() + .From() + .Where(dto => dto.NodeId == entity.Id && dto.NodeObjectType == entity.ContainerObjectType)); - if (nodeDto == null) - { - return; - } - - // move children to the parent so they are not orphans - List childDtos = Database.Fetch(Sql().SelectAll() - .From() - .Where( - "parentID=@parentID AND (nodeObjectType=@containedObjectType OR nodeObjectType=@containerObjectType)", - new - { - parentID = entity.Id, - containedObjectType = entity.ContainedObjectType, - containerObjectType = entity.ContainerObjectType - })); - - foreach (NodeDto childDto in childDtos) - { - childDto.ParentId = nodeDto.ParentId; - Database.Update(childDto); - } + if (nodeDto == null) + { + return; + } - // delete - Database.Delete(nodeDto); + // move children to the parent so they are not orphans + List childDtos = Database.Fetch(Sql().SelectAll() + .From() + .Where( + "parentID=@parentID AND (nodeObjectType=@containedObjectType OR nodeObjectType=@containerObjectType)", + new + { + parentID = entity.Id, + containedObjectType = entity.ContainedObjectType, + containerObjectType = entity.ContainerObjectType + })); - entity.DeleteDate = DateTime.Now; + foreach (NodeDto childDto in childDtos) + { + childDto.ParentId = nodeDto.ParentId; + Database.Update(childDto); } - protected override void PersistNewItem(EntityContainer entity) + // delete + Database.Delete(nodeDto); + + entity.DeleteDate = DateTime.Now; + } + + protected override void PersistNewItem(EntityContainer entity) + { + if (entity == null) { - if (entity == null) - { - throw new ArgumentNullException(nameof(entity)); - } + throw new ArgumentNullException(nameof(entity)); + } - EnsureContainerType(entity); + EnsureContainerType(entity); - if (entity.Name == null) - { - throw new InvalidOperationException("Entity name can't be null."); - } + if (entity.Name == null) + { + throw new InvalidOperationException("Entity name can't be null."); + } - if (string.IsNullOrWhiteSpace(entity.Name)) - { - throw new InvalidOperationException( - "Entity name can't be empty or consist only of white-space characters."); - } + if (string.IsNullOrWhiteSpace(entity.Name)) + { + throw new InvalidOperationException( + "Entity name can't be empty or consist only of white-space characters."); + } - entity.Name = entity.Name.Trim(); + entity.Name = entity.Name.Trim(); - // guard against duplicates - NodeDto nodeDto = Database.FirstOrDefault(Sql().SelectAll() + // guard against duplicates + NodeDto nodeDto = Database.FirstOrDefault(Sql().SelectAll() + .From() + .Where(dto => + dto.ParentId == entity.ParentId && dto.Text == entity.Name && + dto.NodeObjectType == entity.ContainerObjectType)); + if (nodeDto != null) + { + throw new InvalidOperationException("A container with the same name already exists."); + } + + // create + var level = 0; + var path = "-1"; + if (entity.ParentId > -1) + { + NodeDto parentDto = Database.FirstOrDefault(Sql().SelectAll() .From() .Where(dto => - dto.ParentId == entity.ParentId && dto.Text == entity.Name && - dto.NodeObjectType == entity.ContainerObjectType)); - if (nodeDto != null) - { - throw new InvalidOperationException("A container with the same name already exists."); - } + dto.NodeId == entity.ParentId && dto.NodeObjectType == entity.ContainerObjectType)); - // create - var level = 0; - var path = "-1"; - if (entity.ParentId > -1) + if (parentDto == null) { - NodeDto parentDto = Database.FirstOrDefault(Sql().SelectAll() - .From() - .Where(dto => - dto.NodeId == entity.ParentId && dto.NodeObjectType == entity.ContainerObjectType)); + throw new InvalidOperationException("Could not find parent container with id " + entity.ParentId); + } - if (parentDto == null) - { - throw new InvalidOperationException("Could not find parent container with id " + entity.ParentId); - } + level = parentDto.Level; + path = parentDto.Path; + } - level = parentDto.Level; - path = parentDto.Path; - } + // note: sortOrder is NOT managed and always zero for containers - // note: sortOrder is NOT managed and always zero for containers + nodeDto = new NodeDto + { + CreateDate = DateTime.Now, + Level = Convert.ToInt16(level + 1), + NodeObjectType = entity.ContainerObjectType, + ParentId = entity.ParentId, + Path = path, + SortOrder = 0, + Text = entity.Name, + UserId = entity.CreatorId, + UniqueId = entity.Key + }; + + // insert, get the id, update the path with the id + var id = Convert.ToInt32(Database.Insert(nodeDto)); + nodeDto.Path = nodeDto.Path + "," + nodeDto.NodeId; + Database.Save(nodeDto); + + // refresh the entity + entity.Id = id; + entity.Path = nodeDto.Path; + entity.Level = nodeDto.Level; + entity.SortOrder = 0; + entity.CreateDate = nodeDto.CreateDate; + entity.ResetDirtyProperties(); + } - nodeDto = new NodeDto - { - CreateDate = DateTime.Now, - Level = Convert.ToInt16(level + 1), - NodeObjectType = entity.ContainerObjectType, - ParentId = entity.ParentId, - Path = path, - SortOrder = 0, - Text = entity.Name, - UserId = entity.CreatorId, - UniqueId = entity.Key - }; - - // insert, get the id, update the path with the id - var id = Convert.ToInt32(Database.Insert(nodeDto)); - nodeDto.Path = nodeDto.Path + "," + nodeDto.NodeId; - Database.Save(nodeDto); - - // refresh the entity - entity.Id = id; - entity.Path = nodeDto.Path; - entity.Level = nodeDto.Level; - entity.SortOrder = 0; - entity.CreateDate = nodeDto.CreateDate; - entity.ResetDirtyProperties(); + // beware! does NOT manage descendants in case of a new parent + // + protected override void PersistUpdatedItem(EntityContainer entity) + { + if (entity == null) + { + throw new ArgumentNullException(nameof(entity)); } - // beware! does NOT manage descendants in case of a new parent - // - protected override void PersistUpdatedItem(EntityContainer entity) - { - if (entity == null) - { - throw new ArgumentNullException(nameof(entity)); - } + EnsureContainerType(entity); - EnsureContainerType(entity); + if (entity.Name == null) + { + throw new InvalidOperationException("Entity name can't be null."); + } - if (entity.Name == null) - { - throw new InvalidOperationException("Entity name can't be null."); - } + if (string.IsNullOrWhiteSpace(entity.Name)) + { + throw new InvalidOperationException( + "Entity name can't be empty or consist only of white-space characters."); + } - if (string.IsNullOrWhiteSpace(entity.Name)) - { - throw new InvalidOperationException( - "Entity name can't be empty or consist only of white-space characters."); - } + entity.Name = entity.Name.Trim(); - entity.Name = entity.Name.Trim(); + // find container to update + NodeDto nodeDto = Database.FirstOrDefault(Sql().SelectAll() + .From() + .Where(dto => dto.NodeId == entity.Id && dto.NodeObjectType == entity.ContainerObjectType)); + if (nodeDto == null) + { + throw new InvalidOperationException("Could not find container with id " + entity.Id); + } - // find container to update - NodeDto nodeDto = Database.FirstOrDefault(Sql().SelectAll() - .From() - .Where(dto => dto.NodeId == entity.Id && dto.NodeObjectType == entity.ContainerObjectType)); - if (nodeDto == null) - { - throw new InvalidOperationException("Could not find container with id " + entity.Id); - } + // guard against duplicates + NodeDto dupNodeDto = Database.FirstOrDefault(Sql().SelectAll() + .From() + .Where(dto => + dto.ParentId == entity.ParentId && dto.Text == entity.Name && + dto.NodeObjectType == entity.ContainerObjectType)); + if (dupNodeDto != null && dupNodeDto.NodeId != nodeDto.NodeId) + { + throw new InvalidOperationException("A container with the same name already exists."); + } - // guard against duplicates - NodeDto dupNodeDto = Database.FirstOrDefault(Sql().SelectAll() - .From() - .Where(dto => - dto.ParentId == entity.ParentId && dto.Text == entity.Name && - dto.NodeObjectType == entity.ContainerObjectType)); - if (dupNodeDto != null && dupNodeDto.NodeId != nodeDto.NodeId) + // update + nodeDto.Text = entity.Name; + if (nodeDto.ParentId != entity.ParentId) + { + nodeDto.Level = 0; + nodeDto.Path = "-1"; + if (entity.ParentId > -1) { - throw new InvalidOperationException("A container with the same name already exists."); - } + NodeDto parent = Database.FirstOrDefault(Sql().SelectAll() + .From() + .Where(dto => + dto.NodeId == entity.ParentId && dto.NodeObjectType == entity.ContainerObjectType)); - // update - nodeDto.Text = entity.Name; - if (nodeDto.ParentId != entity.ParentId) - { - nodeDto.Level = 0; - nodeDto.Path = "-1"; - if (entity.ParentId > -1) + if (parent == null) { - NodeDto parent = Database.FirstOrDefault(Sql().SelectAll() - .From() - .Where(dto => - dto.NodeId == entity.ParentId && dto.NodeObjectType == entity.ContainerObjectType)); - - if (parent == null) - { - throw new InvalidOperationException( - "Could not find parent container with id " + entity.ParentId); - } - - nodeDto.Level = Convert.ToInt16(parent.Level + 1); - nodeDto.Path = parent.Path + "," + nodeDto.NodeId; + throw new InvalidOperationException( + "Could not find parent container with id " + entity.ParentId); } - nodeDto.ParentId = entity.ParentId; + nodeDto.Level = Convert.ToInt16(parent.Level + 1); + nodeDto.Path = parent.Path + "," + nodeDto.NodeId; } - // note: sortOrder is NOT managed and always zero for containers + nodeDto.ParentId = entity.ParentId; + } - // update - Database.Update(nodeDto); + // note: sortOrder is NOT managed and always zero for containers - // refresh the entity - entity.Path = nodeDto.Path; - entity.Level = nodeDto.Level; - entity.SortOrder = 0; - entity.ResetDirtyProperties(); - } + // update + Database.Update(nodeDto); + + // refresh the entity + entity.Path = nodeDto.Path; + entity.Level = nodeDto.Level; + entity.SortOrder = 0; + entity.ResetDirtyProperties(); + } - private void EnsureContainerType(EntityContainer entity) + private void EnsureContainerType(EntityContainer entity) + { + if (entity.ContainerObjectType != NodeObjectTypeId) { - if (entity.ContainerObjectType != NodeObjectTypeId) - { - throw new InvalidOperationException("The container type does not match the repository object type"); - } + throw new InvalidOperationException("The container type does not match the repository object type"); } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs index ef0f02540e3c..c904b5b440d3 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs @@ -1,13 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Persistence.Querying; -using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Querying; @@ -16,720 +12,769 @@ using Umbraco.Extensions; using static Umbraco.Cms.Core.Persistence.SqlExtensionsStatics; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents the EntityRepository used to query entity objects. +/// +/// +/// Limited to objects that have a corresponding node (in umbracoNode table). +/// Returns objects, i.e. lightweight representation of entities. +/// +internal class EntityRepository : RepositoryBase, IEntityRepositoryExtended { - /// - /// Represents the EntityRepository used to query entity objects. - /// - /// - /// Limited to objects that have a corresponding node (in umbracoNode table). - /// Returns objects, i.e. lightweight representation of entities. - /// - internal class EntityRepository : RepositoryBase, IEntityRepositoryExtended + public EntityRepository(IScopeAccessor scopeAccessor, AppCaches appCaches) + : base(scopeAccessor, appCaches) { - public EntityRepository(IScopeAccessor scopeAccessor, AppCaches appCaches) - : base(scopeAccessor, appCaches) - { - } + } - #region Repository + #region Repository - public IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectType, long pageIndex, int pageSize, out long totalRecords, - IQuery? filter, Ordering? ordering) - { - return GetPagedResultsByQuery(query, new[] { objectType }, pageIndex, pageSize, out totalRecords, filter, ordering); - } + public IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectType, + long pageIndex, int pageSize, out long totalRecords, + IQuery? filter, Ordering? ordering) => + GetPagedResultsByQuery(query, new[] {objectType}, pageIndex, pageSize, out totalRecords, filter, ordering); - // get a page of entities - public IEnumerable GetPagedResultsByQuery(IQuery query, Guid[] objectTypes, long pageIndex, int pageSize, out long totalRecords, - IQuery? filter, Ordering? ordering, Action>? sqlCustomization = null) + // get a page of entities + public IEnumerable GetPagedResultsByQuery(IQuery query, Guid[] objectTypes, + long pageIndex, int pageSize, out long totalRecords, + IQuery? filter, Ordering? ordering, Action>? sqlCustomization = null) + { + var isContent = objectTypes.Any(objectType => + objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint); + var isMedia = objectTypes.Any(objectType => objectType == Constants.ObjectTypes.Media); + var isMember = objectTypes.Any(objectType => objectType == Constants.ObjectTypes.Member); + + Sql sql = GetBaseWhere(isContent, isMedia, isMember, false, s => { - var isContent = objectTypes.Any(objectType => objectType == Cms.Core.Constants.ObjectTypes.Document || objectType == Cms.Core.Constants.ObjectTypes.DocumentBlueprint); - var isMedia = objectTypes.Any(objectType => objectType == Cms.Core.Constants.ObjectTypes.Media); - var isMember = objectTypes.Any(objectType => objectType == Cms.Core.Constants.ObjectTypes.Member); + sqlCustomization?.Invoke(s); - Sql sql = GetBaseWhere(isContent, isMedia, isMember, false, s => + if (filter != null) { - sqlCustomization?.Invoke(s); - - if (filter != null) + foreach (Tuple filterClause in filter.GetWhereClauses()) { - foreach (Tuple filterClause in filter.GetWhereClauses()) - { - s.Where(filterClause.Item1, filterClause.Item2); - } + s.Where(filterClause.Item1, filterClause.Item2); } - }, objectTypes); - - ordering = ordering ?? Ordering.ByDefault(); - - var translator = new SqlTranslator(sql, query); - sql = translator.Translate(); - sql = AddGroupBy(isContent, isMedia, isMember, sql, ordering.IsEmpty); - - if (!ordering.IsEmpty) - { - // apply ordering - ApplyOrdering(ref sql, ordering); } + }, objectTypes); - // TODO: we should be able to do sql = sql.OrderBy(x => Alias(x.NodeId, "NodeId")); but we can't because the OrderBy extension don't support Alias currently - // no matter what we always must have node id ordered at the end - sql = ordering.Direction == Direction.Ascending ? sql.OrderBy("NodeId") : sql.OrderByDescending("NodeId"); - - // for content we must query for ContentEntityDto entities to produce the correct culture variant entity names - var pageIndexToFetch = pageIndex + 1; - IEnumerable dtos; - var page = Database.Page(pageIndexToFetch, pageSize, sql); - dtos = page.Items; - totalRecords = page.TotalItems; + ordering = ordering ?? Ordering.ByDefault(); - var entities = dtos.Select(BuildEntity).ToArray(); + var translator = new SqlTranslator(sql, query); + sql = translator.Translate(); + sql = AddGroupBy(isContent, isMedia, isMember, sql, ordering.IsEmpty); - BuildVariants(entities.OfType()); - - return entities; - } - - public IEntitySlim? Get(Guid key) + if (!ordering.IsEmpty) { - var sql = GetBaseWhere(false, false, false, false, key); - var dto = Database.FirstOrDefault(sql); - return dto == null ? null : BuildEntity(dto); + // apply ordering + ApplyOrdering(ref sql, ordering); } + // TODO: we should be able to do sql = sql.OrderBy(x => Alias(x.NodeId, "NodeId")); but we can't because the OrderBy extension don't support Alias currently + // no matter what we always must have node id ordered at the end + sql = ordering.Direction == Direction.Ascending ? sql.OrderBy("NodeId") : sql.OrderByDescending("NodeId"); - private IEntitySlim? GetEntity(Sql sql, bool isContent, bool isMedia, bool isMember) - { - // isContent is going to return a 1:M result now with the variants so we need to do different things - if (isContent) - { - var cdtos = Database.Fetch(sql); + // for content we must query for ContentEntityDto entities to produce the correct culture variant entity names + var pageIndexToFetch = pageIndex + 1; + IEnumerable dtos; + Page? page = Database.Page(pageIndexToFetch, pageSize, sql); + dtos = page.Items; + totalRecords = page.TotalItems; - return cdtos.Count == 0 ? null : BuildVariants(BuildDocumentEntity(cdtos[0])); - } + EntitySlim[] entities = dtos.Select(BuildEntity).ToArray(); - var dto = isMedia - ? Database.FirstOrDefault(sql) - : Database.FirstOrDefault(sql); + BuildVariants(entities.OfType()); - if (dto == null) return null; + return entities; + } - var entity = BuildEntity(dto); + public IEntitySlim? Get(Guid key) + { + Sql sql = GetBaseWhere(false, false, false, false, key); + BaseDto? dto = Database.FirstOrDefault(sql); + return dto == null ? null : BuildEntity(dto); + } - return entity; - } - public IEntitySlim? Get(Guid key, Guid objectTypeId) + private IEntitySlim? GetEntity(Sql sql, bool isContent, bool isMedia, bool isMember) + { + // isContent is going to return a 1:M result now with the variants so we need to do different things + if (isContent) { - var isContent = objectTypeId == Cms.Core.Constants.ObjectTypes.Document || objectTypeId == Cms.Core.Constants.ObjectTypes.DocumentBlueprint; - var isMedia = objectTypeId == Cms.Core.Constants.ObjectTypes.Media; - var isMember = objectTypeId == Cms.Core.Constants.ObjectTypes.Member; + List? cdtos = Database.Fetch(sql); - var sql = GetFullSqlForEntityType(isContent, isMedia, isMember, objectTypeId, key); - return GetEntity(sql, isContent, isMedia, isMember); + return cdtos.Count == 0 ? null : BuildVariants(BuildDocumentEntity(cdtos[0])); } - public IEntitySlim? Get(int id) + BaseDto? dto = isMedia + ? Database.FirstOrDefault(sql) + : Database.FirstOrDefault(sql); + + if (dto == null) { - var sql = GetBaseWhere(false, false, false, false, id); - var dto = Database.FirstOrDefault(sql); - return dto == null ? null : BuildEntity(dto); + return null; } - public IEntitySlim? Get(int id, Guid objectTypeId) - { - var isContent = objectTypeId == Cms.Core.Constants.ObjectTypes.Document || objectTypeId == Cms.Core.Constants.ObjectTypes.DocumentBlueprint; - var isMedia = objectTypeId == Cms.Core.Constants.ObjectTypes.Media; - var isMember = objectTypeId == Cms.Core.Constants.ObjectTypes.Member; + EntitySlim entity = BuildEntity(dto); - var sql = GetFullSqlForEntityType(isContent, isMedia, isMember, objectTypeId, id); - return GetEntity(sql, isContent, isMedia, isMember); - } + return entity; + } - public IEnumerable GetAll(Guid objectType, params int[] ids) - { - return ids.Length > 0 - ? PerformGetAll(objectType, sql => sql.WhereIn(x => x.NodeId, ids.Distinct())) - : PerformGetAll(objectType); - } + public IEntitySlim? Get(Guid key, Guid objectTypeId) + { + var isContent = objectTypeId == Constants.ObjectTypes.Document || + objectTypeId == Constants.ObjectTypes.DocumentBlueprint; + var isMedia = objectTypeId == Constants.ObjectTypes.Media; + var isMember = objectTypeId == Constants.ObjectTypes.Member; - public IEnumerable GetAll(Guid objectType, params Guid[] keys) - { - return keys.Length > 0 - ? PerformGetAll(objectType, sql => sql.WhereIn(x => x.UniqueId, keys.Distinct())) - : PerformGetAll(objectType); - } + Sql sql = GetFullSqlForEntityType(isContent, isMedia, isMember, objectTypeId, key); + return GetEntity(sql, isContent, isMedia, isMember); + } - private IEnumerable GetEntities(Sql sql, bool isContent, bool isMedia, bool isMember) - { - // isContent is going to return a 1:M result now with the variants so we need to do different things - if (isContent) - { - var cdtos = Database.Fetch(sql); + public IEntitySlim? Get(int id) + { + Sql sql = GetBaseWhere(false, false, false, false, id); + BaseDto? dto = Database.FirstOrDefault(sql); + return dto == null ? null : BuildEntity(dto); + } - return cdtos.Count == 0 - ? Enumerable.Empty() - : BuildVariants(cdtos.Select(BuildDocumentEntity)).ToList(); - } + public IEntitySlim? Get(int id, Guid objectTypeId) + { + var isContent = objectTypeId == Constants.ObjectTypes.Document || + objectTypeId == Constants.ObjectTypes.DocumentBlueprint; + var isMedia = objectTypeId == Constants.ObjectTypes.Media; + var isMember = objectTypeId == Constants.ObjectTypes.Member; - var dtos = isMedia - ? (IEnumerable)Database.Fetch(sql) - : Database.Fetch(sql); + Sql sql = GetFullSqlForEntityType(isContent, isMedia, isMember, objectTypeId, id); + return GetEntity(sql, isContent, isMedia, isMember); + } - var entities = dtos.Select(BuildEntity).ToArray(); + public IEnumerable GetAll(Guid objectType, params int[] ids) => + ids.Length > 0 + ? PerformGetAll(objectType, sql => sql.WhereIn(x => x.NodeId, ids.Distinct())) + : PerformGetAll(objectType); - return entities; - } + public IEnumerable GetAll(Guid objectType, params Guid[] keys) => + keys.Length > 0 + ? PerformGetAll(objectType, sql => sql.WhereIn(x => x.UniqueId, keys.Distinct())) + : PerformGetAll(objectType); - private IEnumerable PerformGetAll(Guid objectType, Action>? filter = null) + private IEnumerable GetEntities(Sql sql, bool isContent, bool isMedia, bool isMember) + { + // isContent is going to return a 1:M result now with the variants so we need to do different things + if (isContent) { - var isContent = objectType == Cms.Core.Constants.ObjectTypes.Document || objectType == Cms.Core.Constants.ObjectTypes.DocumentBlueprint; - var isMedia = objectType == Cms.Core.Constants.ObjectTypes.Media; - var isMember = objectType == Cms.Core.Constants.ObjectTypes.Member; + List? cdtos = Database.Fetch(sql); - var sql = GetFullSqlForEntityType(isContent, isMedia, isMember, objectType, filter); - return GetEntities(sql, isContent, isMedia, isMember); + return cdtos.Count == 0 + ? Enumerable.Empty() + : BuildVariants(cdtos.Select(BuildDocumentEntity)).ToList(); } - public IEnumerable GetAllPaths(Guid objectType, params int[]? ids) - { - return ids?.Any() ?? false - ? PerformGetAllPaths(objectType, sql => sql.WhereIn(x => x.NodeId, ids.Distinct())) - : PerformGetAllPaths(objectType); - } + IEnumerable? dtos = isMedia + ? (IEnumerable)Database.Fetch(sql) + : Database.Fetch(sql); - public IEnumerable GetAllPaths(Guid objectType, params Guid[] keys) - { - return keys.Any() - ? PerformGetAllPaths(objectType, sql => sql.WhereIn(x => x.UniqueId, keys.Distinct())) - : PerformGetAllPaths(objectType); - } + EntitySlim[] entities = dtos.Select(BuildEntity).ToArray(); - private IEnumerable PerformGetAllPaths(Guid objectType, Action>? filter = null) - { - // NodeId is named Id on TreeEntityPath = use an alias - var sql = Sql().Select(x => Alias(x.NodeId, nameof(TreeEntityPath.Id)), x => x.Path).From().Where(x => x.NodeObjectType == objectType); - filter?.Invoke(sql); - return Database.Fetch(sql); - } + return entities; + } - public IEnumerable GetByQuery(IQuery query) - { - var sqlClause = GetBase(false, false, false, null); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - sql = AddGroupBy(false, false, false, sql, true); - var dtos = Database.Fetch(sql); - return dtos.Select(BuildEntity).ToList(); - } + private IEnumerable PerformGetAll(Guid objectType, Action>? filter = null) + { + var isContent = objectType == Constants.ObjectTypes.Document || + objectType == Constants.ObjectTypes.DocumentBlueprint; + var isMedia = objectType == Constants.ObjectTypes.Media; + var isMember = objectType == Constants.ObjectTypes.Member; - public IEnumerable GetByQuery(IQuery query, Guid objectType) - { - var isContent = objectType == Cms.Core.Constants.ObjectTypes.Document || objectType == Cms.Core.Constants.ObjectTypes.DocumentBlueprint; - var isMedia = objectType == Cms.Core.Constants.ObjectTypes.Media; - var isMember = objectType == Cms.Core.Constants.ObjectTypes.Member; + Sql sql = GetFullSqlForEntityType(isContent, isMedia, isMember, objectType, filter); + return GetEntities(sql, isContent, isMedia, isMember); + } - var sql = GetBaseWhere(isContent, isMedia, isMember, false, null, new[] { objectType }); + public IEnumerable GetAllPaths(Guid objectType, params int[]? ids) => + ids?.Any() ?? false + ? PerformGetAllPaths(objectType, sql => sql.WhereIn(x => x.NodeId, ids.Distinct())) + : PerformGetAllPaths(objectType); - var translator = new SqlTranslator(sql, query); - sql = translator.Translate(); - sql = AddGroupBy(isContent, isMedia, isMember, sql, true); + public IEnumerable GetAllPaths(Guid objectType, params Guid[] keys) => + keys.Any() + ? PerformGetAllPaths(objectType, sql => sql.WhereIn(x => x.UniqueId, keys.Distinct())) + : PerformGetAllPaths(objectType); - return GetEntities(sql, isContent, isMedia, isMember); - } + private IEnumerable PerformGetAllPaths(Guid objectType, Action>? filter = null) + { + // NodeId is named Id on TreeEntityPath = use an alias + Sql sql = Sql().Select(x => Alias(x.NodeId, nameof(TreeEntityPath.Id)), x => x.Path) + .From().Where(x => x.NodeObjectType == objectType); + filter?.Invoke(sql); + return Database.Fetch(sql); + } - public UmbracoObjectTypes GetObjectType(int id) - { - var sql = Sql().Select(x => x.NodeObjectType).From().Where(x => x.NodeId == id); - return ObjectTypes.GetUmbracoObjectType(Database.ExecuteScalar(sql)); - } + public IEnumerable GetByQuery(IQuery query) + { + Sql sqlClause = GetBase(false, false, false, null); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + sql = AddGroupBy(false, false, false, sql, true); + List? dtos = Database.Fetch(sql); + return dtos.Select(BuildEntity).ToList(); + } - public UmbracoObjectTypes GetObjectType(Guid key) - { - var sql = Sql().Select(x => x.NodeObjectType).From().Where(x => x.UniqueId == key); - return ObjectTypes.GetUmbracoObjectType(Database.ExecuteScalar(sql)); - } + public IEnumerable GetByQuery(IQuery query, Guid objectType) + { + var isContent = objectType == Constants.ObjectTypes.Document || + objectType == Constants.ObjectTypes.DocumentBlueprint; + var isMedia = objectType == Constants.ObjectTypes.Media; + var isMember = objectType == Constants.ObjectTypes.Member; - public int ReserveId(Guid key) - { - NodeDto node; + Sql sql = GetBaseWhere(isContent, isMedia, isMember, false, null, new[] {objectType}); - Sql sql = SqlContext.Sql() - .Select() - .From() - .Where(x => x.UniqueId == key && x.NodeObjectType == Cms.Core.Constants.ObjectTypes.IdReservation); + var translator = new SqlTranslator(sql, query); + sql = translator.Translate(); + sql = AddGroupBy(isContent, isMedia, isMember, sql, true); - node = Database.SingleOrDefault(sql); - if (node != null) - throw new InvalidOperationException("An identifier has already been reserved for this Udi."); + return GetEntities(sql, isContent, isMedia, isMember); + } - node = new NodeDto - { - UniqueId = key, - Text = "RESERVED.ID", - NodeObjectType = Cms.Core.Constants.ObjectTypes.IdReservation, - - CreateDate = DateTime.Now, - UserId = null, - ParentId = -1, - Level = 1, - Path = "-1", - SortOrder = 0, - Trashed = false - }; - Database.Insert(node); - - return node.NodeId; - } + public UmbracoObjectTypes GetObjectType(int id) + { + Sql sql = Sql().Select(x => x.NodeObjectType).From() + .Where(x => x.NodeId == id); + return ObjectTypes.GetUmbracoObjectType(Database.ExecuteScalar(sql)); + } - public bool Exists(Guid key) - { - var sql = Sql().SelectCount().From().Where(x => x.UniqueId == key); - return Database.ExecuteScalar(sql) > 0; - } + public UmbracoObjectTypes GetObjectType(Guid key) + { + Sql sql = Sql().Select(x => x.NodeObjectType).From() + .Where(x => x.UniqueId == key); + return ObjectTypes.GetUmbracoObjectType(Database.ExecuteScalar(sql)); + } + + public int ReserveId(Guid key) + { + NodeDto node; + + Sql sql = SqlContext.Sql() + .Select() + .From() + .Where(x => x.UniqueId == key && x.NodeObjectType == Constants.ObjectTypes.IdReservation); - public bool Exists(int id) + node = Database.SingleOrDefault(sql); + if (node != null) { - var sql = Sql().SelectCount().From().Where(x => x.NodeId == id); - return Database.ExecuteScalar(sql) > 0; + throw new InvalidOperationException("An identifier has already been reserved for this Udi."); } - private DocumentEntitySlim BuildVariants(DocumentEntitySlim entity) - => BuildVariants(new[] { entity }).First(); - - private IEnumerable BuildVariants(IEnumerable entities) + node = new NodeDto { - List? v = null; - var entitiesList = entities.ToList(); - foreach (var e in entitiesList) - { - if (e.Variations.VariesByCulture()) - (v ?? (v = new List())).Add(e); - } + UniqueId = key, + Text = "RESERVED.ID", + NodeObjectType = Constants.ObjectTypes.IdReservation, + CreateDate = DateTime.Now, + UserId = null, + ParentId = -1, + Level = 1, + Path = "-1", + SortOrder = 0, + Trashed = false + }; + Database.Insert(node); - if (v == null) return entitiesList; + return node.NodeId; + } - // fetch all variant info dtos - var dtos = Database.FetchByGroups(v.Select(x => x.Id), Constants.Sql.MaxParameterCount, GetVariantInfos); + public bool Exists(Guid key) + { + Sql sql = Sql().SelectCount().From().Where(x => x.UniqueId == key); + return Database.ExecuteScalar(sql) > 0; + } - // group by node id (each group contains all languages) - var xdtos = dtos.GroupBy(x => x.NodeId).ToDictionary(x => x.Key, x => x); + public bool Exists(int id) + { + Sql sql = Sql().SelectCount().From().Where(x => x.NodeId == id); + return Database.ExecuteScalar(sql) > 0; + } - foreach (var e in v) - { - // since we're only iterating on entities that vary, we must have something - var edtos = xdtos[e.Id]; + private DocumentEntitySlim BuildVariants(DocumentEntitySlim entity) + => BuildVariants(new[] {entity}).First(); - e.CultureNames = edtos.Where(x => x.CultureAvailable).ToDictionary(x => x.IsoCode, x => x.Name); - e.PublishedCultures = edtos.Where(x => x.CulturePublished).Select(x => x.IsoCode); - e.EditedCultures = edtos.Where(x => x.CultureAvailable && x.CultureEdited).Select(x => x.IsoCode); + private IEnumerable BuildVariants(IEnumerable entities) + { + List? v = null; + var entitiesList = entities.ToList(); + foreach (DocumentEntitySlim e in entitiesList) + { + if (e.Variations.VariesByCulture()) + { + (v ?? (v = new List())).Add(e); } + } + if (v == null) + { return entitiesList; } - #endregion + // fetch all variant info dtos + IEnumerable dtos = Database.FetchByGroups(v.Select(x => x.Id), + Constants.Sql.MaxParameterCount, GetVariantInfos); - #region Sql + // group by node id (each group contains all languages) + var xdtos = dtos.GroupBy(x => x.NodeId).ToDictionary(x => x.Key, x => x); - protected Sql GetVariantInfos(IEnumerable ids) + foreach (DocumentEntitySlim e in v) { - return Sql() - .Select(x => x.NodeId) - .AndSelect(x => x.IsoCode) - .AndSelect("doc", x => Alias(x.Published, "DocumentPublished"), x => Alias(x.Edited, "DocumentEdited")) - .AndSelect("dcv", - x => Alias(x.Available, "CultureAvailable"), x => Alias(x.Published, "CulturePublished"), x => Alias(x.Edited, "CultureEdited"), - x => Alias(x.Name, "Name")) + // since we're only iterating on entities that vary, we must have something + IGrouping edtos = xdtos[e.Id]; - // from node x language - .From() - .CrossJoin() + e.CultureNames = edtos.Where(x => x.CultureAvailable).ToDictionary(x => x.IsoCode, x => x.Name); + e.PublishedCultures = edtos.Where(x => x.CulturePublished).Select(x => x.IsoCode); + e.EditedCultures = edtos.Where(x => x.CultureAvailable && x.CultureEdited).Select(x => x.IsoCode); + } - // join to document - always exists - indicates global document published/edited status - .InnerJoin("doc") - .On((node, doc) => node.NodeId == doc.NodeId, aliasRight: "doc") + return entitiesList; + } - // left-join do document variation - matches cultures that are *available* + indicates when *edited* - .LeftJoin("dcv") - .On((node, dcv, lang) => node.NodeId == dcv.NodeId && lang.Id == dcv.LanguageId, aliasRight: "dcv") + #endregion + + #region Sql + + protected Sql GetVariantInfos(IEnumerable ids) => + Sql() + .Select(x => x.NodeId) + .AndSelect(x => x.IsoCode) + .AndSelect("doc", x => Alias(x.Published, "DocumentPublished"), + x => Alias(x.Edited, "DocumentEdited")) + .AndSelect("dcv", + x => Alias(x.Available, "CultureAvailable"), x => Alias(x.Published, "CulturePublished"), + x => Alias(x.Edited, "CultureEdited"), + x => Alias(x.Name, "Name")) + + // from node x language + .From() + .CrossJoin() + + // join to document - always exists - indicates global document published/edited status + .InnerJoin("doc") + .On((node, doc) => node.NodeId == doc.NodeId, aliasRight: "doc") + + // left-join do document variation - matches cultures that are *available* + indicates when *edited* + .LeftJoin("dcv") + .On( + (node, dcv, lang) => node.NodeId == dcv.NodeId && lang.Id == dcv.LanguageId, aliasRight: "dcv") + + // for selected nodes + .WhereIn(x => x.NodeId, ids) + .OrderBy(x => x.Id); + + // gets the full sql for a given object type and a given unique id + protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, bool isMember, Guid objectType, + Guid uniqueId) + { + Sql sql = GetBaseWhere(isContent, isMedia, isMember, false, objectType, uniqueId); + return AddGroupBy(isContent, isMedia, isMember, sql, true); + } - // for selected nodes - .WhereIn(x => x.NodeId, ids) - .OrderBy(x => x.Id); - } + // gets the full sql for a given object type and a given node id + protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, bool isMember, Guid objectType, + int nodeId) + { + Sql sql = GetBaseWhere(isContent, isMedia, isMember, false, objectType, nodeId); + return AddGroupBy(isContent, isMedia, isMember, sql, true); + } - // gets the full sql for a given object type and a given unique id - protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, bool isMember, Guid objectType, Guid uniqueId) - { - var sql = GetBaseWhere(isContent, isMedia, isMember, false, objectType, uniqueId); - return AddGroupBy(isContent, isMedia, isMember, sql, true); - } + // gets the full sql for a given object type, with a given filter + protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, bool isMember, Guid objectType, + Action>? filter) + { + Sql sql = GetBaseWhere(isContent, isMedia, isMember, false, filter, new[] {objectType}); + return AddGroupBy(isContent, isMedia, isMember, sql, true); + } - // gets the full sql for a given object type and a given node id - protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, bool isMember, Guid objectType, int nodeId) - { - var sql = GetBaseWhere(isContent, isMedia, isMember, false, objectType, nodeId); - return AddGroupBy(isContent, isMedia, isMember, sql, true); - } + // gets the base SELECT + FROM [+ filter] sql + // always from the 'current' content version + protected Sql GetBase(bool isContent, bool isMedia, bool isMember, Action>? filter, + bool isCount = false) + { + Sql sql = Sql(); - // gets the full sql for a given object type, with a given filter - protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, bool isMember, Guid objectType, Action>? filter) + if (isCount) { - var sql = GetBaseWhere(isContent, isMedia, isMember, false, filter, new[] { objectType }); - return AddGroupBy(isContent, isMedia, isMember, sql, true); + sql.SelectCount(); } - - // gets the base SELECT + FROM [+ filter] sql - // always from the 'current' content version - protected Sql GetBase(bool isContent, bool isMedia, bool isMember, Action>? filter, bool isCount = false) + else { - var sql = Sql(); - - if (isCount) - { - sql.SelectCount(); - } - else - { - sql - .Select(x => x.NodeId, x => x.Trashed, x => x.ParentId, x => x.UserId, x => x.Level, x => x.Path) - .AndSelect(x => x.SortOrder, x => x.UniqueId, x => x.Text, x => x.NodeObjectType, x => x.CreateDate) - .Append(", COUNT(child.id) AS children"); - - if (isContent || isMedia || isMember) - sql - .AndSelect(x => Alias(x.Id, "versionId"), x=>x.VersionDate) - .AndSelect(x => x.Alias, x => x.Icon, x => x.Thumbnail, x => x.IsContainer, x => x.Variations); - - if (isContent) - { - sql - .AndSelect(x => x.Published, x => x.Edited); - } - - if (isMedia) - { - sql - .AndSelect(x => Alias(x.Path, "MediaPath")); - } - } - sql - .From(); + .Select(x => x.NodeId, x => x.Trashed, x => x.ParentId, x => x.UserId, x => x.Level, + x => x.Path) + .AndSelect(x => x.SortOrder, x => x.UniqueId, x => x.Text, x => x.NodeObjectType, + x => x.CreateDate) + .Append(", COUNT(child.id) AS children"); if (isContent || isMedia || isMember) { sql - .LeftJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) - .LeftJoin().On((left, right) => left.NodeId == right.NodeId) - .LeftJoin().On((left, right) => left.ContentTypeId == right.NodeId); + .AndSelect(x => Alias(x.Id, "versionId"), x => x.VersionDate) + .AndSelect(x => x.Alias, x => x.Icon, x => x.Thumbnail, x => x.IsContainer, + x => x.Variations); } if (isContent) { sql - .LeftJoin().On((left, right) => left.NodeId == right.NodeId); + .AndSelect(x => x.Published, x => x.Edited); } if (isMedia) { sql - .LeftJoin().On((left, right) => left.Id == right.Id); - } - - //Any LeftJoin statements need to come last - if (isCount == false) - { - sql - .LeftJoin("child").On((left, right) => left.NodeId == right.ParentId, aliasRight: "child"); + .AndSelect(x => Alias(x.Path, "MediaPath")); } - - - filter?.Invoke(sql); - - return sql; } - // gets the base SELECT + FROM [+ filter] + WHERE sql - // for a given object type, with a given filter - protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Action>? filter, Guid[] objectTypes) - { - var sql = GetBase(isContent, isMedia, isMember, filter, isCount); - if (objectTypes.Length > 0) - { - sql.WhereIn(x => x.NodeObjectType, objectTypes); - } - return sql; - } + sql + .From(); - // gets the base SELECT + FROM + WHERE sql - // for a given node id - protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, int id) + if (isContent || isMedia || isMember) { - var sql = GetBase(isContent, isMedia, isMember, null, isCount) - .Where(x => x.NodeId == id); - return AddGroupBy(isContent, isMedia, isMember, sql, true); + sql + .LeftJoin() + .On((left, right) => left.NodeId == right.NodeId && right.Current) + .LeftJoin().On((left, right) => left.NodeId == right.NodeId) + .LeftJoin() + .On((left, right) => left.ContentTypeId == right.NodeId); } - // gets the base SELECT + FROM + WHERE sql - // for a given unique id - protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Guid uniqueId) + if (isContent) { - var sql = GetBase(isContent, isMedia, isMember, null, isCount) - .Where(x => x.UniqueId == uniqueId); - return AddGroupBy(isContent, isMedia, isMember, sql, true); + sql + .LeftJoin().On((left, right) => left.NodeId == right.NodeId); } - // gets the base SELECT + FROM + WHERE sql - // for a given object type and node id - protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Guid objectType, int nodeId) + if (isMedia) { - return GetBase(isContent, isMedia, isMember, null, isCount) - .Where(x => x.NodeId == nodeId && x.NodeObjectType == objectType); + sql + .LeftJoin() + .On((left, right) => left.Id == right.Id); } - // gets the base SELECT + FROM + WHERE sql - // for a given object type and unique id - protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Guid objectType, Guid uniqueId) + //Any LeftJoin statements need to come last + if (isCount == false) { - return GetBase(isContent, isMedia, isMember, null, isCount) - .Where(x => x.UniqueId == uniqueId && x.NodeObjectType == objectType); + sql + .LeftJoin("child") + .On((left, right) => left.NodeId == right.ParentId, aliasRight: "child"); } - // gets the GROUP BY / ORDER BY sql - // required in order to count children - protected Sql AddGroupBy(bool isContent, bool isMedia, bool isMember, Sql sql, bool defaultSort) - { - sql - .GroupBy(x => x.NodeId, x => x.Trashed, x => x.ParentId, x => x.UserId, x => x.Level, x => x.Path) - .AndBy(x => x.SortOrder, x => x.UniqueId, x => x.Text, x => x.NodeObjectType, x => x.CreateDate); - if (isContent) - { - sql - .AndBy(x => x.Published, x => x.Edited); - } + filter?.Invoke(sql); - if (isMedia) - { - sql - .AndBy(x => Alias(x.Path, "MediaPath")); - } + return sql; + } + // gets the base SELECT + FROM [+ filter] + WHERE sql + // for a given object type, with a given filter + protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, + Action>? filter, Guid[] objectTypes) + { + Sql sql = GetBase(isContent, isMedia, isMember, filter, isCount); + if (objectTypes.Length > 0) + { + sql.WhereIn(x => x.NodeObjectType, objectTypes); + } - if (isContent || isMedia || isMember) - sql - .AndBy(x => x.Id, x => x.VersionDate) - .AndBy(x => x.Alias, x => x.Icon, x => x.Thumbnail, x => x.IsContainer, x => x.Variations); + return sql; + } - if (defaultSort) - sql.OrderBy(x => x.SortOrder); + // gets the base SELECT + FROM + WHERE sql + // for a given node id + protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, int id) + { + Sql sql = GetBase(isContent, isMedia, isMember, null, isCount) + .Where(x => x.NodeId == id); + return AddGroupBy(isContent, isMedia, isMember, sql, true); + } - return sql; + // gets the base SELECT + FROM + WHERE sql + // for a given unique id + protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Guid uniqueId) + { + Sql sql = GetBase(isContent, isMedia, isMember, null, isCount) + .Where(x => x.UniqueId == uniqueId); + return AddGroupBy(isContent, isMedia, isMember, sql, true); + } + + // gets the base SELECT + FROM + WHERE sql + // for a given object type and node id + protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Guid objectType, + int nodeId) => + GetBase(isContent, isMedia, isMember, null, isCount) + .Where(x => x.NodeId == nodeId && x.NodeObjectType == objectType); + + // gets the base SELECT + FROM + WHERE sql + // for a given object type and unique id + protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Guid objectType, + Guid uniqueId) => + GetBase(isContent, isMedia, isMember, null, isCount) + .Where(x => x.UniqueId == uniqueId && x.NodeObjectType == objectType); + + // gets the GROUP BY / ORDER BY sql + // required in order to count children + protected Sql AddGroupBy(bool isContent, bool isMedia, bool isMember, Sql sql, + bool defaultSort) + { + sql + .GroupBy(x => x.NodeId, x => x.Trashed, x => x.ParentId, x => x.UserId, x => x.Level, x => x.Path) + .AndBy(x => x.SortOrder, x => x.UniqueId, x => x.Text, x => x.NodeObjectType, x => x.CreateDate); + + if (isContent) + { + sql + .AndBy(x => x.Published, x => x.Edited); } - private void ApplyOrdering(ref Sql sql, Ordering ordering) + if (isMedia) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); - if (ordering == null) throw new ArgumentNullException(nameof(ordering)); + sql + .AndBy(x => Alias(x.Path, "MediaPath")); + } - // TODO: although the default ordering string works for name, it wont work for others without a table or an alias of some sort - // As more things are attempted to be sorted we'll prob have to add more expressions here - string orderBy; - switch (ordering.OrderBy?.ToUpperInvariant()) - { - case "PATH": - orderBy = SqlSyntax.GetQuotedColumn(NodeDto.TableName, "path"); - break; - default: - orderBy = ordering.OrderBy ?? string.Empty; - break; - } + if (isContent || isMedia || isMember) + { + sql + .AndBy(x => x.Id, x => x.VersionDate) + .AndBy(x => x.Alias, x => x.Icon, x => x.Thumbnail, x => x.IsContainer, + x => x.Variations); + } - if (ordering.Direction == Direction.Ascending) - sql.OrderBy(orderBy); - else - sql.OrderByDescending(orderBy); + if (defaultSort) + { + sql.OrderBy(x => x.SortOrder); } - #endregion + return sql; + } - #region Classes + private void ApplyOrdering(ref Sql sql, Ordering ordering) + { + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } - /// - /// The DTO used to fetch results for a generic content item which could be either a document, media or a member - /// - private class GenericContentEntityDto : DocumentEntityDto + if (ordering == null) { - public string? MediaPath { get; set; } + throw new ArgumentNullException(nameof(ordering)); } - /// - /// The DTO used to fetch results for a document item with its variation info - /// - private class DocumentEntityDto : BaseDto + // TODO: although the default ordering string works for name, it wont work for others without a table or an alias of some sort + // As more things are attempted to be sorted we'll prob have to add more expressions here + string orderBy; + switch (ordering.OrderBy?.ToUpperInvariant()) { - public ContentVariation Variations { get; set; } + case "PATH": + orderBy = SqlSyntax.GetQuotedColumn(NodeDto.TableName, "path"); + break; - public bool Published { get; set; } - public bool Edited { get; set; } + default: + orderBy = ordering.OrderBy ?? string.Empty; + break; } - /// - /// The DTO used to fetch results for a media item with its media path info - /// - private class MediaEntityDto : BaseDto + if (ordering.Direction == Direction.Ascending) { - public string? MediaPath { get; set; } + sql.OrderBy(orderBy); } - - /// - /// The DTO used to fetch results for a member item - /// - private class MemberEntityDto : BaseDto + else { + sql.OrderByDescending(orderBy); } + } - public class VariantInfoDto - { - public int NodeId { get; set; } - public string IsoCode { get; set; } = null!; - public string Name { get; set; } = null!; - public bool DocumentPublished { get; set; } - public bool DocumentEdited { get; set; } + #endregion - public bool CultureAvailable { get; set; } - public bool CulturePublished { get; set; } - public bool CultureEdited { get; set; } - } + #region Classes - // ReSharper disable once ClassNeverInstantiated.Local - /// - /// the DTO corresponding to fields selected by GetBase - /// - private class BaseDto - { - // ReSharper disable UnusedAutoPropertyAccessor.Local - // ReSharper disable UnusedMember.Local - public int NodeId { get; set; } - public bool Trashed { get; set; } - public int ParentId { get; set; } - public int? UserId { get; set; } - public int Level { get; set; } - public string Path { get; set; } = null!; - public int SortOrder { get; set; } - public Guid UniqueId { get; set; } - public string? Text { get; set; } - public Guid NodeObjectType { get; set; } - public DateTime CreateDate { get; set; } - public DateTime VersionDate { get; set; } - public int Children { get; set; } - public int VersionId { get; set; } - public string Alias { get; set; } = null!; - public string? Icon { get; set; } - public string? Thumbnail { get; set; } - public bool IsContainer { get; set; } - - // ReSharper restore UnusedAutoPropertyAccessor.Local - // ReSharper restore UnusedMember.Local - } - #endregion + /// + /// The DTO used to fetch results for a generic content item which could be either a document, media or a member + /// + private class GenericContentEntityDto : DocumentEntityDto + { + public string? MediaPath { get; set; } + } - #region Factory + /// + /// The DTO used to fetch results for a document item with its variation info + /// + private class DocumentEntityDto : BaseDto + { + public ContentVariation Variations { get; set; } - private EntitySlim BuildEntity(BaseDto dto) - { - if (dto.NodeObjectType == Cms.Core.Constants.ObjectTypes.Document) - return BuildDocumentEntity(dto); - if (dto.NodeObjectType == Cms.Core.Constants.ObjectTypes.Media) - return BuildMediaEntity(dto); - if (dto.NodeObjectType == Cms.Core.Constants.ObjectTypes.Member) - return BuildMemberEntity(dto); + public bool Published { get; set; } + public bool Edited { get; set; } + } - // EntitySlim does not track changes - var entity = new EntitySlim(); - BuildEntity(entity, dto); - return entity; - } + /// + /// The DTO used to fetch results for a media item with its media path info + /// + private class MediaEntityDto : BaseDto + { + public string? MediaPath { get; set; } + } + + /// + /// The DTO used to fetch results for a member item + /// + private class MemberEntityDto : BaseDto + { + } + + public class VariantInfoDto + { + public int NodeId { get; set; } + public string IsoCode { get; set; } = null!; + public string Name { get; set; } = null!; + public bool DocumentPublished { get; set; } + public bool DocumentEdited { get; set; } + + public bool CultureAvailable { get; set; } + public bool CulturePublished { get; set; } + public bool CultureEdited { get; set; } + } + + // ReSharper disable once ClassNeverInstantiated.Local + /// + /// the DTO corresponding to fields selected by GetBase + /// + private class BaseDto + { + // ReSharper disable UnusedAutoPropertyAccessor.Local + // ReSharper disable UnusedMember.Local + public int NodeId { get; set; } + public bool Trashed { get; set; } + public int ParentId { get; set; } + public int? UserId { get; set; } + public int Level { get; set; } + public string Path { get; } = null!; + public int SortOrder { get; set; } + public Guid UniqueId { get; set; } + public string? Text { get; set; } + public Guid NodeObjectType { get; set; } + public DateTime CreateDate { get; set; } + public DateTime VersionDate { get; set; } + public int Children { get; set; } + public int VersionId { get; set; } + public string Alias { get; } = null!; + public string? Icon { get; set; } + public string? Thumbnail { get; set; } + public bool IsContainer { get; set; } + + // ReSharper restore UnusedAutoPropertyAccessor.Local + // ReSharper restore UnusedMember.Local + } + + #endregion + + #region Factory - private static void BuildEntity(EntitySlim entity, BaseDto dto) - { - entity.Trashed = dto.Trashed; - entity.CreateDate = dto.CreateDate; - entity.UpdateDate = dto.VersionDate; - entity.CreatorId = dto.UserId ?? Cms.Core.Constants.Security.UnknownUserId; - entity.Id = dto.NodeId; - entity.Key = dto.UniqueId; - entity.Level = dto.Level; - entity.Name = dto.Text; - entity.NodeObjectType = dto.NodeObjectType; - entity.ParentId = dto.ParentId; - entity.Path = dto.Path; - entity.SortOrder = dto.SortOrder; - entity.HasChildren = dto.Children > 0; - entity.IsContainer = dto.IsContainer; + private EntitySlim BuildEntity(BaseDto dto) + { + if (dto.NodeObjectType == Constants.ObjectTypes.Document) + { + return BuildDocumentEntity(dto); } - private static void BuildContentEntity(ContentEntitySlim entity, BaseDto dto) + if (dto.NodeObjectType == Constants.ObjectTypes.Media) { - BuildEntity(entity, dto); - entity.ContentTypeAlias = dto.Alias; - entity.ContentTypeIcon = dto.Icon; - entity.ContentTypeThumbnail = dto.Thumbnail; + return BuildMediaEntity(dto); } - private MediaEntitySlim BuildMediaEntity(BaseDto dto) + if (dto.NodeObjectType == Constants.ObjectTypes.Member) { - // EntitySlim does not track changes - var entity = new MediaEntitySlim(); - BuildContentEntity(entity, dto); + return BuildMemberEntity(dto); + } - // fill in the media info - if (dto is MediaEntityDto mediaEntityDto) - { - entity.MediaPath = mediaEntityDto.MediaPath; - } - else if (dto is GenericContentEntityDto genericContentEntityDto) - { - entity.MediaPath = genericContentEntityDto.MediaPath; - } + // EntitySlim does not track changes + var entity = new EntitySlim(); + BuildEntity(entity, dto); + return entity; + } - return entity; - } + private static void BuildEntity(EntitySlim entity, BaseDto dto) + { + entity.Trashed = dto.Trashed; + entity.CreateDate = dto.CreateDate; + entity.UpdateDate = dto.VersionDate; + entity.CreatorId = dto.UserId ?? Constants.Security.UnknownUserId; + entity.Id = dto.NodeId; + entity.Key = dto.UniqueId; + entity.Level = dto.Level; + entity.Name = dto.Text; + entity.NodeObjectType = dto.NodeObjectType; + entity.ParentId = dto.ParentId; + entity.Path = dto.Path; + entity.SortOrder = dto.SortOrder; + entity.HasChildren = dto.Children > 0; + entity.IsContainer = dto.IsContainer; + } - private DocumentEntitySlim BuildDocumentEntity(BaseDto dto) - { - // EntitySlim does not track changes - var entity = new DocumentEntitySlim(); - BuildContentEntity(entity, dto); + private static void BuildContentEntity(ContentEntitySlim entity, BaseDto dto) + { + BuildEntity(entity, dto); + entity.ContentTypeAlias = dto.Alias; + entity.ContentTypeIcon = dto.Icon; + entity.ContentTypeThumbnail = dto.Thumbnail; + } - if (dto is DocumentEntityDto contentDto) - { - // fill in the invariant info - entity.Edited = contentDto.Edited; - entity.Published = contentDto.Published; - entity.Variations = contentDto.Variations; - } + private MediaEntitySlim BuildMediaEntity(BaseDto dto) + { + // EntitySlim does not track changes + var entity = new MediaEntitySlim(); + BuildContentEntity(entity, dto); - return entity; + // fill in the media info + if (dto is MediaEntityDto mediaEntityDto) + { + entity.MediaPath = mediaEntityDto.MediaPath; } - - private MemberEntitySlim BuildMemberEntity(BaseDto dto) + else if (dto is GenericContentEntityDto genericContentEntityDto) { - // EntitySlim does not track changes - var entity = new MemberEntitySlim(); - BuildEntity(entity, dto); + entity.MediaPath = genericContentEntityDto.MediaPath; + } + + return entity; + } - entity.ContentTypeAlias = dto.Alias; - entity.ContentTypeIcon = dto.Icon; - entity.ContentTypeThumbnail = dto.Thumbnail; + private DocumentEntitySlim BuildDocumentEntity(BaseDto dto) + { + // EntitySlim does not track changes + var entity = new DocumentEntitySlim(); + BuildContentEntity(entity, dto); - return entity; + if (dto is DocumentEntityDto contentDto) + { + // fill in the invariant info + entity.Edited = contentDto.Edited; + entity.Published = contentDto.Published; + entity.Variations = contentDto.Variations; } - #endregion + return entity; } + + private MemberEntitySlim BuildMemberEntity(BaseDto dto) + { + // EntitySlim does not track changes + var entity = new MemberEntitySlim(); + BuildEntity(entity, dto); + + entity.ContentTypeAlias = dto.Alias; + entity.ContentTypeIcon = dto.Icon; + entity.ContentTypeThumbnail = dto.Thumbnail; + + return entity; + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepositoryBase.cs index 4ac8adbd91b4..ba9436c0fe15 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepositoryBase.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -13,243 +10,239 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Provides a base class to all based repositories. +/// +/// The type of the entity's unique identifier. +/// The type of the entity managed by this repository. +public abstract class EntityRepositoryBase : RepositoryBase, IReadWriteQueryRepository + where TEntity : class, IEntity { + private static RepositoryCachePolicyOptions? s_defaultOptions; + private IRepositoryCachePolicy? _cachePolicy; + private IQuery? _hasIdQuery; + + /// + /// Initializes a new instance of the class. + /// + protected EntityRepositoryBase(IScopeAccessor scopeAccessor, AppCaches appCaches, + ILogger> logger) + : base(scopeAccessor, appCaches) => + Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + /// - /// Provides a base class to all based repositories. + /// Gets the logger /// - /// The type of the entity's unique identifier. - /// The type of the entity managed by this repository. - public abstract class EntityRepositoryBase : RepositoryBase, IReadWriteQueryRepository - where TEntity : class, IEntity + protected ILogger> Logger { get; } + + /// + /// Gets the isolated cache for the + /// + protected IAppPolicyCache GlobalIsolatedCache => AppCaches.IsolatedCaches.GetOrCreate(); + + /// + /// Gets the isolated cache. + /// + /// Depends on the ambient scope cache mode. + protected IAppPolicyCache IsolatedCache { - private static RepositoryCachePolicyOptions? s_defaultOptions; - private IRepositoryCachePolicy? _cachePolicy; - private IQuery? _hasIdQuery; - - /// - /// Initializes a new instance of the class. - /// - protected EntityRepositoryBase(IScopeAccessor scopeAccessor, AppCaches appCaches, - ILogger> logger) - : base(scopeAccessor, appCaches) => - Logger = logger ?? throw new ArgumentNullException(nameof(logger)); - - /// - /// Gets the logger - /// - protected ILogger> Logger { get; } - - /// - /// Gets the isolated cache for the - /// - protected IAppPolicyCache GlobalIsolatedCache => AppCaches.IsolatedCaches.GetOrCreate(); - - /// - /// Gets the isolated cache. - /// - /// Depends on the ambient scope cache mode. - protected IAppPolicyCache IsolatedCache + get { - get + switch (AmbientScope.RepositoryCacheMode) { - switch (AmbientScope.RepositoryCacheMode) - { - case RepositoryCacheMode.Default: - return AppCaches.IsolatedCaches.GetOrCreate(); - case RepositoryCacheMode.Scoped: - return AmbientScope.IsolatedCaches.GetOrCreate(); - case RepositoryCacheMode.None: - return NoAppCache.Instance; - default: - throw new Exception("oops: cache mode."); - } + case RepositoryCacheMode.Default: + return AppCaches.IsolatedCaches.GetOrCreate(); + case RepositoryCacheMode.Scoped: + return AmbientScope.IsolatedCaches.GetOrCreate(); + case RepositoryCacheMode.None: + return NoAppCache.Instance; + default: + throw new Exception("oops: cache mode."); } } + } - /// - /// Gets the default - /// - protected virtual RepositoryCachePolicyOptions DefaultOptions => s_defaultOptions ?? (s_defaultOptions - = new RepositoryCachePolicyOptions(() => - { - // get count of all entities of current type (TEntity) to ensure cached result is correct - // create query once if it is needed (no need for locking here) - query is static! - IQuery query = _hasIdQuery ?? - (_hasIdQuery = AmbientScope.SqlContext.Query().Where(x => x.Id != 0)); - return PerformCount(query); - })); - - /// - /// Gets the repository cache policy - /// - protected IRepositoryCachePolicy CachePolicy + /// + /// Gets the default + /// + protected virtual RepositoryCachePolicyOptions DefaultOptions => s_defaultOptions ?? (s_defaultOptions + = new RepositoryCachePolicyOptions(() => { - get - { - if (AppCaches == AppCaches.NoCache) - { - return NoCacheRepositoryCachePolicy.Instance; - } - - // create the cache policy using IsolatedCache which is either global - // or scoped depending on the repository cache mode for the current scope - - switch (AmbientScope.RepositoryCacheMode) - { - case RepositoryCacheMode.Default: - case RepositoryCacheMode.Scoped: - // return the same cache policy in both cases - the cache policy is - // supposed to pick either the global or scope cache depending on the - // scope cache mode - return _cachePolicy ?? (_cachePolicy = CreateCachePolicy()); - case RepositoryCacheMode.None: - return NoCacheRepositoryCachePolicy.Instance; - default: - throw new Exception("oops: cache mode."); - } - } - } + // get count of all entities of current type (TEntity) to ensure cached result is correct + // create query once if it is needed (no need for locking here) - query is static! + IQuery query = _hasIdQuery ?? + (_hasIdQuery = AmbientScope.SqlContext.Query().Where(x => x.Id != 0)); + return PerformCount(query); + })); - /// - /// Adds or Updates an entity of type TEntity - /// - /// This method is backed by an cache - public virtual void Save(TEntity entity) + /// + /// Gets the repository cache policy + /// + protected IRepositoryCachePolicy CachePolicy + { + get { - if (entity.HasIdentity == false) + if (AppCaches == AppCaches.NoCache) { - CachePolicy.Create(entity, PersistNewItem); + return NoCacheRepositoryCachePolicy.Instance; } - else + + // create the cache policy using IsolatedCache which is either global + // or scoped depending on the repository cache mode for the current scope + + switch (AmbientScope.RepositoryCacheMode) { - CachePolicy.Update(entity, PersistUpdatedItem); + case RepositoryCacheMode.Default: + case RepositoryCacheMode.Scoped: + // return the same cache policy in both cases - the cache policy is + // supposed to pick either the global or scope cache depending on the + // scope cache mode + return _cachePolicy ?? (_cachePolicy = CreateCachePolicy()); + case RepositoryCacheMode.None: + return NoCacheRepositoryCachePolicy.Instance; + default: + throw new Exception("oops: cache mode."); } } + } - /// - /// Deletes the passed in entity - /// - public virtual void Delete(TEntity entity) - => CachePolicy.Delete(entity, PersistDeletedItem); - - /// - /// Gets an entity by the passed in Id utilizing the repository's cache policy - /// - public TEntity? Get(TId? id) - => CachePolicy.Get(id, PerformGet, PerformGetAll); - - /// - /// Gets all entities of type TEntity or a list according to the passed in Ids - /// - public IEnumerable GetMany(params TId[]? ids) + /// + /// Adds or Updates an entity of type TEntity + /// + /// This method is backed by an cache + public virtual void Save(TEntity entity) + { + if (entity.HasIdentity == false) { - // ensure they are de-duplicated, easy win if people don't do this as this can cause many excess queries - ids = ids?.Distinct() + CachePolicy.Create(entity, PersistNewItem); + } + else + { + CachePolicy.Update(entity, PersistUpdatedItem); + } + } - // don't query by anything that is a default of T (like a zero) - // TODO: I think we should enabled this in case accidental calls are made to get all with invalid ids - // .Where(x => Equals(x, default(TId)) == false) - .ToArray(); + /// + /// Deletes the passed in entity + /// + public virtual void Delete(TEntity entity) + => CachePolicy.Delete(entity, PersistDeletedItem); - // can't query more than 2000 ids at a time... but if someone is really querying 2000+ entities, - // the additional overhead of fetching them in groups is minimal compared to the lookup time of each group - if (ids?.Length <= Constants.Sql.MaxParameterCount) - { - return CachePolicy.GetAll(ids, PerformGetAll) ?? Enumerable.Empty(); - } + /// + /// Gets an entity by the passed in Id utilizing the repository's cache policy + /// + public TEntity? Get(TId? id) + => CachePolicy.Get(id, PerformGet, PerformGetAll); - var entities = new List(); - foreach (IEnumerable group in ids.InGroupsOf(Constants.Sql.MaxParameterCount)) - { - var groups = CachePolicy.GetAll(group.ToArray(), PerformGetAll); - if (groups is not null) - { - entities.AddRange(groups); - } - } + /// + /// Gets all entities of type TEntity or a list according to the passed in Ids + /// + public IEnumerable GetMany(params TId[]? ids) + { + // ensure they are de-duplicated, easy win if people don't do this as this can cause many excess queries + ids = ids?.Distinct() - return entities; - } + // don't query by anything that is a default of T (like a zero) + // TODO: I think we should enabled this in case accidental calls are made to get all with invalid ids + // .Where(x => Equals(x, default(TId)) == false) + .ToArray(); - /// - /// Gets a list of entities by the passed in query - /// - public IEnumerable Get(IQuery query) + // can't query more than 2000 ids at a time... but if someone is really querying 2000+ entities, + // the additional overhead of fetching them in groups is minimal compared to the lookup time of each group + if (ids?.Length <= Constants.Sql.MaxParameterCount) { + return CachePolicy.GetAll(ids, PerformGetAll) ?? Enumerable.Empty(); + } - // ensure we don't include any null refs in the returned collection! - return PerformGetByQuery(query) - .WhereNotNull(); + var entities = new List(); + foreach (IEnumerable group in ids.InGroupsOf(Constants.Sql.MaxParameterCount)) + { + TEntity[]? groups = CachePolicy.GetAll(group.ToArray(), PerformGetAll); + if (groups is not null) + { + entities.AddRange(groups); + } } - /// - /// Returns a boolean indicating whether an entity with the passed Id exists - /// - public bool Exists(TId id) - => CachePolicy.Exists(id, PerformExists, PerformGetAll); + return entities; + } - /// - /// Returns an integer with the count of entities found with the passed in query - /// - public int Count(IQuery query) - => PerformCount(query); + /// + /// Gets a list of entities by the passed in query + /// + public IEnumerable Get(IQuery query) => + // ensure we don't include any null refs in the returned collection! + PerformGetByQuery(query) + .WhereNotNull(); - /// - /// Get the entity id for the - /// - protected virtual TId GetEntityId(TEntity entity) - => (TId)(object)entity.Id; + /// + /// Returns a boolean indicating whether an entity with the passed Id exists + /// + public bool Exists(TId id) + => CachePolicy.Exists(id, PerformExists, PerformGetAll); - /// - /// Create the repository cache policy - /// - protected virtual IRepositoryCachePolicy CreateCachePolicy() - => new DefaultRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, DefaultOptions); + /// + /// Returns an integer with the count of entities found with the passed in query + /// + public int Count(IQuery query) + => PerformCount(query); - protected abstract TEntity? PerformGet(TId? id); + /// + /// Get the entity id for the + /// + protected virtual TId GetEntityId(TEntity entity) + => (TId)(object)entity.Id; - protected abstract IEnumerable PerformGetAll(params TId[]? ids); + /// + /// Create the repository cache policy + /// + protected virtual IRepositoryCachePolicy CreateCachePolicy() + => new DefaultRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, DefaultOptions); - protected abstract IEnumerable PerformGetByQuery(IQuery query); + protected abstract TEntity? PerformGet(TId? id); - protected abstract void PersistNewItem(TEntity item); + protected abstract IEnumerable PerformGetAll(params TId[]? ids); - protected abstract void PersistUpdatedItem(TEntity item); + protected abstract IEnumerable PerformGetByQuery(IQuery query); - // TODO: obsolete, use QueryType instead everywhere like GetBaseQuery(QueryType queryType); - protected abstract Sql GetBaseQuery(bool isCount); + protected abstract void PersistNewItem(TEntity item); - protected abstract string GetBaseWhereClause(); + protected abstract void PersistUpdatedItem(TEntity item); - protected abstract IEnumerable GetDeleteClauses(); + // TODO: obsolete, use QueryType instead everywhere like GetBaseQuery(QueryType queryType); + protected abstract Sql GetBaseQuery(bool isCount); - protected virtual bool PerformExists(TId id) - { - Sql sql = GetBaseQuery(true); - sql.Where(GetBaseWhereClause(), new { id }); - var count = Database.ExecuteScalar(sql); - return count == 1; - } + protected abstract string GetBaseWhereClause(); - protected virtual int PerformCount(IQuery query) - { - Sql sqlClause = GetBaseQuery(true); - var translator = new SqlTranslator(sqlClause, query); - Sql sql = translator.Translate(); + protected abstract IEnumerable GetDeleteClauses(); - return Database.ExecuteScalar(sql); - } + protected virtual bool PerformExists(TId id) + { + Sql sql = GetBaseQuery(true); + sql.Where(GetBaseWhereClause(), new {id}); + var count = Database.ExecuteScalar(sql); + return count == 1; + } - protected virtual void PersistDeletedItem(TEntity entity) - { - IEnumerable deletes = GetDeleteClauses(); - foreach (var delete in deletes) - { - Database.Execute(delete, new { id = GetEntityId(entity) }); - } + protected virtual int PerformCount(IQuery query) + { + Sql sqlClause = GetBaseQuery(true); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); - entity.DeleteDate = DateTime.Now; + return Database.ExecuteScalar(sql); + } + + protected virtual void PersistDeletedItem(TEntity entity) + { + IEnumerable deletes = GetDeleteClauses(); + foreach (var delete in deletes) + { + Database.Execute(delete, new {id = GetEntityId(entity)}); } + + entity.DeleteDate = DateTime.Now; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs index 9739c9a2955b..57f7868e889b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -15,289 +12,307 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class ExternalLoginRepository : EntityRepositoryBase, IExternalLoginRepository, + IExternalLoginWithKeyRepository { - internal class ExternalLoginRepository : EntityRepositoryBase, IExternalLoginRepository, IExternalLoginWithKeyRepository + public ExternalLoginRepository(IScopeAccessor scopeAccessor, AppCaches cache, + ILogger logger) + : base(scopeAccessor, cache, logger) { - public ExternalLoginRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { } + } + + /// + [Obsolete("Use method that takes guid as param")] + public void DeleteUserLogins(int memberId) => DeleteUserLogins(memberId.ToGuid()); - /// - [Obsolete("Use method that takes guid as param")] - public void DeleteUserLogins(int memberId) => DeleteUserLogins(memberId.ToGuid()); + /// + [Obsolete("Use method that takes guid as param")] + public void Save(int userId, IEnumerable logins) => Save(userId.ToGuid(), logins); - /// - [Obsolete("Use method that takes guid as param")] - public void Save(int userId, IEnumerable logins) => Save(userId.ToGuid(), logins); + /// + [Obsolete("Use method that takes guid as param")] + public void Save(int userId, IEnumerable tokens) => Save(userId.ToGuid(), tokens); - /// - [Obsolete("Use method that takes guid as param")] - public void Save(int userId, IEnumerable tokens) => Save(userId.ToGuid(), tokens); + /// + /// Query for user tokens + /// + /// + /// + public IEnumerable Get(IQuery? query) + { + Sql sqlClause = GetBaseTokenQuery(false); + + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); - /// - public void DeleteUserLogins(Guid userOrMemberKey) => Database.Delete("WHERE userOrMemberKey=@userOrMemberKey", new { userOrMemberKey }); + List dtos = Database.Fetch(sql); - /// - public void Save(Guid userOrMemberKey, IEnumerable logins) + foreach (ExternalLoginTokenDto dto in dtos) { - var sql = Sql() - .Select() - .From() - .Where(x => x.UserOrMemberKey == userOrMemberKey) - .ForUpdate(); + yield return ExternalLoginFactory.BuildEntity(dto); + } + } - // deduplicate the logins - logins = logins.DistinctBy(x => x.ProviderKey + x.LoginProvider).ToList(); + /// + /// Count for user tokens + /// + /// + /// + public int Count(IQuery query) + { + Sql sql = Sql().SelectCount().From(); + return Database.ExecuteScalar(sql); + } - var toUpdate = new Dictionary(); - var toDelete = new List(); - var toInsert = new List(logins); + /// + public void DeleteUserLogins(Guid userOrMemberKey) => + Database.Delete("WHERE userOrMemberKey=@userOrMemberKey", new {userOrMemberKey}); - var existingLogins = Database.Fetch(sql); + /// + public void Save(Guid userOrMemberKey, IEnumerable logins) + { + Sql sql = Sql() + .Select() + .From() + .Where(x => x.UserOrMemberKey == userOrMemberKey) + .ForUpdate(); - foreach (var existing in existingLogins) - { - var found = logins.FirstOrDefault(x => - x.LoginProvider.Equals(existing.LoginProvider, StringComparison.InvariantCultureIgnoreCase) - && x.ProviderKey.Equals(existing.ProviderKey, StringComparison.InvariantCultureIgnoreCase)); - - if (found != null) - { - toUpdate.Add(existing.Id, found); - // if it's an update then it's not an insert - toInsert.RemoveAll(x => x.ProviderKey == found.ProviderKey && x.LoginProvider == found.LoginProvider); - } - else - { - toDelete.Add(existing.Id); - } - } + // deduplicate the logins + logins = logins.DistinctBy(x => x.ProviderKey + x.LoginProvider).ToList(); - // do the deletes, updates and inserts - if (toDelete.Count > 0) + var toUpdate = new Dictionary(); + var toDelete = new List(); + var toInsert = new List(logins); + + List? existingLogins = Database.Fetch(sql); + + foreach (ExternalLoginDto? existing in existingLogins) + { + IExternalLogin? found = logins.FirstOrDefault(x => + x.LoginProvider.Equals(existing.LoginProvider, StringComparison.InvariantCultureIgnoreCase) + && x.ProviderKey.Equals(existing.ProviderKey, StringComparison.InvariantCultureIgnoreCase)); + + if (found != null) { - Database.DeleteMany().Where(x => toDelete.Contains(x.Id)).Execute(); + toUpdate.Add(existing.Id, found); + // if it's an update then it's not an insert + toInsert.RemoveAll(x => x.ProviderKey == found.ProviderKey && x.LoginProvider == found.LoginProvider); } - - foreach (var u in toUpdate) + else { - Database.Update(ExternalLoginFactory.BuildDto(userOrMemberKey, u.Value, u.Key)); + toDelete.Add(existing.Id); } + } - Database.InsertBulk(toInsert.Select(i => ExternalLoginFactory.BuildDto(userOrMemberKey, i))); + // do the deletes, updates and inserts + if (toDelete.Count > 0) + { + Database.DeleteMany().Where(x => toDelete.Contains(x.Id)).Execute(); } - protected override IIdentityUserLogin? PerformGet(int id) + foreach (KeyValuePair u in toUpdate) { - var sql = GetBaseQuery(false); - sql.Where(GetBaseWhereClause(), new { id = id }); + Database.Update(ExternalLoginFactory.BuildDto(userOrMemberKey, u.Value, u.Key)); + } - var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); - if (dto == null) - return null; + Database.InsertBulk(toInsert.Select(i => ExternalLoginFactory.BuildDto(userOrMemberKey, i))); + } - var entity = ExternalLoginFactory.BuildEntity(dto); + /// + public void Save(Guid userOrMemberKey, IEnumerable tokens) + { + // get the existing logins (provider + id) + var existingUserLogins = Database + .Fetch(GetBaseQuery(false) + .Where(x => x.UserOrMemberKey == userOrMemberKey)) + .ToDictionary(x => x.LoginProvider, x => x.Id); - // reset dirty initial properties (U4-1946) - entity.ResetDirtyProperties(false); + // deduplicate the tokens + tokens = tokens.DistinctBy(x => x.LoginProvider + x.Name).ToList(); - return entity; - } + var providers = tokens.Select(x => x.LoginProvider).Distinct().ToList(); - protected override IEnumerable PerformGetAll(params int[]? ids) - { - if (ids?.Any() ?? false) - { - return PerformGetAllOnIds(ids); - } + Sql sql = GetBaseTokenQuery(true) + .WhereIn(x => x.LoginProvider, providers) + .Where(x => x.UserOrMemberKey == userOrMemberKey); - var sql = GetBaseQuery(false).OrderByDescending(x => x.CreateDate); + var toUpdate = new Dictionary(); + var toDelete = new List(); + var toInsert = new List(tokens); - return ConvertFromDtos(Database.Fetch(sql)) - .ToArray();// we don't want to re-iterate again! - } + List? existingTokens = Database.Fetch(sql); - private IEnumerable PerformGetAllOnIds(params int[] ids) + foreach (ExternalLoginTokenDto existing in existingTokens) { - if (ids.Any() == false) yield break; - foreach (var id in ids) + IExternalLoginToken? found = tokens.FirstOrDefault(x => + x.LoginProvider.InvariantEquals(existing.ExternalLoginDto.LoginProvider) + && x.Name.InvariantEquals(existing.Name)); + + if (found != null) { - IIdentityUserLogin? identityUserLogin = Get(id); - if (identityUserLogin is not null) - { - yield return identityUserLogin; - } + toUpdate.Add(existing.Id, (found, existing.ExternalLoginId)); + // if it's an update then it's not an insert + toInsert.RemoveAll(x => + x.LoginProvider.InvariantEquals(found.LoginProvider) && x.Name.InvariantEquals(found.Name)); } - } - - private IEnumerable ConvertFromDtos(IEnumerable dtos) - { - foreach (var entity in dtos.Select(ExternalLoginFactory.BuildEntity)) + else { - // reset dirty initial properties (U4-1946) - ((BeingDirtyBase)entity).ResetDirtyProperties(false); - - yield return entity; + toDelete.Add(existing.Id); } } - protected override IEnumerable PerformGetByQuery(IQuery query) + // do the deletes, updates and inserts + if (toDelete.Count > 0) { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); + Database.DeleteMany().Where(x => toDelete.Contains(x.Id)).Execute(); + } - var dtos = Database.Fetch(sql); + foreach (KeyValuePair u in toUpdate) + { + Database.Update(ExternalLoginFactory.BuildDto(u.Value.externalLoginId, u.Value.externalLoginToken, u.Key)); + } - foreach (var dto in dtos) + var insertDtos = new List(); + foreach (IExternalLoginToken t in toInsert) + { + if (!existingUserLogins.TryGetValue(t.LoginProvider, out var externalLoginId)) { - yield return ExternalLoginFactory.BuildEntity(dto); + throw new InvalidOperationException( + $"A token was attempted to be saved for login provider {t.LoginProvider} which is not assigned to this user"); } - } - protected override Sql GetBaseQuery(bool isCount) - { - var sql = Sql(); - if (isCount) - sql.SelectCount(); - else - sql.SelectAll(); - sql.From(); - return sql; + insertDtos.Add(ExternalLoginFactory.BuildDto(externalLoginId, t)); } - protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.ExternalLogin}.id = @id"; + Database.InsertBulk(insertDtos); + } + + protected override IIdentityUserLogin? PerformGet(int id) + { + Sql sql = GetBaseQuery(false); + sql.Where(GetBaseWhereClause(), new {id}); - protected override IEnumerable GetDeleteClauses() + ExternalLoginDto? dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); + if (dto == null) { - var list = new List - { - "DELETE FROM umbracoExternalLogin WHERE id = @id" - }; - return list; + return null; } - protected override void PersistNewItem(IIdentityUserLogin entity) - { - entity.AddingEntity(); + IIdentityUserLogin entity = ExternalLoginFactory.BuildEntity(dto); - var dto = ExternalLoginFactory.BuildDto(entity); + // reset dirty initial properties (U4-1946) + entity.ResetDirtyProperties(false); - var id = Convert.ToInt32(Database.Insert(dto)); - entity.Id = id; + return entity; + } - entity.ResetDirtyProperties(); + protected override IEnumerable PerformGetAll(params int[]? ids) + { + if (ids?.Any() ?? false) + { + return PerformGetAllOnIds(ids); } - protected override void PersistUpdatedItem(IIdentityUserLogin entity) - { - entity.UpdatingEntity(); + Sql sql = GetBaseQuery(false).OrderByDescending(x => x.CreateDate); - var dto = ExternalLoginFactory.BuildDto(entity); + return ConvertFromDtos(Database.Fetch(sql)) + .ToArray(); // we don't want to re-iterate again! + } - Database.Update(dto); + private IEnumerable PerformGetAllOnIds(params int[] ids) + { + if (ids.Any() == false) + { + yield break; + } - entity.ResetDirtyProperties(); + foreach (var id in ids) + { + IIdentityUserLogin? identityUserLogin = Get(id); + if (identityUserLogin is not null) + { + yield return identityUserLogin; + } } + } - /// - /// Query for user tokens - /// - /// - /// - public IEnumerable Get(IQuery? query) + private IEnumerable ConvertFromDtos(IEnumerable dtos) + { + foreach (IIdentityUserLogin entity in dtos.Select(ExternalLoginFactory.BuildEntity)) { - Sql sqlClause = GetBaseTokenQuery(false); + // reset dirty initial properties (U4-1946) + ((BeingDirtyBase)entity).ResetDirtyProperties(false); - var translator = new SqlTranslator(sqlClause, query); - Sql sql = translator.Translate(); + yield return entity; + } + } - List dtos = Database.Fetch(sql); + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); - foreach (ExternalLoginTokenDto dto in dtos) - { - yield return ExternalLoginFactory.BuildEntity(dto); - } - } + List? dtos = Database.Fetch(sql); - /// - /// Count for user tokens - /// - /// - /// - public int Count(IQuery query) + foreach (ExternalLoginDto? dto in dtos) { - Sql sql = Sql().SelectCount().From(); - return Database.ExecuteScalar(sql); + yield return ExternalLoginFactory.BuildEntity(dto); } + } - /// - public void Save(Guid userOrMemberKey, IEnumerable tokens) + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); + if (isCount) { - // get the existing logins (provider + id) - var existingUserLogins = Database - .Fetch(GetBaseQuery(false).Where(x => x.UserOrMemberKey == userOrMemberKey)) - .ToDictionary(x => x.LoginProvider, x => x.Id); + sql.SelectCount(); + } + else + { + sql.SelectAll(); + } - // deduplicate the tokens - tokens = tokens.DistinctBy(x => x.LoginProvider + x.Name).ToList(); + sql.From(); + return sql; + } - var providers = tokens.Select(x => x.LoginProvider).Distinct().ToList(); + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.ExternalLogin}.id = @id"; - Sql sql = GetBaseTokenQuery(true) - .WhereIn(x => x.LoginProvider, providers) - .Where(x => x.UserOrMemberKey == userOrMemberKey); + protected override IEnumerable GetDeleteClauses() + { + var list = new List {"DELETE FROM umbracoExternalLogin WHERE id = @id"}; + return list; + } - var toUpdate = new Dictionary(); - var toDelete = new List(); - var toInsert = new List(tokens); + protected override void PersistNewItem(IIdentityUserLogin entity) + { + entity.AddingEntity(); - var existingTokens = Database.Fetch(sql); + ExternalLoginDto dto = ExternalLoginFactory.BuildDto(entity); - foreach (ExternalLoginTokenDto existing in existingTokens) - { - IExternalLoginToken? found = tokens.FirstOrDefault(x => - x.LoginProvider.InvariantEquals(existing.ExternalLoginDto.LoginProvider) - && x.Name.InvariantEquals(existing.Name)); - - if (found != null) - { - toUpdate.Add(existing.Id, (found, existing.ExternalLoginId)); - // if it's an update then it's not an insert - toInsert.RemoveAll(x => x.LoginProvider.InvariantEquals(found.LoginProvider) && x.Name.InvariantEquals(found.Name)); - } - else - { - toDelete.Add(existing.Id); - } - } + var id = Convert.ToInt32(Database.Insert(dto)); + entity.Id = id; - // do the deletes, updates and inserts - if (toDelete.Count > 0) - { - Database.DeleteMany().Where(x => toDelete.Contains(x.Id)).Execute(); - } + entity.ResetDirtyProperties(); + } - foreach (KeyValuePair u in toUpdate) - { - Database.Update(ExternalLoginFactory.BuildDto(u.Value.externalLoginId, u.Value.externalLoginToken, u.Key)); - } + protected override void PersistUpdatedItem(IIdentityUserLogin entity) + { + entity.UpdatingEntity(); - var insertDtos = new List(); - foreach(IExternalLoginToken t in toInsert) - { - if (!existingUserLogins.TryGetValue(t.LoginProvider, out int externalLoginId)) - { - throw new InvalidOperationException($"A token was attempted to be saved for login provider {t.LoginProvider} which is not assigned to this user"); - } - insertDtos.Add(ExternalLoginFactory.BuildDto(externalLoginId, t)); - } - Database.InsertBulk(insertDtos); - } + ExternalLoginDto dto = ExternalLoginFactory.BuildDto(entity); + + Database.Update(dto); - private Sql GetBaseTokenQuery(bool forUpdate) - => forUpdate ? Sql() + entity.ResetDirtyProperties(); + } + + private Sql GetBaseTokenQuery(bool forUpdate) + => forUpdate + ? Sql() .Select(r => r.Select(x => x.ExternalLoginDto)) .From() .AppendForUpdateHint() // ensure these table values are locked for updates, the ForUpdate ext method does not work here @@ -309,5 +324,4 @@ private Sql GetBaseTokenQuery(bool forUpdate) .From() .InnerJoin() .On(x => x.ExternalLoginId, x => x.Id); - } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/FileRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/FileRepository.cs index 54d07966805f..1d78f034d38b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/FileRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/FileRepository.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; -using System.IO; using System.Text; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; @@ -7,235 +5,235 @@ using Umbraco.Cms.Core.Persistence; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal abstract class FileRepository : IReadRepository, IWriteRepository + where TEntity : IFile { - internal abstract class FileRepository : IReadRepository, IWriteRepository - where TEntity : IFile + protected FileRepository(IFileSystem? fileSystem) => FileSystem = fileSystem; + + protected IFileSystem? FileSystem { get; } + + public virtual void AddFolder(string folderPath) => PersistNewItem(new Folder(folderPath)); + + public virtual void DeleteFolder(string folderPath) => PersistDeletedItem(new Folder(folderPath)); + + internal virtual void PersistNewFolder(Folder entity) => FileSystem?.CreateFolder(entity.Path); + + internal virtual void PersistDeletedFolder(Folder entity) => FileSystem?.DeleteDirectory(entity.Path); + + /// + /// Gets a stream that is used to write to the file + /// + /// + /// + protected virtual Stream GetContentStream(string content) => new MemoryStream(Encoding.UTF8.GetBytes(content)); + + /// + /// Returns all files in the file system + /// + /// + /// + /// + /// Returns a list of all files with their paths. For example: + /// \hello.txt + /// \folder1\test.txt + /// \folder1\blah.csv + /// \folder1\folder2\blahhhhh.svg + /// + protected IEnumerable FindAllFiles(string path, string filter) { - protected FileRepository(IFileSystem? fileSystem) => FileSystem = fileSystem; - - protected IFileSystem? FileSystem { get; } + var list = new List(); + IEnumerable? collection = FileSystem?.GetFiles(path, filter); + if (collection is not null) + { + list.AddRange(collection); + } - public virtual void AddFolder(string folderPath) => PersistNewItem(new Folder(folderPath)); + IEnumerable? directories = FileSystem?.GetDirectories(path); + if (directories is not null) + { + foreach (var directory in directories) + { + list.AddRange(FindAllFiles(directory, filter)); + } + } - public virtual void DeleteFolder(string folderPath) => PersistDeletedItem(new Folder(folderPath)); + return list; + } - #region Implementation of IRepository + protected string? GetFileContent(string? filename) + { + if (filename is null || FileSystem?.FileExists(filename) == false) + { + return null; + } - public virtual void Save(TEntity entity) + try { - if (FileSystem?.FileExists(entity.OriginalPath) == false) + using Stream? stream = FileSystem?.OpenFile(filename!); + if (stream is not null) { - PersistNewItem(entity); - } - else - { - PersistUpdatedItem(entity); + using var reader = new StreamReader(stream, Encoding.UTF8, true); + return reader.ReadToEnd(); } } + catch + { + return null; // deal with race conds + } - public virtual void Delete(TEntity entity) => PersistDeletedItem(entity); - - public abstract TEntity? Get(TId? id); + return null; + } - public abstract IEnumerable GetMany(params TId[]? ids); + public Stream GetFileContentStream(string filepath) + { + if (FileSystem?.FileExists(filepath) == false) + { + return Stream.Null; + } - public virtual bool Exists(TId id) => FileSystem?.FileExists(id!.ToString()!) ?? false; + try + { + return FileSystem?.OpenFile(filepath) ?? Stream.Null; + } + catch + { + return Stream.Null; // deal with race conds + } + } - #endregion + public void SetFileContent(string filepath, Stream content) => FileSystem?.AddFile(filepath, content, true); - #region Implementation of IUnitOfWorkRepository + public long GetFileSize(string filename) + { + if (FileSystem?.FileExists(filename) == false) + { + return -1; + } - public void PersistNewItem(IEntity entity) + try { - //special case for folder - if (entity is Folder folder) - { - PersistNewFolder(folder); - } - else - { - PersistNewItem((TEntity)entity); - } + return FileSystem!.GetSize(filename); } + catch + { + return -1; // deal with race conds + } + } - public void PersistUpdatedItem(IEntity entity) => PersistUpdatedItem((TEntity)entity); + #region Implementation of IRepository - public void PersistDeletedItem(IEntity entity) + public virtual void Save(TEntity entity) + { + if (FileSystem?.FileExists(entity.OriginalPath) == false) { - //special case for folder - if (entity is Folder folder) - { - PersistDeletedFolder(folder); - } - else - { - PersistDeletedItem((TEntity)entity); - } + PersistNewItem(entity); + } + else + { + PersistUpdatedItem(entity); } + } + + public virtual void Delete(TEntity entity) => PersistDeletedItem(entity); + + public abstract TEntity? Get(TId? id); - #endregion + public abstract IEnumerable GetMany(params TId[]? ids); - internal virtual void PersistNewFolder(Folder entity) => FileSystem?.CreateFolder(entity.Path); + public virtual bool Exists(TId id) => FileSystem?.FileExists(id!.ToString()!) ?? false; - internal virtual void PersistDeletedFolder(Folder entity) => FileSystem?.DeleteDirectory(entity.Path); + #endregion - #region Abstract IUnitOfWorkRepository Methods + #region Implementation of IUnitOfWorkRepository - protected virtual void PersistNewItem(TEntity entity) + public void PersistNewItem(IEntity entity) + { + //special case for folder + if (entity is Folder folder) { - if (entity.Content is null || FileSystem is null) - { - return; - } - using (Stream stream = GetContentStream(entity.Content)) - { - FileSystem.AddFile(entity.Path, stream, true); - entity.CreateDate = FileSystem.GetCreated(entity.Path).UtcDateTime; - entity.UpdateDate = FileSystem.GetLastModified(entity.Path).UtcDateTime; - //the id can be the hash - entity.Id = entity.Path.GetHashCode(); - entity.Key = entity.Path.EncodeAsGuid(); - entity.VirtualPath = FileSystem?.GetUrl(entity.Path); - } + PersistNewFolder(folder); } - - protected virtual void PersistUpdatedItem(TEntity entity) + else { - if (entity.Content is null || FileSystem is null) - { - return; - } - using (Stream stream = GetContentStream(entity.Content)) - { - FileSystem.AddFile(entity.Path, stream, true); - entity.CreateDate = FileSystem.GetCreated(entity.Path).UtcDateTime; - entity.UpdateDate = FileSystem.GetLastModified(entity.Path).UtcDateTime; - //the id can be the hash - entity.Id = entity.Path.GetHashCode(); - entity.Key = entity.Path.EncodeAsGuid(); - entity.VirtualPath = FileSystem.GetUrl(entity.Path); - } - - //now that the file has been written, we need to check if the path had been changed - if (entity.Path.InvariantEquals(entity.OriginalPath) == false) - { - //delete the original file - FileSystem?.DeleteFile(entity.OriginalPath); - //reset the original path on the file - entity.ResetOriginalPath(); - } + PersistNewItem((TEntity)entity); } + } + + public void PersistUpdatedItem(IEntity entity) => PersistUpdatedItem((TEntity)entity); - protected virtual void PersistDeletedItem(TEntity entity) + public void PersistDeletedItem(IEntity entity) + { + //special case for folder + if (entity is Folder folder) { - if (FileSystem?.FileExists(entity.Path) ?? false) - { - FileSystem.DeleteFile(entity.Path); - } + PersistDeletedFolder(folder); + } + else + { + PersistDeletedItem((TEntity)entity); } + } - #endregion - - /// - /// Gets a stream that is used to write to the file - /// - /// - /// - protected virtual Stream GetContentStream(string content) => new MemoryStream(Encoding.UTF8.GetBytes(content)); - - /// - /// Returns all files in the file system - /// - /// - /// - /// - /// Returns a list of all files with their paths. For example: - /// - /// \hello.txt - /// \folder1\test.txt - /// \folder1\blah.csv - /// \folder1\folder2\blahhhhh.svg - /// - protected IEnumerable FindAllFiles(string path, string filter) - { - var list = new List(); - var collection = FileSystem?.GetFiles(path, filter); - if (collection is not null) - { - list.AddRange(collection); - } + #endregion - IEnumerable? directories = FileSystem?.GetDirectories(path); - if (directories is not null) - { - foreach (var directory in directories) - { - list.AddRange(FindAllFiles(directory, filter)); - } - } + #region Abstract IUnitOfWorkRepository Methods - return list; + protected virtual void PersistNewItem(TEntity entity) + { + if (entity.Content is null || FileSystem is null) + { + return; } - protected string? GetFileContent(string? filename) + using (Stream stream = GetContentStream(entity.Content)) { - if (filename is null || FileSystem?.FileExists(filename) == false) - { - return null; - } - - try - { - using Stream? stream = FileSystem?.OpenFile(filename!); - if (stream is not null) - { - using var reader = new StreamReader(stream, Encoding.UTF8, true); - return reader.ReadToEnd(); - } - } - catch - { - return null; // deal with race conds - } - - return null; + FileSystem.AddFile(entity.Path, stream, true); + entity.CreateDate = FileSystem.GetCreated(entity.Path).UtcDateTime; + entity.UpdateDate = FileSystem.GetLastModified(entity.Path).UtcDateTime; + //the id can be the hash + entity.Id = entity.Path.GetHashCode(); + entity.Key = entity.Path.EncodeAsGuid(); + entity.VirtualPath = FileSystem?.GetUrl(entity.Path); } + } - public Stream GetFileContentStream(string filepath) + protected virtual void PersistUpdatedItem(TEntity entity) + { + if (entity.Content is null || FileSystem is null) { - if (FileSystem?.FileExists(filepath) == false) - { - return Stream.Null; - } - - try - { - return FileSystem?.OpenFile(filepath) ?? Stream.Null; - } - catch - { - return Stream.Null; // deal with race conds - } + return; } - public void SetFileContent(string filepath, Stream content) => FileSystem?.AddFile(filepath, content, true); + using (Stream stream = GetContentStream(entity.Content)) + { + FileSystem.AddFile(entity.Path, stream, true); + entity.CreateDate = FileSystem.GetCreated(entity.Path).UtcDateTime; + entity.UpdateDate = FileSystem.GetLastModified(entity.Path).UtcDateTime; + //the id can be the hash + entity.Id = entity.Path.GetHashCode(); + entity.Key = entity.Path.EncodeAsGuid(); + entity.VirtualPath = FileSystem.GetUrl(entity.Path); + } - public long GetFileSize(string filename) + //now that the file has been written, we need to check if the path had been changed + if (entity.Path.InvariantEquals(entity.OriginalPath) == false) { - if (FileSystem?.FileExists(filename) == false) - { - return -1; - } + //delete the original file + FileSystem?.DeleteFile(entity.OriginalPath); + //reset the original path on the file + entity.ResetOriginalPath(); + } + } - try - { - return FileSystem!.GetSize(filename); - } - catch - { - return -1; // deal with race conds - } + protected virtual void PersistDeletedItem(TEntity entity) + { + if (FileSystem?.FileExists(entity.Path) ?? false) + { + FileSystem.DeleteFile(entity.Path); } } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/IdKeyMapRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/IdKeyMapRepository.cs index 007e09c4a2b5..7f9858bae6f4 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/IdKeyMapRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/IdKeyMapRepository.cs @@ -1,5 +1,4 @@ -using System; -using Umbraco.Cms.Core; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Infrastructure.Scoping; @@ -10,23 +9,25 @@ public class IdKeyMapRepository : IIdKeyMapRepository { private readonly IScopeAccessor _scopeAccessor; - public IdKeyMapRepository(IScopeAccessor scopeAccessor) - { - _scopeAccessor = scopeAccessor; - } + public IdKeyMapRepository(IScopeAccessor scopeAccessor) => _scopeAccessor = scopeAccessor; public int? GetIdForKey(Guid key, UmbracoObjectTypes umbracoObjectType) { //if it's unknown don't include the nodeObjectType in the query if (umbracoObjectType == UmbracoObjectTypes.Unknown) { - return _scopeAccessor.AmbientScope?.Database.ExecuteScalar("SELECT id FROM umbracoNode WHERE uniqueId=@id", new { id = key}); - } - else - { - return _scopeAccessor.AmbientScope?.Database.ExecuteScalar("SELECT id FROM umbracoNode WHERE uniqueId=@id AND (nodeObjectType=@type OR nodeObjectType=@reservation)", - new { id = key, type = GetNodeObjectTypeGuid(umbracoObjectType), reservation = Cms.Core.Constants.ObjectTypes.IdReservation }); + return _scopeAccessor.AmbientScope?.Database.ExecuteScalar( + "SELECT id FROM umbracoNode WHERE uniqueId=@id", new {id = key}); } + + return _scopeAccessor.AmbientScope?.Database.ExecuteScalar( + "SELECT id FROM umbracoNode WHERE uniqueId=@id AND (nodeObjectType=@type OR nodeObjectType=@reservation)", + new + { + id = key, + type = GetNodeObjectTypeGuid(umbracoObjectType), + reservation = Constants.ObjectTypes.IdReservation + }); } public Guid? GetIdForKey(int id, UmbracoObjectTypes umbracoObjectType) @@ -34,20 +35,28 @@ public IdKeyMapRepository(IScopeAccessor scopeAccessor) //if it's unknown don't include the nodeObjectType in the query if (umbracoObjectType == UmbracoObjectTypes.Unknown) { - return _scopeAccessor.AmbientScope?.Database.ExecuteScalar("SELECT uniqueId FROM umbracoNode WHERE id=@id", new { id }); - } - else - { - return _scopeAccessor.AmbientScope?.Database.ExecuteScalar("SELECT uniqueId FROM umbracoNode WHERE id=@id AND (nodeObjectType=@type OR nodeObjectType=@reservation)", - new { id, type = GetNodeObjectTypeGuid(umbracoObjectType), reservation = Cms.Core.Constants.ObjectTypes.IdReservation }); + return _scopeAccessor.AmbientScope?.Database.ExecuteScalar( + "SELECT uniqueId FROM umbracoNode WHERE id=@id", new {id}); } + + return _scopeAccessor.AmbientScope?.Database.ExecuteScalar( + "SELECT uniqueId FROM umbracoNode WHERE id=@id AND (nodeObjectType=@type OR nodeObjectType=@reservation)", + new + { + id, + type = GetNodeObjectTypeGuid(umbracoObjectType), + reservation = Constants.ObjectTypes.IdReservation + }); } private Guid GetNodeObjectTypeGuid(UmbracoObjectTypes umbracoObjectType) { - var guid = umbracoObjectType.GetGuid(); + Guid guid = umbracoObjectType.GetGuid(); if (guid == Guid.Empty) + { throw new NotSupportedException("Unsupported object type (" + umbracoObjectType + ")."); + } + return guid; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs index 6fd3da008c9b..14bc3e4037f4 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs @@ -1,8 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; @@ -12,112 +10,112 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class KeyValueRepository : EntityRepositoryBase, IKeyValueRepository { - internal class KeyValueRepository : EntityRepositoryBase, IKeyValueRepository + public KeyValueRepository(IScopeAccessor scopeAccessor, ILogger logger) + : base(scopeAccessor, AppCaches.NoCache, logger) { - public KeyValueRepository(IScopeAccessor scopeAccessor, ILogger logger) - : base(scopeAccessor, AppCaches.NoCache, logger) - { } + } - /// - public IReadOnlyDictionary? FindByKeyPrefix(string keyPrefix) - => Get(Query().Where(entity => entity.Identifier!.StartsWith(keyPrefix)))? - .ToDictionary(x => x.Identifier!, x => x.Value); + /// + public IReadOnlyDictionary? FindByKeyPrefix(string keyPrefix) + => Get(Query().Where(entity => entity.Identifier!.StartsWith(keyPrefix)))? + .ToDictionary(x => x.Identifier!, x => x.Value); - #region Overrides of IReadWriteQueryRepository + #region Overrides of IReadWriteQueryRepository - public override void Save(IKeyValue entity) + public override void Save(IKeyValue entity) + { + if (Get(entity.Identifier) == null) + { + PersistNewItem(entity); + } + else { - if (Get(entity.Identifier) == null) - PersistNewItem(entity); - else - PersistUpdatedItem(entity); + PersistUpdatedItem(entity); } + } - #endregion + #endregion - #region Overrides of EntityRepositoryBase + #region Overrides of EntityRepositoryBase - protected override Sql GetBaseQuery(bool isCount) - { - var sql = SqlContext.Sql(); + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = SqlContext.Sql(); - sql = isCount - ? sql.SelectCount() - : sql.Select(); + sql = isCount + ? sql.SelectCount() + : sql.Select(); - sql - .From(); + sql + .From(); - return sql; - } + return sql; + } - protected override string GetBaseWhereClause() => Core.Constants.DatabaseSchema.Tables.KeyValue + ".key = @id"; + protected override string GetBaseWhereClause() => Constants.DatabaseSchema.Tables.KeyValue + ".key = @id"; - protected override IEnumerable GetDeleteClauses() => Enumerable.Empty(); + protected override IEnumerable GetDeleteClauses() => Enumerable.Empty(); - protected override IKeyValue? PerformGet(string? id) - { - var sql = GetBaseQuery(false).Where(x => x.Key == id); - var dto = Database.Fetch(sql).FirstOrDefault(); - return dto == null ? null : Map(dto); - } + protected override IKeyValue? PerformGet(string? id) + { + Sql sql = GetBaseQuery(false).Where(x => x.Key == id); + KeyValueDto? dto = Database.Fetch(sql).FirstOrDefault(); + return dto == null ? null : Map(dto); + } - protected override IEnumerable PerformGetAll(params string[]? ids) - { - var sql = GetBaseQuery(false).WhereIn(x => x.Key, ids); - var dtos = Database.Fetch(sql); - return dtos?.WhereNotNull().Select(Map)!; - } + protected override IEnumerable PerformGetAll(params string[]? ids) + { + Sql sql = GetBaseQuery(false).WhereIn(x => x.Key, ids); + List? dtos = Database.Fetch(sql); + return dtos?.WhereNotNull().Select(Map)!; + } - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - return Database.Fetch(sql).Select(Map).WhereNotNull(); - } + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + return Database.Fetch(sql).Select(Map).WhereNotNull(); + } - protected override void PersistNewItem(IKeyValue entity) - { - var dto = Map(entity); - Database.Insert(dto); - } + protected override void PersistNewItem(IKeyValue entity) + { + KeyValueDto? dto = Map(entity); + Database.Insert(dto); + } - protected override void PersistUpdatedItem(IKeyValue entity) + protected override void PersistUpdatedItem(IKeyValue entity) + { + KeyValueDto? dto = Map(entity); + if (dto is not null) { - var dto = Map(entity); - if (dto is not null) - { - Database.Update(dto); - } + Database.Update(dto); } + } - private static KeyValueDto? Map(IKeyValue keyValue) + private static KeyValueDto? Map(IKeyValue keyValue) + { + if (keyValue == null) { - if (keyValue == null) return null; - - return new KeyValueDto - { - Key = keyValue.Identifier, - Value = keyValue.Value, - UpdateDate = keyValue.UpdateDate, - }; + return null; } - private static IKeyValue? Map(KeyValueDto dto) + return new KeyValueDto {Key = keyValue.Identifier, Value = keyValue.Value, UpdateDate = keyValue.UpdateDate}; + } + + private static IKeyValue? Map(KeyValueDto dto) + { + if (dto == null) { - if (dto == null) return null; - - return new KeyValue - { - Identifier = dto.Key, - Value = dto.Value, - UpdateDate = dto.UpdateDate, - }; + return null; } - #endregion + return new KeyValue {Identifier = dto.Key, Value = dto.Value, UpdateDate = dto.UpdateDate}; } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs index bf1bc4f4b416..0bf4ff3b7e4a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs @@ -1,12 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; -using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; @@ -16,316 +11,353 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents a repository for doing CRUD operations for +/// +internal class LanguageRepository : EntityRepositoryBase, ILanguageRepository { - /// - /// Represents a repository for doing CRUD operations for - /// - internal class LanguageRepository : EntityRepositoryBase, ILanguageRepository + private readonly Dictionary _codeIdMap = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _idCodeMap = new(); + + public LanguageRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) { - private readonly Dictionary _codeIdMap = new Dictionary(StringComparer.OrdinalIgnoreCase); - private readonly Dictionary _idCodeMap = new Dictionary(); + } - public LanguageRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { } + private FullDataSetRepositoryCachePolicy? TypedCachePolicy => + CachePolicy as FullDataSetRepositoryCachePolicy; - protected override IRepositoryCachePolicy CreateCachePolicy() + public ILanguage? GetByIsoCode(string isoCode) + { + // ensure cache is populated, in a non-expensive way + if (TypedCachePolicy != null) { - return new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ false); + TypedCachePolicy.GetAllCached(PerformGetAll); } - private FullDataSetRepositoryCachePolicy? TypedCachePolicy => CachePolicy as FullDataSetRepositoryCachePolicy; + var id = GetIdByIsoCode(isoCode, false); + return id.HasValue ? Get(id.Value) : null; + } - #region Overrides of RepositoryBase + // fast way of getting an id for an isoCode - avoiding cloning + // _codeIdMap is rebuilt whenever PerformGetAll runs + public int? GetIdByIsoCode(string? isoCode, bool throwOnNotFound = true) + { + if (isoCode == null) + { + return null; + } - protected override ILanguage? PerformGet(int id) + // ensure cache is populated, in a non-expensive way + if (TypedCachePolicy != null) + { + TypedCachePolicy.GetAllCached(PerformGetAll); + } + else { - return PerformGetAll(id).FirstOrDefault(); + PerformGetAll(); //we don't have a typed cache (i.e. unit tests) but need to populate the _codeIdMap } - protected override IEnumerable PerformGetAll(params int[]? ids) + lock (_codeIdMap) { - var sql = GetBaseQuery(false).Where(x => x.Id > 0); - if (ids?.Any() ?? false) + if (_codeIdMap.TryGetValue(isoCode, out var id)) { - sql.WhereIn(x => x.Id, ids); + return id; } + } - //this needs to be sorted since that is the way legacy worked - default language is the first one!! - //even though legacy didn't sort, it should be by id - sql.OrderBy(x => x.Id); + if (throwOnNotFound) + { + throw new ArgumentException($"Code {isoCode} does not correspond to an existing language.", + nameof(isoCode)); + } - // get languages - var languages = Database.Fetch(sql).Select(ConvertFromDto).OrderBy(x => x.Id).ToList(); + return null; + } - // initialize the code-id map - lock (_codeIdMap) - { - _codeIdMap.Clear(); - _idCodeMap.Clear(); - foreach (var language in languages) - { - _codeIdMap[language.IsoCode] = language.Id; - _idCodeMap[language.Id] = language.IsoCode.ToLowerInvariant(); - } - } + // fast way of getting an isoCode for an id - avoiding cloning + // _idCodeMap is rebuilt whenever PerformGetAll runs + public string? GetIsoCodeById(int? id, bool throwOnNotFound = true) + { + if (id == null) + { + return null; + } - return languages; + // ensure cache is populated, in a non-expensive way + if (TypedCachePolicy != null) + { + TypedCachePolicy.GetAllCached(PerformGetAll); + } + else + { + PerformGetAll(); } - protected override IEnumerable PerformGetByQuery(IQuery query) + lock (_codeIdMap) // yes, we want to lock _codeIdMap { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - var dtos = Database.Fetch(sql); - return dtos.Select(ConvertFromDto).ToList(); + if (_idCodeMap.TryGetValue(id.Value, out var isoCode)) + { + return isoCode; + } } - #endregion + if (throwOnNotFound) + { + throw new ArgumentException($"Id {id} does not correspond to an existing language.", nameof(id)); + } - #region Overrides of EntityRepositoryBase + return null; + } - protected override Sql GetBaseQuery(bool isCount) - { - var sql = Sql(); + public string GetDefaultIsoCode() => GetDefault().IsoCode; - sql = isCount - ? sql.SelectCount() - : sql.Select(); + public int? GetDefaultId() => GetDefault()?.Id; - sql.From(); + protected override IRepositoryCachePolicy CreateCachePolicy() => + new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, + GetEntityId, /*expires:*/ false); - return sql; - } + protected ILanguage ConvertFromDto(LanguageDto dto) + => LanguageFactory.BuildEntity(dto); - protected override string GetBaseWhereClause() + // do NOT leak that language, it's not deep-cloned! + private ILanguage GetDefault() + { + // get all cached + var languages = + (TypedCachePolicy + ?.GetAllCached( + PerformGetAll) //try to get all cached non-cloned if using the correct cache policy (not the case in unit tests) + ?? CachePolicy.GetAll(Array.Empty(), PerformGetAll)!).ToList(); + + ILanguage? language = languages.FirstOrDefault(x => x.IsDefault); + if (language != null) { - return $"{Constants.DatabaseSchema.Tables.Language}.id = @id"; + return language; } - protected override IEnumerable GetDeleteClauses() + // this is an anomaly, the service/repo should ensure it cannot happen + Logger.LogWarning( + "There is no default language. Fix this anomaly by editing the language table in database and setting one language as the default language."); + + // still, don't kill the site, and return "something" + + ILanguage? first = null; + foreach (ILanguage l in languages) { - var list = new List - { - //NOTE: There is no constraint between the Language and cmsDictionary/cmsLanguageText tables (?) - // but we still need to remove them - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.DictionaryValue + " WHERE languageId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.PropertyData + " WHERE languageId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentVersionCultureVariation + " WHERE languageId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.DocumentCultureVariation + " WHERE languageId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.TagRelationship + " WHERE tagId IN (SELECT id FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Tag + " WHERE languageId = @id)", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Tag + " WHERE languageId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Language + " WHERE id = @id" - }; - return list; + if (first == null || l.Id < first.Id) + { + first = l; + } } - #endregion + return first!; + } + + #region Overrides of RepositoryBase - #region Unit of Work Implementation + protected override ILanguage? PerformGet(int id) => PerformGetAll(id).FirstOrDefault(); - protected override void PersistNewItem(ILanguage entity) + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = GetBaseQuery(false).Where(x => x.Id > 0); + if (ids?.Any() ?? false) { - // validate iso code and culture name - if (entity.IsoCode.IsNullOrWhiteSpace() || entity.CultureName.IsNullOrWhiteSpace()) - throw new InvalidOperationException("Cannot save a language without an ISO code and a culture name."); + sql.WhereIn(x => x.Id, ids); + } - entity.AddingEntity(); + //this needs to be sorted since that is the way legacy worked - default language is the first one!! + //even though legacy didn't sort, it should be by id + sql.OrderBy(x => x.Id); - // deal with entity becoming the new default entity - if (entity.IsDefault) + // get languages + var languages = Database.Fetch(sql).Select(ConvertFromDto).OrderBy(x => x.Id).ToList(); + + // initialize the code-id map + lock (_codeIdMap) + { + _codeIdMap.Clear(); + _idCodeMap.Clear(); + foreach (ILanguage language in languages) { - // set all other entities to non-default - // safe (no race cond) because the service locks languages - var setAllDefaultToFalse = Sql() - .Update(u => u.Set(x => x.IsDefault, false)); - Database.Execute(setAllDefaultToFalse); + _codeIdMap[language.IsoCode] = language.Id; + _idCodeMap[language.Id] = language.IsoCode.ToLowerInvariant(); } + } - // fallback cycles are detected at service level + return languages; + } - // insert - var dto = LanguageFactory.BuildDto(entity); - var id = Convert.ToInt32(Database.Insert(dto)); - entity.Id = id; - entity.ResetDirtyProperties(); - } + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + List? dtos = Database.Fetch(sql); + return dtos.Select(ConvertFromDto).ToList(); + } - protected override void PersistUpdatedItem(ILanguage entity) - { - // validate iso code and culture name - if (entity.IsoCode.IsNullOrWhiteSpace() || entity.CultureName.IsNullOrWhiteSpace()) - throw new InvalidOperationException("Cannot save a language without an ISO code and a culture name."); + #endregion - entity.UpdatingEntity(); + #region Overrides of EntityRepositoryBase - if (entity.IsDefault) - { - // deal with entity becoming the new default entity + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); - // set all other entities to non-default - // safe (no race cond) because the service locks languages - var setAllDefaultToFalse = Sql() - .Update(u => u.Set(x => x.IsDefault, false)); - Database.Execute(setAllDefaultToFalse); - } - else - { - // deal with the entity not being default anymore - // which is illegal - another entity has to become default - var selectDefaultId = Sql() - .Select(x => x.Id) - .From() - .Where(x => x.IsDefault); - - var defaultId = Database.ExecuteScalar(selectDefaultId); - if (entity.Id == defaultId) - throw new InvalidOperationException($"Cannot save the default language ({entity.IsoCode}) as non-default. Make another language the default language instead."); - } + sql = isCount + ? sql.SelectCount() + : sql.Select(); - if (entity.IsPropertyDirty(nameof(ILanguage.IsoCode))) - { - //if the iso code is changing, ensure there's not another lang with the same code already assigned - var sameCode = Sql() - .SelectCount() - .From() - .Where(x => x.IsoCode == entity.IsoCode && x.Id != entity.Id); - - var countOfSameCode = Database.ExecuteScalar(sameCode); - if (countOfSameCode > 0) - throw new InvalidOperationException($"Cannot update the language to a new culture: {entity.IsoCode} since that culture is already assigned to another language entity."); - } + sql.From(); - // fallback cycles are detected at service level + return sql; + } - // update - var dto = LanguageFactory.BuildDto(entity); - Database.Update(dto); - entity.ResetDirtyProperties(); - } + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Language}.id = @id"; - protected override void PersistDeletedItem(ILanguage entity) + protected override IEnumerable GetDeleteClauses() + { + var list = new List { - // validate that the entity is not the default language. - // safe (no race cond) because the service locks languages + //NOTE: There is no constraint between the Language and cmsDictionary/cmsLanguageText tables (?) + // but we still need to remove them + "DELETE FROM " + Constants.DatabaseSchema.Tables.DictionaryValue + " WHERE languageId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyData + " WHERE languageId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentVersionCultureVariation + " WHERE languageId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.DocumentCultureVariation + " WHERE languageId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.TagRelationship + " WHERE tagId IN (SELECT id FROM " + + Constants.DatabaseSchema.Tables.Tag + " WHERE languageId = @id)", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Tag + " WHERE languageId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Language + " WHERE id = @id" + }; + return list; + } - var selectDefaultId = Sql() - .Select(x => x.Id) - .From() - .Where(x => x.IsDefault); + #endregion - var defaultId = Database.ExecuteScalar(selectDefaultId); - if (entity.Id == defaultId) - throw new InvalidOperationException($"Cannot delete the default language ({entity.IsoCode})."); + #region Unit of Work Implementation - // We need to remove any references to the language if it's being used as a fall-back from other ones - var clearFallbackLanguage = Sql() - .Update(u => u - .Set(x => x.FallbackLanguageId, null)) - .Where(x => x.FallbackLanguageId == entity.Id); + protected override void PersistNewItem(ILanguage entity) + { + // validate iso code and culture name + if (entity.IsoCode.IsNullOrWhiteSpace() || entity.CultureName.IsNullOrWhiteSpace()) + { + throw new InvalidOperationException("Cannot save a language without an ISO code and a culture name."); + } - Database.Execute(clearFallbackLanguage); + entity.AddingEntity(); - // delete - base.PersistDeletedItem(entity); + // deal with entity becoming the new default entity + if (entity.IsDefault) + { + // set all other entities to non-default + // safe (no race cond) because the service locks languages + Sql setAllDefaultToFalse = Sql() + .Update(u => u.Set(x => x.IsDefault, false)); + Database.Execute(setAllDefaultToFalse); } - #endregion + // fallback cycles are detected at service level - protected ILanguage ConvertFromDto(LanguageDto dto) - => LanguageFactory.BuildEntity(dto); + // insert + LanguageDto dto = LanguageFactory.BuildDto(entity); + var id = Convert.ToInt32(Database.Insert(dto)); + entity.Id = id; + entity.ResetDirtyProperties(); + } - public ILanguage? GetByIsoCode(string isoCode) + protected override void PersistUpdatedItem(ILanguage entity) + { + // validate iso code and culture name + if (entity.IsoCode.IsNullOrWhiteSpace() || entity.CultureName.IsNullOrWhiteSpace()) { - // ensure cache is populated, in a non-expensive way - if (TypedCachePolicy != null) - { - TypedCachePolicy.GetAllCached(PerformGetAll); - } - - var id = GetIdByIsoCode(isoCode, throwOnNotFound: false); - return id.HasValue ? Get(id.Value) : null; + throw new InvalidOperationException("Cannot save a language without an ISO code and a culture name."); } - // fast way of getting an id for an isoCode - avoiding cloning - // _codeIdMap is rebuilt whenever PerformGetAll runs - public int? GetIdByIsoCode(string? isoCode, bool throwOnNotFound = true) + entity.UpdatingEntity(); + + if (entity.IsDefault) { - if (isoCode == null) return null; + // deal with entity becoming the new default entity - // ensure cache is populated, in a non-expensive way - if (TypedCachePolicy != null) - TypedCachePolicy.GetAllCached(PerformGetAll); - else - PerformGetAll(); //we don't have a typed cache (i.e. unit tests) but need to populate the _codeIdMap + // set all other entities to non-default + // safe (no race cond) because the service locks languages + Sql setAllDefaultToFalse = Sql() + .Update(u => u.Set(x => x.IsDefault, false)); + Database.Execute(setAllDefaultToFalse); + } + else + { + // deal with the entity not being default anymore + // which is illegal - another entity has to become default + Sql selectDefaultId = Sql() + .Select(x => x.Id) + .From() + .Where(x => x.IsDefault); - lock (_codeIdMap) + var defaultId = Database.ExecuteScalar(selectDefaultId); + if (entity.Id == defaultId) { - if (_codeIdMap.TryGetValue(isoCode, out var id)) return id; + throw new InvalidOperationException( + $"Cannot save the default language ({entity.IsoCode}) as non-default. Make another language the default language instead."); } - if (throwOnNotFound) - throw new ArgumentException($"Code {isoCode} does not correspond to an existing language.", nameof(isoCode)); - - return null; } - // fast way of getting an isoCode for an id - avoiding cloning - // _idCodeMap is rebuilt whenever PerformGetAll runs - public string? GetIsoCodeById(int? id, bool throwOnNotFound = true) + if (entity.IsPropertyDirty(nameof(ILanguage.IsoCode))) { - if (id == null) return null; - - // ensure cache is populated, in a non-expensive way - if (TypedCachePolicy != null) - TypedCachePolicy.GetAllCached(PerformGetAll); - else - PerformGetAll(); + //if the iso code is changing, ensure there's not another lang with the same code already assigned + Sql sameCode = Sql() + .SelectCount() + .From() + .Where(x => x.IsoCode == entity.IsoCode && x.Id != entity.Id); - lock (_codeIdMap) // yes, we want to lock _codeIdMap + var countOfSameCode = Database.ExecuteScalar(sameCode); + if (countOfSameCode > 0) { - if (_idCodeMap.TryGetValue(id.Value, out var isoCode)) return isoCode; + throw new InvalidOperationException( + $"Cannot update the language to a new culture: {entity.IsoCode} since that culture is already assigned to another language entity."); } - if (throwOnNotFound) - throw new ArgumentException($"Id {id} does not correspond to an existing language.", nameof(id)); - - return null; } - public string GetDefaultIsoCode() - { - return GetDefault().IsoCode; - } + // fallback cycles are detected at service level - public int? GetDefaultId() - { - return GetDefault()?.Id; - } + // update + LanguageDto dto = LanguageFactory.BuildDto(entity); + Database.Update(dto); + entity.ResetDirtyProperties(); + } - // do NOT leak that language, it's not deep-cloned! - private ILanguage GetDefault() - { - // get all cached - var languages = (TypedCachePolicy?.GetAllCached(PerformGetAll) //try to get all cached non-cloned if using the correct cache policy (not the case in unit tests) - ?? CachePolicy.GetAll(Array.Empty(), PerformGetAll)!).ToList(); + protected override void PersistDeletedItem(ILanguage entity) + { + // validate that the entity is not the default language. + // safe (no race cond) because the service locks languages - var language = languages.FirstOrDefault(x => x.IsDefault); - if (language != null) return language; + Sql selectDefaultId = Sql() + .Select(x => x.Id) + .From() + .Where(x => x.IsDefault); - // this is an anomaly, the service/repo should ensure it cannot happen - Logger.LogWarning("There is no default language. Fix this anomaly by editing the language table in database and setting one language as the default language."); + var defaultId = Database.ExecuteScalar(selectDefaultId); + if (entity.Id == defaultId) + { + throw new InvalidOperationException($"Cannot delete the default language ({entity.IsoCode})."); + } - // still, don't kill the site, and return "something" + // We need to remove any references to the language if it's being used as a fall-back from other ones + Sql clearFallbackLanguage = Sql() + .Update(u => u + .Set(x => x.FallbackLanguageId, null)) + .Where(x => x.FallbackLanguageId == entity.Id); - ILanguage? first = null; - foreach (var l in languages) - { - if (first == null || l.Id < first.Id) - first = l; - } + Database.Execute(clearFallbackLanguage); - return first!; - } + // delete + base.PersistDeletedItem(entity); } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepositoryExtensions.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepositoryExtensions.cs index 72324eb874de..6a42eecd481a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepositoryExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepositoryExtensions.cs @@ -1,14 +1,17 @@ using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal static class LanguageRepositoryExtensions { - internal static class LanguageRepositoryExtensions + public static bool IsDefault(this ILanguageRepository repo, string culture) { - public static bool IsDefault(this ILanguageRepository repo, string culture) + if (culture == null || culture == "*") { - if (culture == null || culture == "*") return false; - return repo.GetDefaultIsoCode().InvariantEquals(culture); + return false; } + + return repo.GetDefaultIsoCode().InvariantEquals(culture); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LogViewerQueryRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LogViewerQueryRepository.cs index bfecc667654e..156ce5ba7891 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LogViewerQueryRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LogViewerQueryRepository.cs @@ -1,9 +1,7 @@ -using System; -using System.Collections.Generic; using System.Data; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; @@ -12,124 +10,116 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class LogViewerQueryRepository : EntityRepositoryBase, ILogViewerQueryRepository { - internal class LogViewerQueryRepository : EntityRepositoryBase, ILogViewerQueryRepository + public LogViewerQueryRepository(IScopeAccessor scopeAccessor, AppCaches cache, + ILogger logger) + : base(scopeAccessor, cache, logger) { - public LogViewerQueryRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { } + } + + public ILogViewerQuery? GetByName(string name) => + //use the underlying GetAll which will force cache all log queries + GetMany()?.FirstOrDefault(x => x.Name == name); - protected override IRepositoryCachePolicy CreateCachePolicy() + protected override IRepositoryCachePolicy CreateCachePolicy() => + new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, + GetEntityId, /*expires:*/ false); + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql? sql = GetBaseQuery(false).Where($"{Constants.DatabaseSchema.Tables.LogViewerQuery}.id > 0"); + if (ids?.Any() ?? false) { - return new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ false); + sql.Where($"{Constants.DatabaseSchema.Tables.LogViewerQuery}.id in (@ids)", new {ids}); } - protected override IEnumerable PerformGetAll(params int[]? ids) - { - var sql = GetBaseQuery(false).Where($"{Cms.Core.Constants.DatabaseSchema.Tables.LogViewerQuery}.id > 0"); - if (ids?.Any() ?? false) - { - sql.Where($"{Cms.Core.Constants.DatabaseSchema.Tables.LogViewerQuery}.id in (@ids)", new { ids = ids }); - } + return Database.Fetch(sql).Select(ConvertFromDto); + } - return Database.Fetch(sql).Select(ConvertFromDto); - } + protected override IEnumerable PerformGetByQuery(IQuery query) => + throw new NotSupportedException("This repository does not support this method"); - protected override IEnumerable PerformGetByQuery(IQuery query) - { - throw new NotSupportedException("This repository does not support this method"); - } + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); + sql = isCount ? sql.SelectCount() : sql.Select(); + sql = sql.From(); + return sql; + } - protected override Sql GetBaseQuery(bool isCount) - { - var sql = Sql(); - sql = isCount ? sql.SelectCount() : sql.Select(); - sql = sql.From(); - return sql; - } + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.LogViewerQuery}.id = @id"; - protected override string GetBaseWhereClause() - { - return $"{Cms.Core.Constants.DatabaseSchema.Tables.LogViewerQuery}.id = @id"; - } + protected override IEnumerable GetDeleteClauses() + { + var list = new List {$"DELETE FROM {Constants.DatabaseSchema.Tables.LogViewerQuery} WHERE id = @id"}; + return list; + } - protected override IEnumerable GetDeleteClauses() + protected override void PersistNewItem(ILogViewerQuery entity) + { + var exists = Database.ExecuteScalar( + $"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.LogViewerQuery} WHERE name = @name", + new {name = entity.Name}); + if (exists > 0) { - var list = new List - { - $"DELETE FROM {Cms.Core.Constants.DatabaseSchema.Tables.LogViewerQuery} WHERE id = @id" - }; - return list; + throw new DuplicateNameException($"The log query name '{entity.Name}' is already used"); } - protected override void PersistNewItem(ILogViewerQuery entity) - { - var exists = Database.ExecuteScalar($"SELECT COUNT(*) FROM {Core.Constants.DatabaseSchema.Tables.LogViewerQuery} WHERE name = @name", - new { name = entity.Name }); - if (exists > 0) throw new DuplicateNameException($"The log query name '{entity.Name}' is already used"); + entity.AddingEntity(); - entity.AddingEntity(); + var factory = new LogViewerQueryModelFactory(); + LogViewerQueryDto dto = factory.BuildDto(entity); - var factory = new LogViewerQueryModelFactory(); - var dto = factory.BuildDto(entity); + var id = Convert.ToInt32(Database.Insert(dto)); + entity.Id = id; + } - var id = Convert.ToInt32(Database.Insert(dto)); - entity.Id = id; - } + protected override void PersistUpdatedItem(ILogViewerQuery entity) + { + entity.UpdatingEntity(); - protected override void PersistUpdatedItem(ILogViewerQuery entity) + var exists = Database.ExecuteScalar( + $"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.LogViewerQuery} WHERE name = @name AND id <> @id", + new {name = entity.Name, id = entity.Id}); + //ensure there is no other log query with the same name on another entity + if (exists > 0) { - entity.UpdatingEntity(); - - var exists = Database.ExecuteScalar($"SELECT COUNT(*) FROM {Core.Constants.DatabaseSchema.Tables.LogViewerQuery} WHERE name = @name AND id <> @id", - new { name = entity.Name, id = entity.Id }); - //ensure there is no other log query with the same name on another entity - if (exists > 0) throw new DuplicateNameException($"The log query name '{entity.Name}' is already used"); - + throw new DuplicateNameException($"The log query name '{entity.Name}' is already used"); + } - var factory = new LogViewerQueryModelFactory(); - var dto = factory.BuildDto(entity); - Database.Update(dto); - } + var factory = new LogViewerQueryModelFactory(); + LogViewerQueryDto dto = factory.BuildDto(entity); - private ILogViewerQuery ConvertFromDto(LogViewerQueryDto dto) - { - var factory = new LogViewerQueryModelFactory(); - var entity = factory.BuildEntity(dto); - return entity; - } + Database.Update(dto); + } - internal class LogViewerQueryModelFactory - { + private ILogViewerQuery ConvertFromDto(LogViewerQueryDto dto) + { + var factory = new LogViewerQueryModelFactory(); + ILogViewerQuery entity = factory.BuildEntity(dto); + return entity; + } - public ILogViewerQuery BuildEntity(LogViewerQueryDto dto) - { - var logViewerQuery = new LogViewerQuery(dto.Name, dto.Query) - { - Id = dto.Id, - }; - return logViewerQuery; - } - - public LogViewerQueryDto BuildDto(ILogViewerQuery entity) - { - var dto = new LogViewerQueryDto { Name = entity.Name, Query = entity.Query, Id = entity.Id }; - return dto; - } - } + protected override ILogViewerQuery? PerformGet(int id) => + //use the underlying GetAll which will force cache all log queries + GetMany()?.FirstOrDefault(x => x.Id == id); - protected override ILogViewerQuery? PerformGet(int id) + internal class LogViewerQueryModelFactory + { + public ILogViewerQuery BuildEntity(LogViewerQueryDto dto) { - //use the underlying GetAll which will force cache all log queries - return GetMany()?.FirstOrDefault(x => x.Id == id); + var logViewerQuery = new LogViewerQuery(dto.Name, dto.Query) {Id = dto.Id}; + return logViewerQuery; } - public ILogViewerQuery? GetByName(string name) + public LogViewerQueryDto BuildDto(ILogViewerQuery entity) { - //use the underlying GetAll which will force cache all log queries - return GetMany()?.FirstOrDefault(x => x.Name == name); + var dto = new LogViewerQueryDto {Name = entity.Name, Query = entity.Query, Id = entity.Id}; + return dto; } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MacroRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MacroRepository.cs index a918590a0c87..50d3e9d3e5bf 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MacroRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MacroRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -16,250 +13,253 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class MacroRepository : EntityRepositoryBase, IMacroWithAliasRepository { - internal class MacroRepository : EntityRepositoryBase, IMacroWithAliasRepository + private readonly IRepositoryCachePolicy _macroByAliasCachePolicy; + private readonly IShortStringHelper _shortStringHelper; + + public MacroRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, + IShortStringHelper shortStringHelper) + : base(scopeAccessor, cache, logger) { - private readonly IShortStringHelper _shortStringHelper; - private readonly IRepositoryCachePolicy _macroByAliasCachePolicy; + _shortStringHelper = shortStringHelper; + _macroByAliasCachePolicy = + new DefaultRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, DefaultOptions); + } - public MacroRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IShortStringHelper shortStringHelper) - : base(scopeAccessor, cache, logger) - { - _shortStringHelper = shortStringHelper; - _macroByAliasCachePolicy = new DefaultRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, DefaultOptions); - } + public IMacro? Get(Guid id) + { + Sql sql = GetBaseQuery().Where(x => x.UniqueId == id); + return GetBySql(sql); + } - protected override IMacro? PerformGet(int id) - { - var sql = GetBaseQuery(false); - sql.Where(GetBaseWhereClause(), new { id }); - return GetBySql(sql); - } + public IEnumerable GetMany(params Guid[]? ids) => + ids?.Length > 0 ? ids.Select(Get).WhereNotNull() : GetAllNoIds(); - public IMacro? Get(Guid id) - { - var sql = GetBaseQuery().Where(x => x.UniqueId == id); - return GetBySql(sql); - } + public bool Exists(Guid id) => Get(id) != null; - private IMacro? GetBySql(Sql sql) - { - var macroDto = Database - .FetchOneToMany(x => x.MacroPropertyDtos, sql) - .FirstOrDefault(); + public IMacro? GetByAlias(string alias) => + _macroByAliasCachePolicy.Get(alias, PerformGetByAlias, PerformGetAllByAlias); - if (macroDto == null) - return null; + public IEnumerable GetAllByAlias(string[] aliases) + { + if (aliases.Any() is false) + { + return base.GetMany(); + } - var entity = MacroFactory.BuildEntity(_shortStringHelper, macroDto); + return _macroByAliasCachePolicy.GetAll(aliases, PerformGetAllByAlias); + } - // reset dirty initial properties (U4-1946) - ((BeingDirtyBase)entity).ResetDirtyProperties(false); + protected override IMacro? PerformGet(int id) + { + Sql sql = GetBaseQuery(false); + sql.Where(GetBaseWhereClause(), new {id}); + return GetBySql(sql); + } - return entity; - } + private IMacro? GetBySql(Sql sql) + { + MacroDto? macroDto = Database + .FetchOneToMany(x => x.MacroPropertyDtos, sql) + .FirstOrDefault(); - public IEnumerable GetMany(params Guid[]? ids) + if (macroDto == null) { - return ids?.Length > 0 ? ids.Select(Get).WhereNotNull() : GetAllNoIds(); + return null; } - public bool Exists(Guid id) - { - return Get(id) != null; - } + IMacro entity = MacroFactory.BuildEntity(_shortStringHelper, macroDto); - public IMacro? GetByAlias(string alias) - { - return _macroByAliasCachePolicy.Get(alias, PerformGetByAlias, PerformGetAllByAlias); - } + // reset dirty initial properties (U4-1946) + ((BeingDirtyBase)entity).ResetDirtyProperties(false); - public IEnumerable GetAllByAlias(string[] aliases) - { - if (aliases.Any() is false) - { - return base.GetMany(); - } + return entity; + } - return _macroByAliasCachePolicy.GetAll(aliases, PerformGetAllByAlias); - } + private IMacro? PerformGetByAlias(string? alias) + { + IQuery query = Query().Where(x => x.Alias.Equals(alias)); + return PerformGetByQuery(query)?.FirstOrDefault(); + } - private IMacro? PerformGetByAlias(string? alias) + private IEnumerable PerformGetAllByAlias(params string[]? aliases) + { + if (aliases is null || aliases.Any() is false) { - var query = Query().Where(x => x.Alias.Equals(alias)); - return PerformGetByQuery(query)?.FirstOrDefault(); + return base.GetMany(); } - private IEnumerable PerformGetAllByAlias(params string[]? aliases) - { - if (aliases is null || aliases.Any() is false) - { - return base.GetMany(); - } + IQuery query = Query().Where(x => aliases.Contains(x.Alias)); + return PerformGetByQuery(query); + } - var query = Query().Where(x => aliases.Contains(x.Alias)); - return PerformGetByQuery(query); - } + protected override IEnumerable PerformGetAll(params int[]? ids) => + ids?.Length > 0 ? ids.Select(Get).WhereNotNull() : GetAllNoIds(); - protected override IEnumerable PerformGetAll(params int[]? ids) - { - return ids?.Length > 0 ? ids.Select(Get).WhereNotNull() : GetAllNoIds(); - } + private IEnumerable GetAllNoIds() + { + Sql sql = GetBaseQuery(false) + //must be sorted this way for the relator to work + .OrderBy(x => x.Id); + + return Database + .FetchOneToMany(x => x.MacroPropertyDtos, sql) + .Transform(ConvertFromDtos) + .ToArray(); // do it now and once + } - private IEnumerable GetAllNoIds() + private IEnumerable ConvertFromDtos(IEnumerable dtos) + { + foreach (IMacro entity in dtos.Select(x => MacroFactory.BuildEntity(_shortStringHelper, x))) { - var sql = GetBaseQuery(false) - //must be sorted this way for the relator to work - .OrderBy(x => x.Id); - - return Database - .FetchOneToMany(x => x.MacroPropertyDtos, sql) - .Transform(ConvertFromDtos) - .ToArray(); // do it now and once + // reset dirty initial properties (U4-1946) + ((BeingDirtyBase)entity).ResetDirtyProperties(false); + + yield return entity; } + } - private IEnumerable ConvertFromDtos(IEnumerable dtos) - { + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); - foreach (var entity in dtos.Select(x => MacroFactory.BuildEntity(_shortStringHelper, x))) - { - // reset dirty initial properties (U4-1946) - ((BeingDirtyBase)entity).ResetDirtyProperties(false); + return Database + .FetchOneToMany(x => x.MacroPropertyDtos, sql) + .Select(x => Get(x.Id)!); + } - yield return entity; - } - } + protected override Sql GetBaseQuery(bool isCount) => + isCount ? Sql().SelectCount().From() : GetBaseQuery(); - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); + private Sql GetBaseQuery() => + Sql() + .SelectAll() + .From() + .LeftJoin() + .On(left => left.Id, right => right.Macro); - return Database - .FetchOneToMany(x => x.MacroPropertyDtos, sql) - .Select(x => Get(x.Id)!); - } + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Macro}.id = @id"; - protected override Sql GetBaseQuery(bool isCount) + protected override IEnumerable GetDeleteClauses() + { + var list = new List { - return isCount ? Sql().SelectCount().From() : GetBaseQuery(); - } + "DELETE FROM cmsMacroProperty WHERE macro = @id", "DELETE FROM cmsMacro WHERE id = @id" + }; + return list; + } - private Sql GetBaseQuery() - { - return Sql() - .SelectAll() - .From() - .LeftJoin() - .On(left => left.Id, right => right.Macro); - } + protected override void PersistNewItem(IMacro entity) + { + entity.AddingEntity(); - protected override string GetBaseWhereClause() - { - return $"{Constants.DatabaseSchema.Tables.Macro}.id = @id"; - } + MacroDto dto = MacroFactory.BuildDto(entity); + + var id = Convert.ToInt32(Database.Insert(dto)); + entity.Id = id; - protected override IEnumerable GetDeleteClauses() + if (dto.MacroPropertyDtos is not null) { - var list = new List - { - "DELETE FROM cmsMacroProperty WHERE macro = @id", - "DELETE FROM cmsMacro WHERE id = @id" - }; - return list; + foreach (MacroPropertyDto propDto in dto.MacroPropertyDtos) + { + //need to set the id explicitly here + propDto.Macro = id; + var propId = Convert.ToInt32(Database.Insert(propDto)); + entity.Properties[propDto.Alias].Id = propId; + } } - protected override void PersistNewItem(IMacro entity) - { - entity.AddingEntity(); + entity.ResetDirtyProperties(); + } - var dto = MacroFactory.BuildDto(entity); + protected override void PersistUpdatedItem(IMacro entity) + { + entity.UpdatingEntity(); + MacroDto dto = MacroFactory.BuildDto(entity); - var id = Convert.ToInt32(Database.Insert(dto)); - entity.Id = id; + Database.Update(dto); - if (dto.MacroPropertyDtos is not null) + //update the properties if they've changed + var macro = (Macro)entity; + if (macro.IsPropertyDirty("Properties") || macro.Properties.Values.Any(x => x.IsDirty())) + { + var ids = dto.MacroPropertyDtos?.Where(x => x.Id > 0).Select(x => x.Id).ToArray(); + if (ids?.Length > 0) { - foreach (var propDto in dto.MacroPropertyDtos) - { - //need to set the id explicitly here - propDto.Macro = id; - var propId = Convert.ToInt32(Database.Insert(propDto)); - entity.Properties[propDto.Alias].Id = propId; - } + Database.Delete("WHERE macro=@macro AND id NOT IN (@ids)", new {macro = dto.Id, ids}); + } + else + { + Database.Delete("WHERE macro=@macro", new {macro = dto.Id}); } - entity.ResetDirtyProperties(); - } - - protected override void PersistUpdatedItem(IMacro entity) - { - entity.UpdatingEntity(); - var dto = MacroFactory.BuildDto(entity); - - Database.Update(dto); - - //update the properties if they've changed - var macro = (Macro)entity; - if (macro.IsPropertyDirty("Properties") || macro.Properties.Values.Any(x => x.IsDirty())) + // detect new aliases, replace with temp aliases + // this ensures that we don't have collisions, ever + var aliases = new Dictionary(); + if (dto.MacroPropertyDtos is null) { - var ids = dto.MacroPropertyDtos?.Where(x => x.Id > 0).Select(x => x.Id).ToArray(); - if (ids?.Length > 0) - Database.Delete("WHERE macro=@macro AND id NOT IN (@ids)", new { macro = dto.Id, ids }); - else - Database.Delete("WHERE macro=@macro", new { macro = dto.Id }); + return; + } - // detect new aliases, replace with temp aliases - // this ensures that we don't have collisions, ever - var aliases = new Dictionary(); - if (dto.MacroPropertyDtos is null) + foreach (MacroPropertyDto propDto in dto.MacroPropertyDtos) + { + IMacroProperty? prop = macro.Properties.Values.FirstOrDefault(x => x.Id == propDto.Id); + if (prop == null) { - return; + throw new Exception("oops: property."); } - foreach (var propDto in dto.MacroPropertyDtos) + + if (propDto.Id == 0 || prop.IsPropertyDirty("Alias")) { - var prop = macro.Properties.Values.FirstOrDefault(x => x.Id == propDto.Id); - if (prop == null) throw new Exception("oops: property."); - if (propDto.Id == 0 || prop.IsPropertyDirty("Alias")) - { - var tempAlias = Guid.NewGuid().ToString("N").Substring(0, 8); - aliases[tempAlias] = propDto.Alias; - propDto.Alias = tempAlias; - } + var tempAlias = Guid.NewGuid().ToString("N").Substring(0, 8); + aliases[tempAlias] = propDto.Alias; + propDto.Alias = tempAlias; } + } - // insert or update existing properties, with temp aliases - foreach (var propDto in dto.MacroPropertyDtos) + // insert or update existing properties, with temp aliases + foreach (MacroPropertyDto propDto in dto.MacroPropertyDtos) + { + if (propDto.Id == 0) + { + // insert + propDto.Id = Convert.ToInt32(Database.Insert(propDto)); + macro.Properties[aliases[propDto.Alias]].Id = propDto.Id; + } + else { - if (propDto.Id == 0) + // update + IMacroProperty? property = macro.Properties.Values.FirstOrDefault(x => x.Id == propDto.Id); + if (property == null) { - // insert - propDto.Id = Convert.ToInt32(Database.Insert(propDto)); - macro.Properties[aliases[propDto.Alias]].Id = propDto.Id; + throw new Exception("oops: property."); } - else + + if (property.IsDirty()) { - // update - var property = macro.Properties.Values.FirstOrDefault(x => x.Id == propDto.Id); - if (property == null) throw new Exception("oops: property."); - if (property.IsDirty()) - Database.Update(propDto); + Database.Update(propDto); } } + } - // replace the temp aliases with the real ones - foreach (var propDto in dto.MacroPropertyDtos) + // replace the temp aliases with the real ones + foreach (MacroPropertyDto propDto in dto.MacroPropertyDtos) + { + if (aliases.ContainsKey(propDto.Alias) == false) { - if (aliases.ContainsKey(propDto.Alias) == false) continue; - - propDto.Alias = aliases[propDto.Alias]; - Database.Update(propDto); + continue; } - } - entity.ResetDirtyProperties(); + propDto.Alias = aliases[propDto.Alias]; + Database.Update(propDto); + } } + + entity.ResetDirtyProperties(); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs index c763b3481ac1..2313fafa0308 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs @@ -1,9 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; @@ -21,564 +19,561 @@ using Umbraco.Extensions; using static Umbraco.Cms.Core.Persistence.SqlExtensionsStatics; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents a repository for doing CRUD operations for +/// +public class MediaRepository : ContentRepositoryBase, IMediaRepository { - /// - /// Represents a repository for doing CRUD operations for - /// - public class MediaRepository : ContentRepositoryBase, IMediaRepository + private readonly AppCaches _cache; + private readonly MediaByGuidReadRepository _mediaByGuidReadRepository; + private readonly IMediaTypeRepository _mediaTypeRepository; + private readonly MediaUrlGeneratorCollection _mediaUrlGenerators; + private readonly IJsonSerializer _serializer; + private readonly ITagRepository _tagRepository; + + public MediaRepository( + IScopeAccessor scopeAccessor, + AppCaches cache, + ILogger logger, + ILoggerFactory loggerFactory, + IMediaTypeRepository mediaTypeRepository, + ITagRepository tagRepository, + ILanguageRepository languageRepository, + IRelationRepository relationRepository, + IRelationTypeRepository relationTypeRepository, + PropertyEditorCollection propertyEditorCollection, + MediaUrlGeneratorCollection mediaUrlGenerators, + DataValueReferenceFactoryCollection dataValueReferenceFactories, + IDataTypeService dataTypeService, + IJsonSerializer serializer, + IEventAggregator eventAggregator) + : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, + propertyEditorCollection, dataValueReferenceFactories, dataTypeService, eventAggregator) { - private readonly AppCaches _cache; - private readonly IMediaTypeRepository _mediaTypeRepository; - private readonly ITagRepository _tagRepository; - private readonly MediaUrlGeneratorCollection _mediaUrlGenerators; - private readonly IJsonSerializer _serializer; - private readonly MediaByGuidReadRepository _mediaByGuidReadRepository; - - public MediaRepository( - IScopeAccessor scopeAccessor, - AppCaches cache, - ILogger logger, - ILoggerFactory loggerFactory, - IMediaTypeRepository mediaTypeRepository, - ITagRepository tagRepository, - ILanguageRepository languageRepository, - IRelationRepository relationRepository, - IRelationTypeRepository relationTypeRepository, - PropertyEditorCollection propertyEditorCollection, - MediaUrlGeneratorCollection mediaUrlGenerators, - DataValueReferenceFactoryCollection dataValueReferenceFactories, - IDataTypeService dataTypeService, - IJsonSerializer serializer, - IEventAggregator eventAggregator) - : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection, dataValueReferenceFactories, dataTypeService, eventAggregator) - { - _cache = cache; - _mediaTypeRepository = mediaTypeRepository ?? throw new ArgumentNullException(nameof(mediaTypeRepository)); - _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); - _mediaUrlGenerators = mediaUrlGenerators; - _serializer = serializer; - _mediaByGuidReadRepository = new MediaByGuidReadRepository(this, scopeAccessor, cache, loggerFactory.CreateLogger()); - } - - protected override MediaRepository This => this; - + _cache = cache; + _mediaTypeRepository = mediaTypeRepository ?? throw new ArgumentNullException(nameof(mediaTypeRepository)); + _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); + _mediaUrlGenerators = mediaUrlGenerators; + _serializer = serializer; + _mediaByGuidReadRepository = new MediaByGuidReadRepository(this, scopeAccessor, cache, + loggerFactory.CreateLogger()); + } - #region Repository Base + protected override MediaRepository This => this; - protected override Guid NodeObjectTypeId => Cms.Core.Constants.ObjectTypes.Media; + /// + public override IEnumerable GetPage(IQuery? query, + long pageIndex, int pageSize, out long totalRecords, + IQuery? filter, Ordering? ordering) + { + Sql? filterSql = null; - protected override IMedia? PerformGet(int id) + if (filter != null) { - var sql = GetBaseQuery(QueryType.Single) - .Where(x => x.NodeId == id) - .SelectTop(1); - - var dto = Database.Fetch(sql).FirstOrDefault(); - return dto == null - ? null - : MapDtoToContent(dto); + filterSql = Sql(); + foreach (Tuple clause in filter.GetWhereClauses()) + { + filterSql = filterSql.Append($"AND ({clause.Item1})", clause.Item2); + } } - protected override IEnumerable PerformGetAll(params int[]? ids) - { - var sql = GetBaseQuery(QueryType.Many); - - if (ids?.Any() ?? false) - sql.WhereIn(x => x.NodeId, ids); + return GetPage(query, pageIndex, pageSize, out totalRecords, + x => MapDtosToContent(x), + filterSql, + ordering); + } - return MapDtosToContent(Database.Fetch(sql)); - } + private IEnumerable MapDtosToContent(List dtos, bool withCache = false) + { + var temps = new List>(); + var contentTypes = new Dictionary(); + var content = new Core.Models.Media[dtos.Count]; - protected override IEnumerable PerformGetByQuery(IQuery query) + for (var i = 0; i < dtos.Count; i++) { - var sqlClause = GetBaseQuery(QueryType.Many); + ContentDto dto = dtos[i]; - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); + if (withCache) + { + // if the cache contains the (proper version of the) item, use it + IMedia? cached = + IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); + if (cached != null && cached.VersionId == dto.ContentVersionDto.Id) + { + content[i] = (Core.Models.Media)cached; + continue; + } + } - sql - .OrderBy(x => x.Level) - .OrderBy(x => x.SortOrder); + // else, need to build it - return MapDtosToContent(Database.Fetch(sql)); - } + // get the content type - the repository is full cache *but* still deep-clones + // whatever comes out of it, so use our own local index here to avoid this + var contentTypeId = dto.ContentTypeId; + if (contentTypes.TryGetValue(contentTypeId, out IMediaType? contentType) == false) + { + contentTypes[contentTypeId] = contentType = _mediaTypeRepository.Get(contentTypeId); + } - protected override Sql GetBaseQuery(QueryType queryType) - { - return GetBaseQuery(queryType); + Core.Models.Media c = content[i] = ContentBaseFactory.BuildEntity(dto, contentType); + + // need properties + var versionId = dto.ContentVersionDto.Id; + temps.Add(new TempContent(dto.NodeId, versionId, 0, contentType, c)); } - protected virtual Sql GetBaseQuery(QueryType queryType, bool current = true, bool joinMediaVersion = false) - { - var sql = SqlContext.Sql(); + // load all properties for all documents from database in 1 query - indexed by version id + IDictionary properties = GetPropertyCollections(temps); - switch (queryType) + // assign properties + foreach (TempContent temp in temps) + { + if (temp.Content is not null) { - case QueryType.Count: - sql = sql.SelectCount(); - break; - case QueryType.Ids: - sql = sql.Select(x => x.NodeId); - break; - case QueryType.Single: - case QueryType.Many: - sql = sql.Select(r => - r.Select(x => x.NodeDto) - .Select(x => x.ContentVersionDto)) + temp.Content.Properties = properties[temp.VersionId]; - // ContentRepositoryBase expects a variantName field to order by name - // for now, just return the plain invariant node name - .AndSelect(x => Alias(x.Text, "variantName")); - break; + // reset dirty initial properties (U4-1946) + temp.Content.ResetDirtyProperties(false); } + } - sql - .From() - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .InnerJoin().On(left => left.NodeId, right => right.NodeId); + return content; + } - if (joinMediaVersion) - sql.InnerJoin().On((left, right) => left.Id == right.Id); + private IMedia MapDtoToContent(ContentDto dto) + { + IMediaType? contentType = _mediaTypeRepository.Get(dto.ContentTypeId); + Core.Models.Media media = ContentBaseFactory.BuildEntity(dto, contentType); + + // get properties - indexed by version id + var versionId = dto.ContentVersionDto.Id; + var temp = new TempContent(dto.NodeId, versionId, 0, contentType); + IDictionary properties = + GetPropertyCollections(new List> {temp}); + media.Properties = properties[versionId]; + + // reset dirty initial properties (U4-1946) + media.ResetDirtyProperties(false); + return media; + } - sql.Where(x => x.NodeObjectType == NodeObjectTypeId); - if (current) - sql.Where(x => x.Current); // always get the current version + #region Repository Base - return sql; - } + protected override Guid NodeObjectTypeId => Constants.ObjectTypes.Media; - protected override Sql GetBaseQuery(bool isCount) - { - return GetBaseQuery(isCount ? QueryType.Count : QueryType.Single); - } + protected override IMedia? PerformGet(int id) + { + Sql sql = GetBaseQuery(QueryType.Single) + .Where(x => x.NodeId == id) + .SelectTop(1); + + ContentDto? dto = Database.Fetch(sql).FirstOrDefault(); + return dto == null + ? null + : MapDtoToContent(dto); + } - // ah maybe not, that what's used for eg Exists in base repo - protected override string GetBaseWhereClause() - { - return $"{Cms.Core.Constants.DatabaseSchema.Tables.Node}.id = @id"; - } + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = GetBaseQuery(QueryType.Many); - protected override IEnumerable GetDeleteClauses() + if (ids?.Any() ?? false) { - var list = new List - { - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.User2NodeNotify + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.UserGroup2Node + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.UserGroup2NodePermission + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.UserStartNode + " WHERE startNode = @id", - "UPDATE " + Cms.Core.Constants.DatabaseSchema.Tables.UserGroup + " SET startContentId = NULL WHERE startContentId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Relation + " WHERE parentId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Relation + " WHERE childId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.TagRelationship + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Document + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion + " WHERE id IN (SELECT id FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.PropertyData + " WHERE versionId IN (SELECT id FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Content + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Node + " WHERE id = @id" - }; - return list; + sql.WhereIn(x => x.NodeId, ids); } - #endregion + return MapDtosToContent(Database.Fetch(sql)); + } - #region Versions + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(QueryType.Many); - public override IEnumerable GetAllVersions(int nodeId) - { - var sql = GetBaseQuery(QueryType.Many, current: false) - .Where(x => x.NodeId == nodeId) - .OrderByDescending(x => x.Current) - .AndByDescending(x => x.VersionDate); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); - return MapDtosToContent(Database.Fetch(sql), true); - } + sql + .OrderBy(x => x.Level) + .OrderBy(x => x.SortOrder); - public override IMedia? GetVersion(int versionId) - { - var sql = GetBaseQuery(QueryType.Single) - .Where(x => x.Id == versionId); + return MapDtosToContent(Database.Fetch(sql)); + } - var dto = Database.Fetch(sql).FirstOrDefault(); - return dto == null ? null : MapDtoToContent(dto); - } + protected override Sql GetBaseQuery(QueryType queryType) => GetBaseQuery(queryType); - public IMedia? GetMediaByPath(string mediaPath) + protected virtual Sql GetBaseQuery(QueryType queryType, bool current = true, + bool joinMediaVersion = false) + { + Sql sql = SqlContext.Sql(); + + switch (queryType) { - var umbracoFileValue = mediaPath; - const string pattern = ".*[_][0-9]+[x][0-9]+[.].*"; - var isResized = Regex.IsMatch(mediaPath, pattern); + case QueryType.Count: + sql = sql.SelectCount(); + break; + case QueryType.Ids: + sql = sql.Select(x => x.NodeId); + break; + case QueryType.Single: + case QueryType.Many: + sql = sql.Select(r => + r.Select(x => x.NodeDto) + .Select(x => x.ContentVersionDto)) - // If the image has been resized we strip the "_403x328" of the original "/media/1024/koala_403x328.jpg" URL. - if (isResized) - { - var underscoreIndex = mediaPath.LastIndexOf('_'); - var dotIndex = mediaPath.LastIndexOf('.'); - umbracoFileValue = string.Concat(mediaPath.Substring(0, underscoreIndex), mediaPath.Substring(dotIndex)); - } + // ContentRepositoryBase expects a variantName field to order by name + // for now, just return the plain invariant node name + .AndSelect(x => Alias(x.Text, "variantName")); + break; + } - var sql = GetBaseQuery(QueryType.Single, joinMediaVersion: true) - .Where(x => x.Path == umbracoFileValue) - .SelectTop(1); + sql + .From() + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId); - var dto = Database.Fetch(sql).FirstOrDefault(); - return dto == null - ? null - : MapDtoToContent(dto); + if (joinMediaVersion) + { + sql.InnerJoin() + .On((left, right) => left.Id == right.Id); } - protected override void PerformDeleteVersion(int id, int versionId) + sql.Where(x => x.NodeObjectType == NodeObjectTypeId); + + if (current) { - Database.Delete("WHERE versionId = @versionId", new { versionId }); - Database.Delete("WHERE versionId = @versionId", new { versionId }); + sql.Where(x => x.Current); // always get the current version } - #endregion + return sql; + } - #region Persist + protected override Sql GetBaseQuery(bool isCount) => + GetBaseQuery(isCount ? QueryType.Count : QueryType.Single); - protected override void PersistNewItem(IMedia entity) - { - entity.AddingEntity(); + // ah maybe not, that what's used for eg Exists in base repo + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Node}.id = @id"; - // ensure unique name on the same level - entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name)!; - - // ensure that strings don't contain characters that are invalid in xml - // TODO: do we really want to keep doing this here? - entity.SanitizeEntityPropertiesForXmlStorage(); + protected override IEnumerable GetDeleteClauses() + { + var list = new List + { + "DELETE FROM " + Constants.DatabaseSchema.Tables.User2NodeNotify + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.UserGroup2Node + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.UserGroup2NodePermission + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.UserStartNode + " WHERE startNode = @id", + "UPDATE " + Constants.DatabaseSchema.Tables.UserGroup + + " SET startContentId = NULL WHERE startContentId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Relation + " WHERE parentId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Relation + " WHERE childId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.TagRelationship + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Document + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.MediaVersion + " WHERE id IN (SELECT id FROM " + + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", + "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyData + " WHERE versionId IN (SELECT id FROM " + + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", + "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Content + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Node + " WHERE id = @id" + }; + return list; + } - // create the dto - var dto = ContentBaseFactory.BuildDto(_mediaUrlGenerators, entity); - - // derive path and level from parent - var parent = GetParentNodeDto(entity.ParentId); - var level = parent.Level + 1; - - // get sort order - var sortOrder = GetNewChildSortOrder(entity.ParentId, 0); - - // persist the node dto - var nodeDto = dto.ContentDto.NodeDto; - nodeDto.Path = parent.Path; - nodeDto.Level = Convert.ToInt16(level); - nodeDto.SortOrder = sortOrder; - - // see if there's a reserved identifier for this unique id - // and then either update or insert the node dto - var id = GetReservedId(nodeDto.UniqueId); - if (id > 0) - nodeDto.NodeId = id; - else - Database.Insert(nodeDto); - - nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); - nodeDto.ValidatePathWithException(); - Database.Update(nodeDto); - - // update entity - entity.Id = nodeDto.NodeId; - entity.Path = nodeDto.Path; - entity.SortOrder = sortOrder; - entity.Level = level; - - // persist the content dto - var contentDto = dto.ContentDto; - contentDto.NodeId = nodeDto.NodeId; - Database.Insert(contentDto); - - // persist the content version dto - // assumes a new version id and version date (modified date) has been set - var contentVersionDto = dto.MediaVersionDto.ContentVersionDto; - contentVersionDto.NodeId = nodeDto.NodeId; - contentVersionDto.Current = true; - Database.Insert(contentVersionDto); - entity.VersionId = contentVersionDto.Id; + #endregion - // persist the media version dto - var mediaVersionDto = dto.MediaVersionDto; - mediaVersionDto.Id = entity.VersionId; - Database.Insert(mediaVersionDto); + #region Versions - // persist the property data - InsertPropertyValues(entity, 0, out _, out _); + public override IEnumerable GetAllVersions(int nodeId) + { + Sql sql = GetBaseQuery(QueryType.Many, false) + .Where(x => x.NodeId == nodeId) + .OrderByDescending(x => x.Current) + .AndByDescending(x => x.VersionDate); - // set tags - SetEntityTags(entity, _tagRepository, _serializer); + return MapDtosToContent(Database.Fetch(sql), true); + } - PersistRelations(entity); + public override IMedia? GetVersion(int versionId) + { + Sql sql = GetBaseQuery(QueryType.Single) + .Where(x => x.Id == versionId); - OnUowRefreshedEntity(new MediaRefreshNotification(entity, new EventMessages())); + ContentDto? dto = Database.Fetch(sql).FirstOrDefault(); + return dto == null ? null : MapDtoToContent(dto); + } - entity.ResetDirtyProperties(); - } + public IMedia? GetMediaByPath(string mediaPath) + { + var umbracoFileValue = mediaPath; + const string pattern = ".*[_][0-9]+[x][0-9]+[.].*"; + var isResized = Regex.IsMatch(mediaPath, pattern); - protected override void PersistUpdatedItem(IMedia entity) + // If the image has been resized we strip the "_403x328" of the original "/media/1024/koala_403x328.jpg" URL. + if (isResized) { - // update - entity.UpdatingEntity(); - - // Check if this entity is being moved as a descendant as part of a bulk moving operations. - // In this case we can bypass a lot of the below operations which will make this whole operation go much faster. - // When moving we don't need to create new versions, etc... because we cannot roll this operation back anyways. - var isMoving = entity.IsMoving(); + var underscoreIndex = mediaPath.LastIndexOf('_'); + var dotIndex = mediaPath.LastIndexOf('.'); + umbracoFileValue = string.Concat(mediaPath.Substring(0, underscoreIndex), mediaPath.Substring(dotIndex)); + } - if (!isMoving) - { - // ensure unique name on the same level - entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id)!; + Sql sql = GetBaseQuery(QueryType.Single, joinMediaVersion: true) + .Where(x => x.Path == umbracoFileValue) + .SelectTop(1); - // ensure that strings don't contain characters that are invalid in xml - // TODO: do we really want to keep doing this here? - entity.SanitizeEntityPropertiesForXmlStorage(); + ContentDto? dto = Database.Fetch(sql).FirstOrDefault(); + return dto == null + ? null + : MapDtoToContent(dto); + } - // if parent has changed, get path, level and sort order - if (entity.IsPropertyDirty(nameof(entity.ParentId))) - { - var parent = GetParentNodeDto(entity.ParentId); + protected override void PerformDeleteVersion(int id, int versionId) + { + Database.Delete("WHERE versionId = @versionId", new {versionId}); + Database.Delete("WHERE versionId = @versionId", new {versionId}); + } - entity.Path = string.Concat(parent.Path, ",", entity.Id); - entity.Level = parent.Level + 1; - entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0); - } - } + #endregion - // create the dto - var dto = ContentBaseFactory.BuildDto(_mediaUrlGenerators, entity); + #region Persist - // update the node dto - var nodeDto = dto.ContentDto.NodeDto; - nodeDto.ValidatePathWithException(); - Database.Update(nodeDto); + protected override void PersistNewItem(IMedia entity) + { + entity.AddingEntity(); - if (!isMoving) - { - // update the content dto - Database.Update(dto.ContentDto); + // ensure unique name on the same level + entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name)!; - // update the content & media version dtos - var contentVersionDto = dto.MediaVersionDto.ContentVersionDto; - var mediaVersionDto = dto.MediaVersionDto; - contentVersionDto.Current = true; - Database.Update(contentVersionDto); - Database.Update(mediaVersionDto); + // ensure that strings don't contain characters that are invalid in xml + // TODO: do we really want to keep doing this here? + entity.SanitizeEntityPropertiesForXmlStorage(); - // replace the property data - ReplacePropertyValues(entity, entity.VersionId, 0, out _, out _); + // create the dto + MediaDto dto = ContentBaseFactory.BuildDto(_mediaUrlGenerators, entity); - SetEntityTags(entity, _tagRepository, _serializer); + // derive path and level from parent + NodeDto parent = GetParentNodeDto(entity.ParentId); + var level = parent.Level + 1; - PersistRelations(entity); - } + // get sort order + var sortOrder = GetNewChildSortOrder(entity.ParentId, 0); - OnUowRefreshedEntity(new MediaRefreshNotification(entity, new EventMessages())); + // persist the node dto + NodeDto nodeDto = dto.ContentDto.NodeDto; + nodeDto.Path = parent.Path; + nodeDto.Level = Convert.ToInt16(level); + nodeDto.SortOrder = sortOrder; - entity.ResetDirtyProperties(); + // see if there's a reserved identifier for this unique id + // and then either update or insert the node dto + var id = GetReservedId(nodeDto.UniqueId); + if (id > 0) + { + nodeDto.NodeId = id; } - - protected override void PersistDeletedItem(IMedia entity) + else { - // Raise event first else potential FK issues - OnUowRemovingEntity(entity); - base.PersistDeletedItem(entity); + Database.Insert(nodeDto); } - #endregion + nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); + nodeDto.ValidatePathWithException(); + Database.Update(nodeDto); - #region Recycle Bin + // update entity + entity.Id = nodeDto.NodeId; + entity.Path = nodeDto.Path; + entity.SortOrder = sortOrder; + entity.Level = level; - public override int RecycleBinId => Cms.Core.Constants.System.RecycleBinMedia; + // persist the content dto + ContentDto contentDto = dto.ContentDto; + contentDto.NodeId = nodeDto.NodeId; + Database.Insert(contentDto); - public bool RecycleBinSmells() - { - var cache = _cache.RuntimeCache; - var cacheKey = CacheKeys.MediaRecycleBinCacheKey; + // persist the content version dto + // assumes a new version id and version date (modified date) has been set + ContentVersionDto contentVersionDto = dto.MediaVersionDto.ContentVersionDto; + contentVersionDto.NodeId = nodeDto.NodeId; + contentVersionDto.Current = true; + Database.Insert(contentVersionDto); + entity.VersionId = contentVersionDto.Id; - // always cache either true or false - return cache.GetCacheItem(cacheKey, () => CountChildren(RecycleBinId) > 0); - } + // persist the media version dto + MediaVersionDto mediaVersionDto = dto.MediaVersionDto; + mediaVersionDto.Id = entity.VersionId; + Database.Insert(mediaVersionDto); - #endregion + // persist the property data + InsertPropertyValues(entity, 0, out _, out _); - #region Read Repository implementation for Guid keys + // set tags + SetEntityTags(entity, _tagRepository, _serializer); - public IMedia? Get(Guid id) - { - return _mediaByGuidReadRepository.Get(id); - } + PersistRelations(entity); - IEnumerable IReadRepository.GetMany(params Guid[]? ids) - { - return _mediaByGuidReadRepository.GetMany(ids); - } + OnUowRefreshedEntity(new MediaRefreshNotification(entity, new EventMessages())); - public bool Exists(Guid id) - { - return _mediaByGuidReadRepository.Exists(id); - } + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IMedia entity) + { + // update + entity.UpdatingEntity(); + // Check if this entity is being moved as a descendant as part of a bulk moving operations. + // In this case we can bypass a lot of the below operations which will make this whole operation go much faster. + // When moving we don't need to create new versions, etc... because we cannot roll this operation back anyways. + var isMoving = entity.IsMoving(); - // A reading repository purely for looking up by GUID - // TODO: This is ugly and to fix we need to decouple the IRepositoryQueryable -> IRepository -> IReadRepository which should all be separate things! - // This sub-repository pattern is super old and totally unecessary anymore, caching can be handled in much nicer ways without this - private class MediaByGuidReadRepository : EntityRepositoryBase + if (!isMoving) { - private readonly MediaRepository _outerRepo; + // ensure unique name on the same level + entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id)!; + + // ensure that strings don't contain characters that are invalid in xml + // TODO: do we really want to keep doing this here? + entity.SanitizeEntityPropertiesForXmlStorage(); - public MediaByGuidReadRepository(MediaRepository outerRepo, IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) + // if parent has changed, get path, level and sort order + if (entity.IsPropertyDirty(nameof(entity.ParentId))) { - _outerRepo = outerRepo; + NodeDto parent = GetParentNodeDto(entity.ParentId); + + entity.Path = string.Concat(parent.Path, ",", entity.Id); + entity.Level = parent.Level + 1; + entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0); } + } - protected override IMedia? PerformGet(Guid id) - { - var sql = _outerRepo.GetBaseQuery(QueryType.Single) - .Where(x => x.UniqueId == id); + // create the dto + MediaDto dto = ContentBaseFactory.BuildDto(_mediaUrlGenerators, entity); - var dto = Database.Fetch(sql.SelectTop(1)).FirstOrDefault(); + // update the node dto + NodeDto nodeDto = dto.ContentDto.NodeDto; + nodeDto.ValidatePathWithException(); + Database.Update(nodeDto); - if (dto == null) - return null; + if (!isMoving) + { + // update the content dto + Database.Update(dto.ContentDto); - var content = _outerRepo.MapDtoToContent(dto); + // update the content & media version dtos + ContentVersionDto contentVersionDto = dto.MediaVersionDto.ContentVersionDto; + MediaVersionDto mediaVersionDto = dto.MediaVersionDto; + contentVersionDto.Current = true; + Database.Update(contentVersionDto); + Database.Update(mediaVersionDto); - return content; - } + // replace the property data + ReplacePropertyValues(entity, entity.VersionId, 0, out _, out _); - protected override IEnumerable PerformGetAll(params Guid[]? ids) - { - var sql = _outerRepo.GetBaseQuery(QueryType.Many); - if (ids?.Length > 0) - sql.WhereIn(x => x.UniqueId, ids); + SetEntityTags(entity, _tagRepository, _serializer); - return _outerRepo.MapDtosToContent(Database.Fetch(sql)); - } + PersistRelations(entity); + } - protected override IEnumerable PerformGetByQuery(IQuery query) - { - throw new InvalidOperationException("This method won't be implemented."); - } + OnUowRefreshedEntity(new MediaRefreshNotification(entity, new EventMessages())); - protected override IEnumerable GetDeleteClauses() - { - throw new InvalidOperationException("This method won't be implemented."); - } + entity.ResetDirtyProperties(); + } - protected override void PersistNewItem(IMedia entity) - { - throw new InvalidOperationException("This method won't be implemented."); - } + protected override void PersistDeletedItem(IMedia entity) + { + // Raise event first else potential FK issues + OnUowRemovingEntity(entity); + base.PersistDeletedItem(entity); + } - protected override void PersistUpdatedItem(IMedia entity) - { - throw new InvalidOperationException("This method won't be implemented."); - } + #endregion - protected override Sql GetBaseQuery(bool isCount) - { - throw new InvalidOperationException("This method won't be implemented."); - } + #region Recycle Bin - protected override string GetBaseWhereClause() - { - throw new InvalidOperationException("This method won't be implemented."); - } - } + public override int RecycleBinId => Constants.System.RecycleBinMedia; - #endregion + public bool RecycleBinSmells() + { + IAppPolicyCache cache = _cache.RuntimeCache; + var cacheKey = CacheKeys.MediaRecycleBinCacheKey; - /// - public override IEnumerable GetPage(IQuery? query, - long pageIndex, int pageSize, out long totalRecords, - IQuery? filter, Ordering? ordering) - { - Sql? filterSql = null; + // always cache either true or false + return cache.GetCacheItem(cacheKey, () => CountChildren(RecycleBinId) > 0); + } - if (filter != null) - { - filterSql = Sql(); - foreach (var clause in filter.GetWhereClauses()) - filterSql = filterSql.Append($"AND ({clause.Item1})", clause.Item2); - } + #endregion - return GetPage(query, pageIndex, pageSize, out totalRecords, - x => MapDtosToContent(x), - filterSql, - ordering); - } + #region Read Repository implementation for Guid keys - private IEnumerable MapDtosToContent(List dtos, bool withCache = false) - { - var temps = new List>(); - var contentTypes = new Dictionary(); - var content = new Core.Models.Media[dtos.Count]; + public IMedia? Get(Guid id) => _mediaByGuidReadRepository.Get(id); - for (var i = 0; i < dtos.Count; i++) - { - var dto = dtos[i]; + IEnumerable IReadRepository.GetMany(params Guid[]? ids) => + _mediaByGuidReadRepository.GetMany(ids); - if (withCache) - { - // if the cache contains the (proper version of the) item, use it - var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); - if (cached != null && cached.VersionId == dto.ContentVersionDto.Id) - { - content[i] = (Core.Models.Media)cached; - continue; - } - } + public bool Exists(Guid id) => _mediaByGuidReadRepository.Exists(id); - // else, need to build it - // get the content type - the repository is full cache *but* still deep-clones - // whatever comes out of it, so use our own local index here to avoid this - var contentTypeId = dto.ContentTypeId; - if (contentTypes.TryGetValue(contentTypeId, out IMediaType? contentType) == false) - contentTypes[contentTypeId] = contentType = _mediaTypeRepository.Get(contentTypeId); + // A reading repository purely for looking up by GUID + // TODO: This is ugly and to fix we need to decouple the IRepositoryQueryable -> IRepository -> IReadRepository which should all be separate things! + // This sub-repository pattern is super old and totally unecessary anymore, caching can be handled in much nicer ways without this + private class MediaByGuidReadRepository : EntityRepositoryBase + { + private readonly MediaRepository _outerRepo; - var c = content[i] = ContentBaseFactory.BuildEntity(dto, contentType); + public MediaByGuidReadRepository(MediaRepository outerRepo, IScopeAccessor scopeAccessor, AppCaches cache, + ILogger logger) + : base(scopeAccessor, cache, logger) => + _outerRepo = outerRepo; - // need properties - var versionId = dto.ContentVersionDto.Id; - temps.Add(new TempContent(dto.NodeId, versionId, 0, contentType, c)); - } + protected override IMedia? PerformGet(Guid id) + { + Sql sql = _outerRepo.GetBaseQuery(QueryType.Single) + .Where(x => x.UniqueId == id); - // load all properties for all documents from database in 1 query - indexed by version id - var properties = GetPropertyCollections(temps); + ContentDto? dto = Database.Fetch(sql.SelectTop(1)).FirstOrDefault(); - // assign properties - foreach (var temp in temps) + if (dto == null) { - if (temp.Content is not null) - { - temp.Content.Properties = properties[temp.VersionId]; - - // reset dirty initial properties (U4-1946) - temp.Content.ResetDirtyProperties(false); - } + return null; } + IMedia content = _outerRepo.MapDtoToContent(dto); + return content; } - private IMedia MapDtoToContent(ContentDto dto) + protected override IEnumerable PerformGetAll(params Guid[]? ids) { - var contentType = _mediaTypeRepository.Get(dto.ContentTypeId); - var media = ContentBaseFactory.BuildEntity(dto, contentType); - - // get properties - indexed by version id - var versionId = dto.ContentVersionDto.Id; - var temp = new TempContent(dto.NodeId, versionId, 0, contentType); - var properties = GetPropertyCollections(new List> { temp }); - media.Properties = properties[versionId]; + Sql sql = _outerRepo.GetBaseQuery(QueryType.Many); + if (ids?.Length > 0) + { + sql.WhereIn(x => x.UniqueId, ids); + } - // reset dirty initial properties (U4-1946) - media.ResetDirtyProperties(false); - return media; + return _outerRepo.MapDtosToContent(Database.Fetch(sql)); } + protected override IEnumerable PerformGetByQuery(IQuery query) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override IEnumerable GetDeleteClauses() => + throw new InvalidOperationException("This method won't be implemented."); + + protected override void PersistNewItem(IMedia entity) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override void PersistUpdatedItem(IMedia entity) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override Sql GetBaseQuery(bool isCount) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override string GetBaseWhereClause() => + throw new InvalidOperationException("This method won't be implemented."); } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeContainerRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeContainerRepository.cs index 069b49de2f00..10f62f09bc81 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeContainerRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeContainerRepository.cs @@ -1,14 +1,16 @@ using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Infrastructure.Scoping; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class MediaTypeContainerRepository : EntityContainerRepository, IMediaTypeContainerRepository { - class MediaTypeContainerRepository : EntityContainerRepository, IMediaTypeContainerRepository + public MediaTypeContainerRepository(IScopeAccessor scopeAccessor, AppCaches cache, + ILogger logger) + : base(scopeAccessor, cache, logger, Constants.ObjectTypes.MediaTypeContainer) { - public MediaTypeContainerRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger, Cms.Core.Constants.ObjectTypes.MediaTypeContainer) - { } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeRepository.cs index 6742d2457dfe..6900e96d46c9 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -14,126 +11,122 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents a repository for doing CRUD operations for +/// +internal class MediaTypeRepository : ContentTypeRepositoryBase, IMediaTypeRepository { - /// - /// Represents a repository for doing CRUD operations for - /// - internal class MediaTypeRepository : ContentTypeRepositoryBase, IMediaTypeRepository + public MediaTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, + IContentTypeCommonRepository commonRepository, ILanguageRepository languageRepository, + IShortStringHelper shortStringHelper) + : base(scopeAccessor, cache, logger, commonRepository, languageRepository, shortStringHelper) { - public MediaTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository, ILanguageRepository languageRepository, IShortStringHelper shortStringHelper) - : base(scopeAccessor, cache, logger, commonRepository, languageRepository, shortStringHelper) - { } + } - protected override bool SupportsPublishing => MediaType.SupportsPublishingConst; + protected override bool SupportsPublishing => MediaType.SupportsPublishingConst; - protected override IRepositoryCachePolicy CreateCachePolicy() - { - return new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ true); - } + protected override Guid NodeObjectTypeId => Constants.ObjectTypes.MediaType; - // every GetExists method goes cachePolicy.GetSomething which in turns goes PerformGetAll, - // since this is a FullDataSet policy - and everything is cached - // so here, - // every PerformGet/Exists just GetMany() and then filters - // except PerformGetAll which is the one really doing the job + protected override IRepositoryCachePolicy CreateCachePolicy() => + new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, + GetEntityId, /*expires:*/ true); - protected override IMediaType? PerformGet(int id) - => GetMany()?.FirstOrDefault(x => x.Id == id); + // every GetExists method goes cachePolicy.GetSomething which in turns goes PerformGetAll, + // since this is a FullDataSet policy - and everything is cached + // so here, + // every PerformGet/Exists just GetMany() and then filters + // except PerformGetAll which is the one really doing the job - protected override IMediaType? PerformGet(Guid id) - => GetMany()?.FirstOrDefault(x => x.Key == id); + protected override IMediaType? PerformGet(int id) + => GetMany()?.FirstOrDefault(x => x.Id == id); - protected override bool PerformExists(Guid id) - => GetMany()?.FirstOrDefault(x => x.Key == id) != null; + protected override IMediaType? PerformGet(Guid id) + => GetMany()?.FirstOrDefault(x => x.Key == id); - protected override IMediaType? PerformGet(string alias) - => GetMany()?.FirstOrDefault(x => x.Alias.InvariantEquals(alias)); + protected override bool PerformExists(Guid id) + => GetMany()?.FirstOrDefault(x => x.Key == id) != null; - protected override IEnumerable? GetAllWithFullCachePolicy() - { - return CommonRepository.GetAllTypes()?.OfType(); - } + protected override IMediaType? PerformGet(string alias) + => GetMany()?.FirstOrDefault(x => x.Alias.InvariantEquals(alias)); - protected override IEnumerable? PerformGetAll(params Guid[]? ids) - { - var all = GetMany(); - return ids?.Any() ?? false ? all?.Where(x => ids.Contains(x.Key)) : all; - } + protected override IEnumerable? GetAllWithFullCachePolicy() => + CommonRepository.GetAllTypes()?.OfType(); - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var baseQuery = GetBaseQuery(false); - var translator = new SqlTranslator(baseQuery, query); - var sql = translator.Translate(); - var ids = Database.Fetch(sql).Distinct().ToArray(); + protected override IEnumerable? PerformGetAll(params Guid[]? ids) + { + IEnumerable? all = GetMany(); + return ids?.Any() ?? false ? all?.Where(x => ids.Contains(x.Key)) : all; + } - return ids.Length > 0 ? GetMany(ids).OrderBy(x => x.Name).WhereNotNull() : Enumerable.Empty(); - } + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql baseQuery = GetBaseQuery(false); + var translator = new SqlTranslator(baseQuery, query); + Sql sql = translator.Translate(); + var ids = Database.Fetch(sql).Distinct().ToArray(); - protected override Sql GetBaseQuery(bool isCount) - { - var sql = Sql(); + return ids.Length > 0 ? GetMany(ids).OrderBy(x => x.Name).WhereNotNull() : Enumerable.Empty(); + } - sql = isCount - ? sql.SelectCount() - : sql.Select(x => x.NodeId); + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); - sql - .From() - .InnerJoin().On( left => left.NodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId); + sql = isCount + ? sql.SelectCount() + : sql.Select(x => x.NodeId); - return sql; - } + sql + .From() + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId); - protected override string GetBaseWhereClause() - { - return $"{Constants.DatabaseSchema.Tables.Node}.id = @id"; - } + return sql; + } - protected override IEnumerable GetDeleteClauses() - { - var l = (List) base.GetDeleteClauses(); // we know it's a list - l.Add("DELETE FROM cmsContentType WHERE nodeId = @id"); - l.Add("DELETE FROM umbracoNode WHERE id = @id"); - return l; - } + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Node}.id = @id"; - protected override Guid NodeObjectTypeId => Cms.Core.Constants.ObjectTypes.MediaType; + protected override IEnumerable GetDeleteClauses() + { + var l = (List)base.GetDeleteClauses(); // we know it's a list + l.Add("DELETE FROM cmsContentType WHERE nodeId = @id"); + l.Add("DELETE FROM umbracoNode WHERE id = @id"); + return l; + } - protected override void PersistNewItem(IMediaType entity) - { - entity.AddingEntity(); + protected override void PersistNewItem(IMediaType entity) + { + entity.AddingEntity(); - PersistNewBaseContentType(entity); + PersistNewBaseContentType(entity); - entity.ResetDirtyProperties(); - } + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IMediaType entity) + { + ValidateAlias(entity); + + //Updates Modified date + entity.UpdatingEntity(); - protected override void PersistUpdatedItem(IMediaType entity) + //Look up parent to get and set the correct Path if ParentId has changed + if (entity.IsPropertyDirty("ParentId")) { - ValidateAlias(entity); - - //Updates Modified date - entity.UpdatingEntity(); - - //Look up parent to get and set the correct Path if ParentId has changed - if (entity.IsPropertyDirty("ParentId")) - { - var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); - entity.Path = string.Concat(parent.Path, ",", entity.Id); - entity.Level = parent.Level + 1; - var maxSortOrder = - Database.ExecuteScalar( - "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", - new { ParentId = entity.ParentId, NodeObjectType = NodeObjectTypeId }); - entity.SortOrder = maxSortOrder + 1; - } - - PersistUpdatedBaseContentType(entity); - - entity.ResetDirtyProperties(); + NodeDto? parent = Database.First("WHERE id = @ParentId", new {entity.ParentId}); + entity.Path = string.Concat(parent.Path, ",", entity.Id); + entity.Level = parent.Level + 1; + var maxSortOrder = + Database.ExecuteScalar( + "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", + new {entity.ParentId, NodeObjectType = NodeObjectTypeId}); + entity.SortOrder = maxSortOrder + 1; } + + PersistUpdatedBaseContentType(entity); + + entity.ResetDirtyProperties(); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberGroupRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberGroupRepository.cs index f94ffe03a3c0..8206cdc98ca4 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberGroupRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberGroupRepository.cs @@ -1,8 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; @@ -15,307 +13,299 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class MemberGroupRepository : EntityRepositoryBase, IMemberGroupRepository { - internal class MemberGroupRepository : EntityRepositoryBase, IMemberGroupRepository - { - private readonly IEventMessagesFactory _eventMessagesFactory; + private readonly IEventMessagesFactory _eventMessagesFactory; - public MemberGroupRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IEventMessagesFactory eventMessagesFactory) - : base(scopeAccessor, cache, logger) => - _eventMessagesFactory = eventMessagesFactory; + public MemberGroupRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, + IEventMessagesFactory eventMessagesFactory) + : base(scopeAccessor, cache, logger) => + _eventMessagesFactory = eventMessagesFactory; - protected override IMemberGroup? PerformGet(int id) - { - var sql = GetBaseQuery(false); - sql.Where(GetBaseWhereClause(), new { id = id }); + protected Guid NodeObjectTypeId => Constants.ObjectTypes.MemberGroup; - var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); + public IMemberGroup? Get(Guid uniqueId) + { + Sql sql = GetBaseQuery(false); + sql.Where(x => x.UniqueId == uniqueId); - return dto == null ? null : MemberGroupFactory.BuildEntity(dto); - } + NodeDto? dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); - protected override IEnumerable PerformGetAll(params int[]? ids) - { - var sql = Sql() - .SelectAll() - .From() - .Where(dto => dto.NodeObjectType == NodeObjectTypeId); - - if (ids?.Any() ?? false) - sql.WhereIn(x => x.NodeId, ids); + return dto == null ? null : MemberGroupFactory.BuildEntity(dto); + } - return Database.Fetch(sql).Select(x => MemberGroupFactory.BuildEntity(x)); - } + public IMemberGroup? GetByName(string? name) => + IsolatedCache.GetCacheItem( + typeof(IMemberGroup).FullName + "." + name, + () => + { + IQuery qry = Query().Where(group => group.Name!.Equals(name)); + IEnumerable? result = Get(qry); + return result?.FirstOrDefault(); + }, + //cache for 5 mins since that is the default in the Runtime app cache + TimeSpan.FromMinutes(5), + //sliding is true + true); + + public IMemberGroup? CreateIfNotExists(string roleName) + { + IQuery qry = Query().Where(group => group.Name!.Equals(roleName)); + IEnumerable? result = Get(qry); - protected override IEnumerable PerformGetByQuery(IQuery query) + if (result?.Any() ?? false) { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - - return Database.Fetch(sql).Select(x => MemberGroupFactory.BuildEntity(x)); + return null; } - protected override Sql GetBaseQuery(bool isCount) - { - var sql = Sql(); + var grp = new MemberGroup {Name = roleName}; + PersistNewItem(grp); - sql = isCount - ? sql.SelectCount() - : sql.Select(); + EventMessages evtMsgs = _eventMessagesFactory.Get(); + if (AmbientScope.Notifications.PublishCancelable(new MemberGroupSavingNotification(grp, evtMsgs))) + { + return null; + } - sql - .From() - .Where(x => x.NodeObjectType == NodeObjectTypeId); + AmbientScope.Notifications.Publish(new MemberGroupSavedNotification(grp, evtMsgs)); - return sql; - } + return grp; + } - protected override string GetBaseWhereClause() - { - return $"{Cms.Core.Constants.DatabaseSchema.Tables.Node}.id = @id"; - } + public IEnumerable GetMemberGroupsForMember(int memberId) + { + Sql sql = Sql() + .Select("umbracoNode.*") + .From() + .InnerJoin() + .On(dto => dto.NodeId, dto => dto.MemberGroup) + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .Where(x => x.Member == memberId); + + return Database.Fetch(sql) + .DistinctBy(dto => dto.NodeId) + .Select(x => MemberGroupFactory.BuildEntity(x)); + } - protected override IEnumerable GetDeleteClauses() - { - var list = new[] - { - "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @id", - "DELETE FROM umbracoUserGroup2Node WHERE nodeId = @id", - "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @id", - "DELETE FROM umbracoRelation WHERE parentId = @id", - "DELETE FROM umbracoRelation WHERE childId = @id", - "DELETE FROM cmsTagRelationship WHERE nodeId = @id", - "DELETE FROM cmsMember2MemberGroup WHERE MemberGroup = @id", - "DELETE FROM umbracoNode WHERE id = @id" - }; - return list; - } + public IEnumerable GetMemberGroupsForMember(string? username) + { + Sql? sql = Sql() + .Select("un.*") + .From("umbracoNode AS un") + .InnerJoin("cmsMember2MemberGroup") + .On("cmsMember2MemberGroup.MemberGroup = un.id") + .InnerJoin("cmsMember") + .On("cmsMember.nodeId = cmsMember2MemberGroup.Member") + .Where("un.nodeObjectType=@objectType", new {objectType = NodeObjectTypeId}) + .Where("cmsMember.LoginName=@loginName", new {loginName = username}); + + return Database.Fetch(sql) + .DistinctBy(dto => dto.NodeId) + .Select(x => MemberGroupFactory.BuildEntity(x)); + } - protected Guid NodeObjectTypeId => Cms.Core.Constants.ObjectTypes.MemberGroup; - protected override void PersistNewItem(IMemberGroup entity) - { - //Save to db - entity.AddingEntity(); - var group = (MemberGroup)entity; - var dto = MemberGroupFactory.BuildDto(group); - var o = Database.IsNew(dto) ? Convert.ToInt32(Database.Insert(dto)) : Database.Update(dto); - group.Id = dto.NodeId; //Set Id on entity to ensure an Id is set - - //Update with new correct path and id - dto.Path = string.Concat("-1,", dto.NodeId); - Database.Update(dto); - //assign to entity - group.Id = o; - group.ResetDirtyProperties(); - } + public void ReplaceRoles(int[] memberIds, string[] roleNames) => AssignRolesInternal(memberIds, roleNames, true); - protected override void PersistUpdatedItem(IMemberGroup entity) - { - var dto = MemberGroupFactory.BuildDto(entity); + public void AssignRoles(int[] memberIds, string[] roleNames) => AssignRolesInternal(memberIds, roleNames); - Database.Update(dto); + public void DissociateRoles(int[] memberIds, string[] roleNames) => DissociateRolesInternal(memberIds, roleNames); - entity.ResetDirtyProperties(); - } + protected override IMemberGroup? PerformGet(int id) + { + Sql sql = GetBaseQuery(false); + sql.Where(GetBaseWhereClause(), new {id}); - public IMemberGroup? Get(Guid uniqueId) - { - var sql = GetBaseQuery(false); - sql.Where(x => x.UniqueId == uniqueId); + NodeDto? dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); - var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); + return dto == null ? null : MemberGroupFactory.BuildEntity(dto); + } - return dto == null ? null : MemberGroupFactory.BuildEntity(dto); - } + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = Sql() + .SelectAll() + .From() + .Where(dto => dto.NodeObjectType == NodeObjectTypeId); - public IMemberGroup? GetByName(string? name) + if (ids?.Any() ?? false) { - return IsolatedCache.GetCacheItem( - typeof(IMemberGroup).FullName + "." + name, - () => - { - var qry = Query().Where(group => group.Name!.Equals(name)); - var result = Get(qry); - return result?.FirstOrDefault(); - }, - //cache for 5 mins since that is the default in the Runtime app cache - TimeSpan.FromMinutes(5), - //sliding is true - true); + sql.WhereIn(x => x.NodeId, ids); } - public IMemberGroup? CreateIfNotExists(string roleName) - { - var qry = Query().Where(group => group.Name!.Equals(roleName)); - var result = Get(qry); + return Database.Fetch(sql).Select(x => MemberGroupFactory.BuildEntity(x)); + } - if (result?.Any() ?? false) - return null; + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); - var grp = new MemberGroup - { - Name = roleName - }; - PersistNewItem(grp); + return Database.Fetch(sql).Select(x => MemberGroupFactory.BuildEntity(x)); + } - var evtMsgs = _eventMessagesFactory.Get(); - if (AmbientScope.Notifications.PublishCancelable(new MemberGroupSavingNotification(grp, evtMsgs))) - { - return null; - } + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); - AmbientScope.Notifications.Publish(new MemberGroupSavedNotification(grp, evtMsgs)); + sql = isCount + ? sql.SelectCount() + : sql.Select(); - return grp; - } + sql + .From() + .Where(x => x.NodeObjectType == NodeObjectTypeId); - public IEnumerable GetMemberGroupsForMember(int memberId) - { - var sql = Sql() - .Select("umbracoNode.*") - .From() - .InnerJoin() - .On(dto => dto.NodeId, dto => dto.MemberGroup) - .Where(x => x.NodeObjectType == NodeObjectTypeId) - .Where(x => x.Member == memberId); + return sql; + } - return Database.Fetch(sql) - .DistinctBy(dto => dto.NodeId) - .Select(x => MemberGroupFactory.BuildEntity(x)); - } + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Node}.id = @id"; - public IEnumerable GetMemberGroupsForMember(string? username) + protected override IEnumerable GetDeleteClauses() + { + var list = new[] { - var sql = Sql() - .Select("un.*") - .From("umbracoNode AS un") - .InnerJoin("cmsMember2MemberGroup") - .On("cmsMember2MemberGroup.MemberGroup = un.id") - .InnerJoin("cmsMember") - .On("cmsMember.nodeId = cmsMember2MemberGroup.Member") - .Where("un.nodeObjectType=@objectType", new { objectType = NodeObjectTypeId }) - .Where("cmsMember.LoginName=@loginName", new { loginName = username }); - - return Database.Fetch(sql) - .DistinctBy(dto => dto.NodeId) - .Select(x => MemberGroupFactory.BuildEntity(x)); - } + "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @id", + "DELETE FROM umbracoUserGroup2Node WHERE nodeId = @id", + "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @id", + "DELETE FROM umbracoRelation WHERE parentId = @id", "DELETE FROM umbracoRelation WHERE childId = @id", + "DELETE FROM cmsTagRelationship WHERE nodeId = @id", + "DELETE FROM cmsMember2MemberGroup WHERE MemberGroup = @id", "DELETE FROM umbracoNode WHERE id = @id" + }; + return list; + } + protected override void PersistNewItem(IMemberGroup entity) + { + //Save to db + entity.AddingEntity(); + var group = (MemberGroup)entity; + NodeDto dto = MemberGroupFactory.BuildDto(group); + var o = Database.IsNew(dto) ? Convert.ToInt32(Database.Insert(dto)) : Database.Update(dto); + group.Id = dto.NodeId; //Set Id on entity to ensure an Id is set + + //Update with new correct path and id + dto.Path = string.Concat("-1,", dto.NodeId); + Database.Update(dto); + //assign to entity + group.Id = o; + group.ResetDirtyProperties(); + } + protected override void PersistUpdatedItem(IMemberGroup entity) + { + NodeDto dto = MemberGroupFactory.BuildDto(entity); - public void ReplaceRoles(int[] memberIds, string[] roleNames) => AssignRolesInternal(memberIds, roleNames, true); + Database.Update(dto); - public void AssignRoles(int[] memberIds, string[] roleNames) => AssignRolesInternal(memberIds, roleNames); + entity.ResetDirtyProperties(); + } - private void AssignRolesInternal(int[] memberIds, string[] roleNames, bool replace = false) + private void AssignRolesInternal(int[] memberIds, string[] roleNames, bool replace = false) + { + //ensure they're unique + memberIds = memberIds.Distinct().ToArray(); + + //create the missing roles first + + Sql existingSql = Sql() + .SelectAll() + .From() + .Where(dto => dto.NodeObjectType == NodeObjectTypeId) + .Where("umbracoNode." + SqlSyntax.GetQuotedColumnName("text") + " in (@names)", new {names = roleNames}); + IEnumerable existingRoles = Database.Fetch(existingSql).Select(x => x.Text); + IEnumerable missingRoles = roleNames.Except(existingRoles, StringComparer.CurrentCultureIgnoreCase); + MemberGroup[] missingGroups = missingRoles.Select(x => new MemberGroup {Name = x}).ToArray(); + + EventMessages evtMsgs = _eventMessagesFactory.Get(); + if (AmbientScope.Notifications.PublishCancelable(new MemberGroupSavingNotification(missingGroups, evtMsgs))) { - //ensure they're unique - memberIds = memberIds.Distinct().ToArray(); - - //create the missing roles first - - Sql existingSql = Sql() - .SelectAll() - .From() - .Where(dto => dto.NodeObjectType == NodeObjectTypeId) - .Where("umbracoNode." + SqlSyntax.GetQuotedColumnName("text") + " in (@names)", new { names = roleNames }); - IEnumerable existingRoles = Database.Fetch(existingSql).Select(x => x.Text); - IEnumerable missingRoles = roleNames.Except(existingRoles, StringComparer.CurrentCultureIgnoreCase); - MemberGroup[] missingGroups = missingRoles.Select(x => new MemberGroup { Name = x }).ToArray(); - - var evtMsgs = _eventMessagesFactory.Get(); - if (AmbientScope.Notifications.PublishCancelable(new MemberGroupSavingNotification(missingGroups, evtMsgs))) - { - return; - } - - foreach (MemberGroup m in missingGroups) - { - PersistNewItem(m); - } - - AmbientScope.Notifications.Publish(new MemberGroupSavedNotification(missingGroups, evtMsgs)); + return; + } - //now go get all the dto's for roles with these role names - var rolesForNames = Database.Fetch(existingSql) - .ToDictionary(x => x.Text!, StringComparer.InvariantCultureIgnoreCase); + foreach (MemberGroup m in missingGroups) + { + PersistNewItem(m); + } - AssignedRolesDto[] currentlyAssigned; - if (replace) - { - // delete all assigned groups first - Database.Execute("DELETE FROM cmsMember2MemberGroup WHERE Member IN (@memberIds)", new { memberIds }); + AmbientScope.Notifications.Publish(new MemberGroupSavedNotification(missingGroups, evtMsgs)); - currentlyAssigned = Array.Empty(); - } - else - { - //get the groups that are currently assigned to any of these members + //now go get all the dto's for roles with these role names + var rolesForNames = Database.Fetch(existingSql) + .ToDictionary(x => x.Text!, StringComparer.InvariantCultureIgnoreCase); - Sql assignedSql = Sql() - .Select($"{SqlSyntax.GetQuotedColumnName("text")},{SqlSyntax.GetQuotedColumnName("Member")},{SqlSyntax.GetQuotedColumnName("MemberGroup")}") - .From() - .InnerJoin() - .On(dto => dto.NodeId, dto => dto.MemberGroup) - .Where(x => x.NodeObjectType == NodeObjectTypeId) - .WhereIn(x => x.Member, memberIds); + AssignedRolesDto[] currentlyAssigned; + if (replace) + { + // delete all assigned groups first + Database.Execute("DELETE FROM cmsMember2MemberGroup WHERE Member IN (@memberIds)", new {memberIds}); - currentlyAssigned = Database.Fetch(assignedSql).ToArray(); - } + currentlyAssigned = Array.Empty(); + } + else + { + //get the groups that are currently assigned to any of these members - //assign the roles for each member id + Sql assignedSql = Sql() + .Select( + $"{SqlSyntax.GetQuotedColumnName("text")},{SqlSyntax.GetQuotedColumnName("Member")},{SqlSyntax.GetQuotedColumnName("MemberGroup")}") + .From() + .InnerJoin() + .On(dto => dto.NodeId, dto => dto.MemberGroup) + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .WhereIn(x => x.Member, memberIds); - foreach (var memberId in memberIds) - { - //find any roles for the current member that are currently assigned that - //exist in the roleNames list, then determine which ones are not currently assigned. - var mId = memberId; - AssignedRolesDto[] found = currentlyAssigned.Where(x => x.MemberId == mId).ToArray(); - IEnumerable assignedRoles = found.Where(x => roleNames.Contains(x.RoleName, StringComparer.CurrentCultureIgnoreCase)).Select(x => x.RoleName); - IEnumerable nonAssignedRoles = roleNames.Except(assignedRoles, StringComparer.CurrentCultureIgnoreCase); - - IEnumerable dtos = nonAssignedRoles - .Select(x => new Member2MemberGroupDto - { - Member = mId, - MemberGroup = rolesForNames[x!].NodeId - }); - - Database.InsertBulk(dtos); - } + currentlyAssigned = Database.Fetch(assignedSql).ToArray(); } - public void DissociateRoles(int[] memberIds, string[] roleNames) - { - DissociateRolesInternal(memberIds, roleNames); - } + //assign the roles for each member id - private void DissociateRolesInternal(int[] memberIds, string[] roleNames) + foreach (var memberId in memberIds) { - var existingSql = Sql() - .SelectAll() - .From() - .Where(dto => dto.NodeObjectType == NodeObjectTypeId) - .Where("umbracoNode." + SqlSyntax.GetQuotedColumnName("text") + " in (@names)", new { names = roleNames }); - var existingRolesIds = Database.Fetch(existingSql).Select(x => x.NodeId).ToArray(); - - Database.Execute("DELETE FROM cmsMember2MemberGroup WHERE Member IN (@memberIds) AND MemberGroup IN (@memberGroups)", - new { /*memberIds =*/ memberIds, memberGroups = existingRolesIds }); + //find any roles for the current member that are currently assigned that + //exist in the roleNames list, then determine which ones are not currently assigned. + var mId = memberId; + AssignedRolesDto[] found = currentlyAssigned.Where(x => x.MemberId == mId).ToArray(); + IEnumerable assignedRoles = found + .Where(x => roleNames.Contains(x.RoleName, StringComparer.CurrentCultureIgnoreCase)) + .Select(x => x.RoleName); + IEnumerable nonAssignedRoles = + roleNames.Except(assignedRoles, StringComparer.CurrentCultureIgnoreCase); + + IEnumerable dtos = nonAssignedRoles + .Select(x => new Member2MemberGroupDto {Member = mId, MemberGroup = rolesForNames[x!].NodeId}); + + Database.InsertBulk(dtos); } + } - private class AssignedRolesDto - { - [Column("text")] - public string? RoleName { get; set; } + private void DissociateRolesInternal(int[] memberIds, string[] roleNames) + { + Sql? existingSql = Sql() + .SelectAll() + .From() + .Where(dto => dto.NodeObjectType == NodeObjectTypeId) + .Where("umbracoNode." + SqlSyntax.GetQuotedColumnName("text") + " in (@names)", new {names = roleNames}); + var existingRolesIds = Database.Fetch(existingSql).Select(x => x.NodeId).ToArray(); + + Database.Execute( + "DELETE FROM cmsMember2MemberGroup WHERE Member IN (@memberIds) AND MemberGroup IN (@memberGroups)", + new + { + /*memberIds =*/ + memberIds, memberGroups = existingRolesIds + }); + } - [Column("Member")] - public int MemberId { get; set; } + private class AssignedRolesDto + { + [Column("text")] public string? RoleName { get; set; } - [Column("MemberGroup")] - public int MemberGroupId { get; set; } - } + [Column("Member")] public int MemberId { get; set; } + + [Column("MemberGroup")] public int MemberGroupId { get; set; } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs index 9c414824367a..ec7b05fcb011 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NPoco; @@ -15,7 +12,6 @@ using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.PropertyEditors; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; @@ -26,800 +22,799 @@ using Umbraco.Extensions; using static Umbraco.Cms.Core.Persistence.SqlExtensionsStatics; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents a repository for doing CRUD operations for +/// +public class MemberRepository : ContentRepositoryBase, IMemberRepository { + private readonly IJsonSerializer _jsonSerializer; + private readonly IRepositoryCachePolicy _memberByUsernameCachePolicy; + private readonly IMemberGroupRepository _memberGroupRepository; + private readonly IMemberTypeRepository _memberTypeRepository; + private readonly MemberPasswordConfigurationSettings _passwordConfiguration; + private readonly IPasswordHasher _passwordHasher; + private readonly ITagRepository _tagRepository; + private bool _passwordConfigInitialized; + private string? _passwordConfigJson; + + public MemberRepository( + IScopeAccessor scopeAccessor, + AppCaches cache, + ILogger logger, + IMemberTypeRepository memberTypeRepository, + IMemberGroupRepository memberGroupRepository, + ITagRepository tagRepository, + ILanguageRepository languageRepository, + IRelationRepository relationRepository, + IRelationTypeRepository relationTypeRepository, + IPasswordHasher passwordHasher, + PropertyEditorCollection propertyEditors, + DataValueReferenceFactoryCollection dataValueReferenceFactories, + IDataTypeService dataTypeService, + IJsonSerializer serializer, + IEventAggregator eventAggregator, + IOptions passwordConfiguration) + : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, + propertyEditors, dataValueReferenceFactories, dataTypeService, eventAggregator) + { + _memberTypeRepository = + memberTypeRepository ?? throw new ArgumentNullException(nameof(memberTypeRepository)); + _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); + _passwordHasher = passwordHasher; + _jsonSerializer = serializer; + _memberGroupRepository = memberGroupRepository; + _passwordConfiguration = passwordConfiguration.Value; + _memberByUsernameCachePolicy = + new DefaultRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, DefaultOptions); + } + /// - /// Represents a repository for doing CRUD operations for + /// Returns a serialized dictionary of the password configuration that is stored against the member in the database /// - public class MemberRepository : ContentRepositoryBase, IMemberRepository + private string? DefaultPasswordConfigJson { - private readonly IJsonSerializer _jsonSerializer; - private readonly IRepositoryCachePolicy _memberByUsernameCachePolicy; - private readonly IMemberGroupRepository _memberGroupRepository; - private readonly IMemberTypeRepository _memberTypeRepository; - private readonly MemberPasswordConfigurationSettings _passwordConfiguration; - private readonly IPasswordHasher _passwordHasher; - private readonly ITagRepository _tagRepository; - private bool _passwordConfigInitialized; - private string? _passwordConfigJson; - - public MemberRepository( - IScopeAccessor scopeAccessor, - AppCaches cache, - ILogger logger, - IMemberTypeRepository memberTypeRepository, - IMemberGroupRepository memberGroupRepository, - ITagRepository tagRepository, - ILanguageRepository languageRepository, - IRelationRepository relationRepository, - IRelationTypeRepository relationTypeRepository, - IPasswordHasher passwordHasher, - PropertyEditorCollection propertyEditors, - DataValueReferenceFactoryCollection dataValueReferenceFactories, - IDataTypeService dataTypeService, - IJsonSerializer serializer, - IEventAggregator eventAggregator, - IOptions passwordConfiguration) - : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, - propertyEditors, dataValueReferenceFactories, dataTypeService, eventAggregator) - { - _memberTypeRepository = - memberTypeRepository ?? throw new ArgumentNullException(nameof(memberTypeRepository)); - _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); - _passwordHasher = passwordHasher; - _jsonSerializer = serializer; - _memberGroupRepository = memberGroupRepository; - _passwordConfiguration = passwordConfiguration.Value; - _memberByUsernameCachePolicy = - new DefaultRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, DefaultOptions); - } - - /// - /// Returns a serialized dictionary of the password configuration that is stored against the member in the database - /// - private string? DefaultPasswordConfigJson - { - get + get + { + if (_passwordConfigInitialized) { - if (_passwordConfigInitialized) - { - return _passwordConfigJson; - } - - var passwordConfig = new PersistedPasswordSettings - { - HashAlgorithm = _passwordConfiguration.HashAlgorithmType - }; - - _passwordConfigJson = passwordConfig == null ? null : _jsonSerializer.Serialize(passwordConfig); - _passwordConfigInitialized = true; return _passwordConfigJson; } + + var passwordConfig = new PersistedPasswordSettings + { + HashAlgorithm = _passwordConfiguration.HashAlgorithmType + }; + + _passwordConfigJson = passwordConfig == null ? null : _jsonSerializer.Serialize(passwordConfig); + _passwordConfigInitialized = true; + return _passwordConfigJson; } + } - protected override MemberRepository This => this; + protected override MemberRepository This => this; - public override int RecycleBinId => throw new NotSupportedException(); + public override int RecycleBinId => throw new NotSupportedException(); - public IEnumerable FindMembersInRole(string roleName, string usernameToMatch, - StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) + public IEnumerable FindMembersInRole(string roleName, string usernameToMatch, + StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) + { + //get the group id + IQuery grpQry = Query().Where(group => group.Name!.Equals(roleName)); + IMemberGroup? memberGroup = _memberGroupRepository.Get(grpQry)?.FirstOrDefault(); + if (memberGroup == null) { - //get the group id - IQuery grpQry = Query().Where(group => group.Name!.Equals(roleName)); - IMemberGroup? memberGroup = _memberGroupRepository.Get(grpQry)?.FirstOrDefault(); - if (memberGroup == null) - { - return Enumerable.Empty(); - } + return Enumerable.Empty(); + } - // get the members by username - IQuery query = Query(); - switch (matchType) - { - case StringPropertyMatchType.Exact: - query.Where(member => member.Username.Equals(usernameToMatch)); - break; - case StringPropertyMatchType.Contains: - query.Where(member => member.Username.Contains(usernameToMatch)); - break; - case StringPropertyMatchType.StartsWith: - query.Where(member => member.Username.StartsWith(usernameToMatch)); - break; - case StringPropertyMatchType.EndsWith: - query.Where(member => member.Username.EndsWith(usernameToMatch)); - break; - case StringPropertyMatchType.Wildcard: - query.Where(member => member.Username.SqlWildcard(usernameToMatch, TextColumnType.NVarchar)); - break; - default: - throw new ArgumentOutOfRangeException(nameof(matchType)); - } + // get the members by username + IQuery query = Query(); + switch (matchType) + { + case StringPropertyMatchType.Exact: + query.Where(member => member.Username.Equals(usernameToMatch)); + break; + case StringPropertyMatchType.Contains: + query.Where(member => member.Username.Contains(usernameToMatch)); + break; + case StringPropertyMatchType.StartsWith: + query.Where(member => member.Username.StartsWith(usernameToMatch)); + break; + case StringPropertyMatchType.EndsWith: + query.Where(member => member.Username.EndsWith(usernameToMatch)); + break; + case StringPropertyMatchType.Wildcard: + query.Where(member => member.Username.SqlWildcard(usernameToMatch, TextColumnType.NVarchar)); + break; + default: + throw new ArgumentOutOfRangeException(nameof(matchType)); + } - IMember[]? matchedMembers = Get(query)?.ToArray(); + IMember[]? matchedMembers = Get(query)?.ToArray(); - var membersInGroup = new List(); + var membersInGroup = new List(); - if (matchedMembers is null) - { - return membersInGroup; - } - //then we need to filter the matched members that are in the role - foreach (IEnumerable group in matchedMembers.Select(x => x.Id) - .InGroupsOf(Constants.Sql.MaxParameterCount)) - { - Sql sql = Sql().SelectAll().From() - .Where(dto => dto.MemberGroup == memberGroup.Id) - .WhereIn(dto => dto.Member, group); + if (matchedMembers is null) + { + return membersInGroup; + } - var memberIdsInGroup = Database.Fetch(sql) - .Select(x => x.Member).ToArray(); + //then we need to filter the matched members that are in the role + foreach (IEnumerable group in matchedMembers.Select(x => x.Id) + .InGroupsOf(Constants.Sql.MaxParameterCount)) + { + Sql sql = Sql().SelectAll().From() + .Where(dto => dto.MemberGroup == memberGroup.Id) + .WhereIn(dto => dto.Member, group); - membersInGroup.AddRange(matchedMembers.Where(x => memberIdsInGroup.Contains(x.Id))); - } + var memberIdsInGroup = Database.Fetch(sql) + .Select(x => x.Member).ToArray(); - return membersInGroup; + membersInGroup.AddRange(matchedMembers.Where(x => memberIdsInGroup.Contains(x.Id))); } - /// - /// Get all members in a specific group - /// - /// - /// - public IEnumerable GetByMemberGroup(string groupName) + return membersInGroup; + } + + /// + /// Get all members in a specific group + /// + /// + /// + public IEnumerable GetByMemberGroup(string groupName) + { + IQuery grpQry = Query().Where(group => group.Name!.Equals(groupName)); + IMemberGroup? memberGroup = _memberGroupRepository.Get(grpQry)?.FirstOrDefault(); + if (memberGroup == null) { - IQuery grpQry = Query().Where(group => group.Name!.Equals(groupName)); - IMemberGroup? memberGroup = _memberGroupRepository.Get(grpQry)?.FirstOrDefault(); - if (memberGroup == null) - { - return Enumerable.Empty(); - } + return Enumerable.Empty(); + } - Sql subQuery = Sql().Select("Member").From() - .Where(dto => dto.MemberGroup == memberGroup.Id); + Sql subQuery = Sql().Select("Member").From() + .Where(dto => dto.MemberGroup == memberGroup.Id); - Sql sql = GetBaseQuery(false) - // TODO: An inner join would be better, though I've read that the query optimizer will always turn a - // subquery with an IN clause into an inner join anyways. - .Append("WHERE umbracoNode.id IN (" + subQuery.SQL + ")", subQuery.Arguments) - .OrderByDescending(x => x.VersionDate) - .OrderBy(x => x.SortOrder); + Sql sql = GetBaseQuery(false) + // TODO: An inner join would be better, though I've read that the query optimizer will always turn a + // subquery with an IN clause into an inner join anyways. + .Append("WHERE umbracoNode.id IN (" + subQuery.SQL + ")", subQuery.Arguments) + .OrderByDescending(x => x.VersionDate) + .OrderBy(x => x.SortOrder); - return MapDtosToContent(Database.Fetch(sql)); - } + return MapDtosToContent(Database.Fetch(sql)); + } - public bool Exists(string username) - { - Sql sql = Sql() - .SelectCount() - .From() - .Where(x => x.LoginName == username); + public bool Exists(string username) + { + Sql sql = Sql() + .SelectCount() + .From() + .Where(x => x.LoginName == username); - return Database.ExecuteScalar(sql) > 0; - } + return Database.ExecuteScalar(sql) > 0; + } - public int GetCountByQuery(IQuery? query) - { - Sql sqlWithProps = GetNodeIdQueryWithPropertyData(); - var translator = new SqlTranslator(sqlWithProps, query); - Sql sql = translator.Translate(); + public int GetCountByQuery(IQuery? query) + { + Sql sqlWithProps = GetNodeIdQueryWithPropertyData(); + var translator = new SqlTranslator(sqlWithProps, query); + Sql sql = translator.Translate(); - //get the COUNT base query - Sql fullSql = GetBaseQuery(true) - .Append(new Sql("WHERE umbracoNode.id IN (" + sql.SQL + ")", sql.Arguments)); + //get the COUNT base query + Sql fullSql = GetBaseQuery(true) + .Append(new Sql("WHERE umbracoNode.id IN (" + sql.SQL + ")", sql.Arguments)); - return Database.ExecuteScalar(fullSql); - } + return Database.ExecuteScalar(fullSql); + } - /// - [Obsolete( - "This is now a NoOp since last login date is no longer an umbraco property, set the date on the IMember directly and Save it instead, scheduled for removal in V11.")] - public void SetLastLogin(string username, DateTime date) - { + /// + [Obsolete( + "This is now a NoOp since last login date is no longer an umbraco property, set the date on the IMember directly and Save it instead, scheduled for removal in V11.")] + public void SetLastLogin(string username, DateTime date) + { + } - } + /// + /// Gets paged member results. + /// + public override IEnumerable GetPage(IQuery? query, + long pageIndex, int pageSize, out long totalRecords, + IQuery? filter, + Ordering? ordering) + { + Sql? filterSql = null; - /// - /// Gets paged member results. - /// - public override IEnumerable GetPage(IQuery? query, - long pageIndex, int pageSize, out long totalRecords, - IQuery? filter, - Ordering? ordering) + if (filter != null) { - Sql? filterSql = null; - - if (filter != null) + filterSql = Sql(); + foreach (Tuple clause in filter.GetWhereClauses()) { - filterSql = Sql(); - foreach (Tuple clause in filter.GetWhereClauses()) - { - filterSql = filterSql.Append($"AND ({clause.Item1})", clause.Item2); - } + filterSql = filterSql.Append($"AND ({clause.Item1})", clause.Item2); } - - return GetPage(query, pageIndex, pageSize, out totalRecords, - x => MapDtosToContent(x), - filterSql, - ordering); } - public IMember? GetByUsername(string? username) => - _memberByUsernameCachePolicy.Get(username, PerformGetByUsername, PerformGetAllByUsername); + return GetPage(query, pageIndex, pageSize, out totalRecords, + x => MapDtosToContent(x), + filterSql, + ordering); + } - public int[] GetMemberIds(string[] usernames) - { - Guid memberObjectType = Constants.ObjectTypes.Member; + public IMember? GetByUsername(string? username) => + _memberByUsernameCachePolicy.Get(username, PerformGetByUsername, PerformGetAllByUsername); - Sql memberSql = Sql() - .Select("umbracoNode.id") - .From() - .InnerJoin() - .On(dto => dto.NodeId, dto => dto.NodeId) - .Where(x => x.NodeObjectType == memberObjectType) - .Where("cmsMember.LoginName in (@usernames)", new - { - /*usernames =*/ - usernames - }); - return Database.Fetch(memberSql).ToArray(); + public int[] GetMemberIds(string[] usernames) + { + Guid memberObjectType = Constants.ObjectTypes.Member; + + Sql memberSql = Sql() + .Select("umbracoNode.id") + .From() + .InnerJoin() + .On(dto => dto.NodeId, dto => dto.NodeId) + .Where(x => x.NodeObjectType == memberObjectType) + .Where("cmsMember.LoginName in (@usernames)", new + { + /*usernames =*/ + usernames + }); + return Database.Fetch(memberSql).ToArray(); + } + + protected override string ApplySystemOrdering(ref Sql sql, Ordering ordering) + { + if (ordering.OrderBy.InvariantEquals("email")) + { + return SqlSyntax.GetFieldName(x => x.Email); } - protected override string ApplySystemOrdering(ref Sql sql, Ordering ordering) + if (ordering.OrderBy.InvariantEquals("loginName")) { - if (ordering.OrderBy.InvariantEquals("email")) - { - return SqlSyntax.GetFieldName(x => x.Email); - } + return SqlSyntax.GetFieldName(x => x.LoginName); + } - if (ordering.OrderBy.InvariantEquals("loginName")) - { - return SqlSyntax.GetFieldName(x => x.LoginName); - } + if (ordering.OrderBy.InvariantEquals("userName")) + { + return SqlSyntax.GetFieldName(x => x.LoginName); + } - if (ordering.OrderBy.InvariantEquals("userName")) - { - return SqlSyntax.GetFieldName(x => x.LoginName); - } + if (ordering.OrderBy.InvariantEquals("updateDate")) + { + return SqlSyntax.GetFieldName(x => x.VersionDate); + } - if (ordering.OrderBy.InvariantEquals("updateDate")) - { - return SqlSyntax.GetFieldName(x => x.VersionDate); - } + if (ordering.OrderBy.InvariantEquals("createDate")) + { + return SqlSyntax.GetFieldName(x => x.CreateDate); + } - if (ordering.OrderBy.InvariantEquals("createDate")) - { - return SqlSyntax.GetFieldName(x => x.CreateDate); - } + if (ordering.OrderBy.InvariantEquals("contentTypeAlias")) + { + return SqlSyntax.GetFieldName(x => x.Alias); + } - if (ordering.OrderBy.InvariantEquals("contentTypeAlias")) - { - return SqlSyntax.GetFieldName(x => x.Alias); - } + return base.ApplySystemOrdering(ref sql, ordering); + } - return base.ApplySystemOrdering(ref sql, ordering); - } + private IEnumerable MapDtosToContent(List dtos, bool withCache = false) + { + var temps = new List>(); + var contentTypes = new Dictionary(); + var content = new Member[dtos.Count]; - private IEnumerable MapDtosToContent(List dtos, bool withCache = false) + for (var i = 0; i < dtos.Count; i++) { - var temps = new List>(); - var contentTypes = new Dictionary(); - var content = new Member[dtos.Count]; + MemberDto dto = dtos[i]; - for (var i = 0; i < dtos.Count; i++) + if (withCache) { - MemberDto dto = dtos[i]; - - if (withCache) + // if the cache contains the (proper version of the) item, use it + IMember? cached = + IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); + if (cached != null && cached.VersionId == dto.ContentVersionDto.Id) { - // if the cache contains the (proper version of the) item, use it - IMember? cached = - IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); - if (cached != null && cached.VersionId == dto.ContentVersionDto.Id) - { - content[i] = (Member)cached; - continue; - } + content[i] = (Member)cached; + continue; } + } - // else, need to build it + // else, need to build it - // get the content type - the repository is full cache *but* still deep-clones - // whatever comes out of it, so use our own local index here to avoid this - var contentTypeId = dto.ContentDto.ContentTypeId; - if (contentTypes.TryGetValue(contentTypeId, out IMemberType? contentType) == false) - { - contentTypes[contentTypeId] = contentType = _memberTypeRepository.Get(contentTypeId); - } + // get the content type - the repository is full cache *but* still deep-clones + // whatever comes out of it, so use our own local index here to avoid this + var contentTypeId = dto.ContentDto.ContentTypeId; + if (contentTypes.TryGetValue(contentTypeId, out IMemberType? contentType) == false) + { + contentTypes[contentTypeId] = contentType = _memberTypeRepository.Get(contentTypeId); + } - Member c = content[i] = ContentBaseFactory.BuildEntity(dto, contentType); + Member c = content[i] = ContentBaseFactory.BuildEntity(dto, contentType); - // need properties - var versionId = dto.ContentVersionDto.Id; - temps.Add(new TempContent(dto.NodeId, versionId, 0, contentType, c)); - } + // need properties + var versionId = dto.ContentVersionDto.Id; + temps.Add(new TempContent(dto.NodeId, versionId, 0, contentType, c)); + } - // load all properties for all documents from database in 1 query - indexed by version id - IDictionary properties = GetPropertyCollections(temps); + // load all properties for all documents from database in 1 query - indexed by version id + IDictionary properties = GetPropertyCollections(temps); - // assign properties - foreach (TempContent temp in temps) + // assign properties + foreach (TempContent temp in temps) + { + if (temp.Content is not null) { - if (temp.Content is not null) - { - temp.Content.Properties = properties[temp.VersionId]; + temp.Content.Properties = properties[temp.VersionId]; - // reset dirty initial properties (U4-1946) - temp.Content.ResetDirtyProperties(false); - } + // reset dirty initial properties (U4-1946) + temp.Content.ResetDirtyProperties(false); } - - return content; } - private IMember MapDtoToContent(MemberDto dto) - { - IMemberType? memberType = _memberTypeRepository.Get(dto.ContentDto.ContentTypeId); - Member member = ContentBaseFactory.BuildEntity(dto, memberType); + return content; + } - // get properties - indexed by version id - var versionId = dto.ContentVersionDto.Id; - var temp = new TempContent(dto.ContentDto.NodeId, versionId, 0, memberType); - IDictionary properties = - GetPropertyCollections(new List> { temp }); - member.Properties = properties[versionId]; + private IMember MapDtoToContent(MemberDto dto) + { + IMemberType? memberType = _memberTypeRepository.Get(dto.ContentDto.ContentTypeId); + Member member = ContentBaseFactory.BuildEntity(dto, memberType); + + // get properties - indexed by version id + var versionId = dto.ContentVersionDto.Id; + var temp = new TempContent(dto.ContentDto.NodeId, versionId, 0, memberType); + IDictionary properties = + GetPropertyCollections(new List> {temp}); + member.Properties = properties[versionId]; + + // reset dirty initial properties (U4-1946) + member.ResetDirtyProperties(false); + return member; + } - // reset dirty initial properties (U4-1946) - member.ResetDirtyProperties(false); - return member; - } + private IMember? PerformGetByUsername(string? username) + { + IQuery query = Query().Where(x => x.Username.Equals(username)); + return PerformGetByQuery(query).FirstOrDefault(); + } - private IMember? PerformGetByUsername(string? username) - { - IQuery query = Query().Where(x => x.Username.Equals(username)); - return PerformGetByQuery(query).FirstOrDefault(); - } + private IEnumerable PerformGetAllByUsername(params string[]? usernames) + { + IQuery query = Query().WhereIn(x => x.Username, usernames); + return PerformGetByQuery(query); + } + + #region Repository Base + + protected override Guid NodeObjectTypeId => Constants.ObjectTypes.Member; - private IEnumerable PerformGetAllByUsername(params string[]? usernames) + protected override IMember? PerformGet(int id) + { + Sql sql = GetBaseQuery(QueryType.Single) + .Where(x => x.NodeId == id) + .SelectTop(1); + + MemberDto? dto = Database.Fetch(sql).FirstOrDefault(); + return dto == null + ? null + : MapDtoToContent(dto); + } + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = GetBaseQuery(QueryType.Many); + + if (ids?.Any() ?? false) { - IQuery query = Query().WhereIn(x => x.Username, usernames); - return PerformGetByQuery(query); + sql.WhereIn(x => x.NodeId, ids); } - #region Repository Base + return MapDtosToContent(Database.Fetch(sql)); + } - protected override Guid NodeObjectTypeId => Constants.ObjectTypes.Member; + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql baseQuery = GetBaseQuery(false); + + // TODO: why is this different from content/media?! + // check if the query is based on properties or not - protected override IMember? PerformGet(int id) + IEnumerable> wheres = query.GetWhereClauses(); + //this is a pretty rudimentary check but will work, we just need to know if this query requires property + // level queries + if (wheres.Any(x => x.Item1.Contains("cmsPropertyType"))) { - Sql sql = GetBaseQuery(QueryType.Single) - .Where(x => x.NodeId == id) - .SelectTop(1); + Sql sqlWithProps = GetNodeIdQueryWithPropertyData(); + var translator = new SqlTranslator(sqlWithProps, query); + Sql sql = translator.Translate(); - MemberDto? dto = Database.Fetch(sql).FirstOrDefault(); - return dto == null - ? null - : MapDtoToContent(dto); - } + baseQuery.Append("WHERE umbracoNode.id IN (" + sql.SQL + ")", sql.Arguments) + .OrderBy(x => x.SortOrder); - protected override IEnumerable PerformGetAll(params int[]? ids) + return MapDtosToContent(Database.Fetch(baseQuery)); + } + else { - Sql sql = GetBaseQuery(QueryType.Many); - - if (ids?.Any() ?? false) - { - sql.WhereIn(x => x.NodeId, ids); - } + var translator = new SqlTranslator(baseQuery, query); + Sql sql = translator.Translate() + .OrderBy(x => x.SortOrder); return MapDtosToContent(Database.Fetch(sql)); } + } - protected override IEnumerable PerformGetByQuery(IQuery query) - { - Sql baseQuery = GetBaseQuery(false); + protected override Sql GetBaseQuery(QueryType queryType) => GetBaseQuery(queryType, true); - // TODO: why is this different from content/media?! - // check if the query is based on properties or not + protected virtual Sql GetBaseQuery(QueryType queryType, bool current) + { + Sql sql = SqlContext.Sql(); - IEnumerable> wheres = query.GetWhereClauses(); - //this is a pretty rudimentary check but will work, we just need to know if this query requires property - // level queries - if (wheres.Any(x => x.Item1.Contains("cmsPropertyType"))) - { - Sql sqlWithProps = GetNodeIdQueryWithPropertyData(); - var translator = new SqlTranslator(sqlWithProps, query); - Sql sql = translator.Translate(); + switch (queryType) // TODO: pretend we still need these queries for now + { + case QueryType.Count: + sql = sql.SelectCount(); + break; + case QueryType.Ids: + sql = sql.Select(x => x.NodeId); + break; + case QueryType.Single: + case QueryType.Many: + sql = sql.Select(r => + r.Select(x => x.ContentVersionDto) + .Select(x => x.ContentDto, r1 => + r1.Select(x => x.NodeDto))) + + // ContentRepositoryBase expects a variantName field to order by name + // so get it here, though for members it's just the plain node name + .AndSelect(x => Alias(x.Text, "variantName")); + break; + } - baseQuery.Append("WHERE umbracoNode.id IN (" + sql.SQL + ")", sql.Arguments) - .OrderBy(x => x.SortOrder); + sql + .From() + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) - return MapDtosToContent(Database.Fetch(baseQuery)); - } - else - { - var translator = new SqlTranslator(baseQuery, query); - Sql sql = translator.Translate() - .OrderBy(x => x.SortOrder); + // joining the type so we can do a query against the member type - not sure if this adds much overhead or not? + // the execution plan says it doesn't so we'll go with that and in that case, it might be worth joining the content + // types by default on the document and media repos so we can query by content type there too. + .InnerJoin() + .On(left => left.ContentTypeId, right => right.NodeId); - return MapDtosToContent(Database.Fetch(sql)); - } + sql.Where(x => x.NodeObjectType == NodeObjectTypeId); + + if (current) + { + sql.Where(x => x.Current); // always get the current version } - protected override Sql GetBaseQuery(QueryType queryType) => GetBaseQuery(queryType, true); + return sql; + } - protected virtual Sql GetBaseQuery(QueryType queryType, bool current) + // TODO: move that one up to Versionable! or better: kill it! + protected override Sql GetBaseQuery(bool isCount) => + GetBaseQuery(isCount ? QueryType.Count : QueryType.Single); + + protected override string GetBaseWhereClause() // TODO: can we kill / refactor this? + => + "umbracoNode.id = @id"; + + // TODO: document/understand that one + protected Sql GetNodeIdQueryWithPropertyData() => + Sql() + .Select("DISTINCT(umbracoNode.id)") + .From() + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .InnerJoin() + .On((left, right) => left.ContentTypeId == right.NodeId) + .InnerJoin() + .On((left, right) => left.NodeId == right.NodeId) + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .LeftJoin() + .On(left => left.ContentTypeId, right => right.ContentTypeId) + .LeftJoin() + .On(left => left.DataTypeId, right => right.NodeId) + .LeftJoin().On(x => x + .Where((left, right) => left.PropertyTypeId == right.Id) + .Where((left, right) => left.VersionId == right.Id)) + .Where(x => x.NodeObjectType == NodeObjectTypeId); + + protected override IEnumerable GetDeleteClauses() + { + var list = new List { - Sql sql = SqlContext.Sql(); - - switch (queryType) // TODO: pretend we still need these queries for now - { - case QueryType.Count: - sql = sql.SelectCount(); - break; - case QueryType.Ids: - sql = sql.Select(x => x.NodeId); - break; - case QueryType.Single: - case QueryType.Many: - sql = sql.Select(r => - r.Select(x => x.ContentVersionDto) - .Select(x => x.ContentDto, r1 => - r1.Select(x => x.NodeDto))) - - // ContentRepositoryBase expects a variantName field to order by name - // so get it here, though for members it's just the plain node name - .AndSelect(x => Alias(x.Text, "variantName")); - break; - } + "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @id", + "DELETE FROM umbracoUserGroup2Node WHERE nodeId = @id", + "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @id", + "DELETE FROM umbracoRelation WHERE parentId = @id", + "DELETE FROM umbracoRelation WHERE childId = @id", + "DELETE FROM cmsTagRelationship WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyData + + " WHERE versionId IN (SELECT id FROM " + Constants.DatabaseSchema.Tables.ContentVersion + + " WHERE nodeId = @id)", + "DELETE FROM cmsMember2MemberGroup WHERE Member = @id", + "DELETE FROM cmsMember WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Content + " WHERE nodeId = @id", + "DELETE FROM umbracoNode WHERE id = @id" + }; + return list; + } - sql - .From() - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) + #endregion - // joining the type so we can do a query against the member type - not sure if this adds much overhead or not? - // the execution plan says it doesn't so we'll go with that and in that case, it might be worth joining the content - // types by default on the document and media repos so we can query by content type there too. - .InnerJoin() - .On(left => left.ContentTypeId, right => right.NodeId); + #region Versions - sql.Where(x => x.NodeObjectType == NodeObjectTypeId); + public override IEnumerable GetAllVersions(int nodeId) + { + Sql sql = GetBaseQuery(QueryType.Many, false) + .Where(x => x.NodeId == nodeId) + .OrderByDescending(x => x.Current) + .AndByDescending(x => x.VersionDate); - if (current) - { - sql.Where(x => x.Current); // always get the current version - } + return MapDtosToContent(Database.Fetch(sql), true); + } - return sql; - } - - // TODO: move that one up to Versionable! or better: kill it! - protected override Sql GetBaseQuery(bool isCount) => - GetBaseQuery(isCount ? QueryType.Count : QueryType.Single); - - protected override string GetBaseWhereClause() // TODO: can we kill / refactor this? - => - "umbracoNode.id = @id"; - - // TODO: document/understand that one - protected Sql GetNodeIdQueryWithPropertyData() => - Sql() - .Select("DISTINCT(umbracoNode.id)") - .From() - .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .InnerJoin() - .On((left, right) => left.ContentTypeId == right.NodeId) - .InnerJoin() - .On((left, right) => left.NodeId == right.NodeId) - .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .LeftJoin() - .On(left => left.ContentTypeId, right => right.ContentTypeId) - .LeftJoin() - .On(left => left.DataTypeId, right => right.NodeId) - .LeftJoin().On(x => x - .Where((left, right) => left.PropertyTypeId == right.Id) - .Where((left, right) => left.VersionId == right.Id)) - .Where(x => x.NodeObjectType == NodeObjectTypeId); - - protected override IEnumerable GetDeleteClauses() - { - var list = new List - { - "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @id", - "DELETE FROM umbracoUserGroup2Node WHERE nodeId = @id", - "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @id", - "DELETE FROM umbracoRelation WHERE parentId = @id", - "DELETE FROM umbracoRelation WHERE childId = @id", - "DELETE FROM cmsTagRelationship WHERE nodeId = @id", - "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyData + - " WHERE versionId IN (SELECT id FROM " + Constants.DatabaseSchema.Tables.ContentVersion + - " WHERE nodeId = @id)", - "DELETE FROM cmsMember2MemberGroup WHERE Member = @id", - "DELETE FROM cmsMember WHERE nodeId = @id", - "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id", - "DELETE FROM " + Constants.DatabaseSchema.Tables.Content + " WHERE nodeId = @id", - "DELETE FROM umbracoNode WHERE id = @id" - }; - return list; - } + public override IMember? GetVersion(int versionId) + { + Sql sql = GetBaseQuery(QueryType.Single) + .Where(x => x.Id == versionId); - #endregion + MemberDto? dto = Database.Fetch(sql).FirstOrDefault(); + return dto == null ? null : MapDtoToContent(dto); + } - #region Versions + protected override void PerformDeleteVersion(int id, int versionId) + { + Database.Delete("WHERE versionId = @VersionId", new {versionId}); + Database.Delete("WHERE versionId = @VersionId", new {versionId}); + } - public override IEnumerable GetAllVersions(int nodeId) - { - Sql sql = GetBaseQuery(QueryType.Many, false) - .Where(x => x.NodeId == nodeId) - .OrderByDescending(x => x.Current) - .AndByDescending(x => x.VersionDate); + #endregion - return MapDtosToContent(Database.Fetch(sql), true); - } + #region Persist - public override IMember? GetVersion(int versionId) - { - Sql sql = GetBaseQuery(QueryType.Single) - .Where(x => x.Id == versionId); - - MemberDto? dto = Database.Fetch(sql).FirstOrDefault(); - return dto == null ? null : MapDtoToContent(dto); - } + protected override void PersistNewItem(IMember entity) + { + entity.AddingEntity(); - protected override void PerformDeleteVersion(int id, int versionId) + // ensure security stamp if missing + if (entity.SecurityStamp.IsNullOrWhiteSpace()) { - Database.Delete("WHERE versionId = @VersionId", new { versionId }); - Database.Delete("WHERE versionId = @VersionId", new { versionId }); + entity.SecurityStamp = Guid.NewGuid().ToString(); } - #endregion + // ensure that strings don't contain characters that are invalid in xml + // TODO: do we really want to keep doing this here? + entity.SanitizeEntityPropertiesForXmlStorage(); - #region Persist + // create the dto + MemberDto memberDto = ContentBaseFactory.BuildDto(entity); - protected override void PersistNewItem(IMember entity) - { - entity.AddingEntity(); + // check if we have a user config else use the default + memberDto.PasswordConfig = entity.PasswordConfiguration ?? DefaultPasswordConfigJson; - // ensure security stamp if missing - if (entity.SecurityStamp.IsNullOrWhiteSpace()) - { - entity.SecurityStamp = Guid.NewGuid().ToString(); - } - - // ensure that strings don't contain characters that are invalid in xml - // TODO: do we really want to keep doing this here? - entity.SanitizeEntityPropertiesForXmlStorage(); + // derive path and level from parent + NodeDto parent = GetParentNodeDto(entity.ParentId); + var level = parent.Level + 1; - // create the dto - MemberDto memberDto = ContentBaseFactory.BuildDto(entity); + // get sort order + var sortOrder = GetNewChildSortOrder(entity.ParentId, 0); - // check if we have a user config else use the default - memberDto.PasswordConfig = entity.PasswordConfiguration ?? DefaultPasswordConfigJson; + // persist the node dto + NodeDto nodeDto = memberDto.ContentDto.NodeDto; + nodeDto.Path = parent.Path; + nodeDto.Level = Convert.ToInt16(level); + nodeDto.SortOrder = sortOrder; - // derive path and level from parent - NodeDto parent = GetParentNodeDto(entity.ParentId); - var level = parent.Level + 1; - - // get sort order - var sortOrder = GetNewChildSortOrder(entity.ParentId, 0); - - // persist the node dto - NodeDto nodeDto = memberDto.ContentDto.NodeDto; - nodeDto.Path = parent.Path; - nodeDto.Level = Convert.ToInt16(level); - nodeDto.SortOrder = sortOrder; + // see if there's a reserved identifier for this unique id + // and then either update or insert the node dto + var id = GetReservedId(nodeDto.UniqueId); + if (id > 0) + { + nodeDto.NodeId = id; + nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); + nodeDto.ValidatePathWithException(); + Database.Update(nodeDto); + } + else + { + Database.Insert(nodeDto); - // see if there's a reserved identifier for this unique id - // and then either update or insert the node dto - var id = GetReservedId(nodeDto.UniqueId); - if (id > 0) - { - nodeDto.NodeId = id; - nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); - nodeDto.ValidatePathWithException(); - Database.Update(nodeDto); - } - else - { - Database.Insert(nodeDto); + // update path, now that we have an id + nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); + nodeDto.ValidatePathWithException(); + Database.Update(nodeDto); + } - // update path, now that we have an id - nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); - nodeDto.ValidatePathWithException(); - Database.Update(nodeDto); - } + // update entity + entity.Id = nodeDto.NodeId; + entity.Path = nodeDto.Path; + entity.SortOrder = sortOrder; + entity.Level = level; + + // persist the content dto + ContentDto contentDto = memberDto.ContentDto; + contentDto.NodeId = nodeDto.NodeId; + Database.Insert(contentDto); + + // persist the content version dto + // assumes a new version id and version date (modified date) has been set + ContentVersionDto contentVersionDto = memberDto.ContentVersionDto; + contentVersionDto.NodeId = nodeDto.NodeId; + contentVersionDto.Current = true; + Database.Insert(contentVersionDto); + entity.VersionId = contentVersionDto.Id; + + // persist the member dto + memberDto.NodeId = nodeDto.NodeId; + + // if the password is empty, generate one with the special prefix + // this will hash the guid with a salt so should be nicely random + if (entity.RawPasswordValue.IsNullOrWhiteSpace()) + { + memberDto.Password = Constants.Security.EmptyPasswordPrefix + + _passwordHasher.HashPassword(Guid.NewGuid().ToString("N")); + entity.RawPasswordValue = memberDto.Password; + } - // update entity - entity.Id = nodeDto.NodeId; - entity.Path = nodeDto.Path; - entity.SortOrder = sortOrder; - entity.Level = level; - - // persist the content dto - ContentDto contentDto = memberDto.ContentDto; - contentDto.NodeId = nodeDto.NodeId; - Database.Insert(contentDto); - - // persist the content version dto - // assumes a new version id and version date (modified date) has been set - ContentVersionDto contentVersionDto = memberDto.ContentVersionDto; - contentVersionDto.NodeId = nodeDto.NodeId; - contentVersionDto.Current = true; - Database.Insert(contentVersionDto); - entity.VersionId = contentVersionDto.Id; - - // persist the member dto - memberDto.NodeId = nodeDto.NodeId; - - // if the password is empty, generate one with the special prefix - // this will hash the guid with a salt so should be nicely random - if (entity.RawPasswordValue.IsNullOrWhiteSpace()) - { - memberDto.Password = Constants.Security.EmptyPasswordPrefix + - _passwordHasher.HashPassword(Guid.NewGuid().ToString("N")); - entity.RawPasswordValue = memberDto.Password; - } + Database.Insert(memberDto); - Database.Insert(memberDto); + // persist the property data + InsertPropertyValues(entity, 0, out _, out _); - // persist the property data - InsertPropertyValues(entity, 0, out _, out _); + SetEntityTags(entity, _tagRepository, _jsonSerializer); - SetEntityTags(entity, _tagRepository, _jsonSerializer); + PersistRelations(entity); - PersistRelations(entity); + OnUowRefreshedEntity(new MemberRefreshNotification(entity, new EventMessages())); - OnUowRefreshedEntity(new MemberRefreshNotification(entity, new EventMessages())); + entity.ResetDirtyProperties(); + } - entity.ResetDirtyProperties(); - } + protected override void PersistUpdatedItem(IMember entity) + { + // update + entity.UpdatingEntity(); - protected override void PersistUpdatedItem(IMember entity) + // ensure security stamp if missing + if (entity.SecurityStamp.IsNullOrWhiteSpace()) { - // update - entity.UpdatingEntity(); + entity.SecurityStamp = Guid.NewGuid().ToString(); + } - // ensure security stamp if missing - if (entity.SecurityStamp.IsNullOrWhiteSpace()) - { - entity.SecurityStamp = Guid.NewGuid().ToString(); - } + // ensure that strings don't contain characters that are invalid in xml + // TODO: do we really want to keep doing this here? + entity.SanitizeEntityPropertiesForXmlStorage(); - // ensure that strings don't contain characters that are invalid in xml - // TODO: do we really want to keep doing this here? - entity.SanitizeEntityPropertiesForXmlStorage(); + // if parent has changed, get path, level and sort order + if (entity.IsPropertyDirty("ParentId")) + { + NodeDto parent = GetParentNodeDto(entity.ParentId); - // if parent has changed, get path, level and sort order - if (entity.IsPropertyDirty("ParentId")) - { - NodeDto parent = GetParentNodeDto(entity.ParentId); + entity.Path = string.Concat(parent.Path, ",", entity.Id); + entity.Level = parent.Level + 1; + entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0); + } - entity.Path = string.Concat(parent.Path, ",", entity.Id); - entity.Level = parent.Level + 1; - entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0); - } + // create the dto + MemberDto memberDto = ContentBaseFactory.BuildDto(entity); - // create the dto - MemberDto memberDto = ContentBaseFactory.BuildDto(entity); + // update the node dto + NodeDto nodeDto = memberDto.ContentDto.NodeDto; + Database.Update(nodeDto); - // update the node dto - NodeDto nodeDto = memberDto.ContentDto.NodeDto; - Database.Update(nodeDto); + // update the content dto + Database.Update(memberDto.ContentDto); - // update the content dto - Database.Update(memberDto.ContentDto); + // update the content version dto + Database.Update(memberDto.ContentVersionDto); - // update the content version dto - Database.Update(memberDto.ContentVersionDto); + // update the member dto + // but only the changed columns, 'cos we cannot update password if empty + var changedCols = new List(); - // update the member dto - // but only the changed columns, 'cos we cannot update password if empty - var changedCols = new List(); + if (entity.IsPropertyDirty("SecurityStamp")) + { + changedCols.Add("securityStampToken"); + } - if (entity.IsPropertyDirty("SecurityStamp")) - { - changedCols.Add("securityStampToken"); - } + if (entity.IsPropertyDirty("Email")) + { + changedCols.Add("Email"); + } - if (entity.IsPropertyDirty("Email")) - { - changedCols.Add("Email"); - } + if (entity.IsPropertyDirty("Username")) + { + changedCols.Add("LoginName"); + } - if (entity.IsPropertyDirty("Username")) - { - changedCols.Add("LoginName"); - } + if (entity.IsPropertyDirty(nameof(entity.FailedPasswordAttempts))) + { + changedCols.Add(nameof(entity.FailedPasswordAttempts)); + } - if (entity.IsPropertyDirty(nameof(entity.FailedPasswordAttempts))) - { - changedCols.Add(nameof(entity.FailedPasswordAttempts)); - } + if (entity.IsPropertyDirty(nameof(entity.IsApproved))) + { + changedCols.Add(nameof(entity.IsApproved)); + } - if (entity.IsPropertyDirty(nameof(entity.IsApproved))) - { - changedCols.Add(nameof(entity.IsApproved)); - } + if (entity.IsPropertyDirty(nameof(entity.IsLockedOut))) + { + changedCols.Add(nameof(entity.IsLockedOut)); + } - if (entity.IsPropertyDirty(nameof(entity.IsLockedOut))) - { - changedCols.Add(nameof(entity.IsLockedOut)); - } + if (entity.IsPropertyDirty(nameof(entity.LastLockoutDate))) + { + changedCols.Add(nameof(entity.LastLockoutDate)); + } - if (entity.IsPropertyDirty(nameof(entity.LastLockoutDate))) - { - changedCols.Add(nameof(entity.LastLockoutDate)); - } + if (entity.IsPropertyDirty(nameof(entity.LastLoginDate))) + { + changedCols.Add(nameof(entity.LastLoginDate)); + } - if (entity.IsPropertyDirty(nameof(entity.LastLoginDate))) - { - changedCols.Add(nameof(entity.LastLoginDate)); - } + if (entity.IsPropertyDirty(nameof(entity.LastPasswordChangeDate))) + { + changedCols.Add(nameof(entity.LastPasswordChangeDate)); + } - if (entity.IsPropertyDirty(nameof(entity.LastPasswordChangeDate))) - { - changedCols.Add(nameof(entity.LastPasswordChangeDate)); - } + // this can occur from an upgrade + if (memberDto.PasswordConfig.IsNullOrWhiteSpace()) + { + memberDto.PasswordConfig = DefaultPasswordConfigJson; + changedCols.Add("passwordConfig"); + } + else if (memberDto.PasswordConfig == Constants.Security.UnknownPasswordConfigJson) + { + changedCols.Add("passwordConfig"); + } - // this can occur from an upgrade - if (memberDto.PasswordConfig.IsNullOrWhiteSpace()) - { - memberDto.PasswordConfig = DefaultPasswordConfigJson; - changedCols.Add("passwordConfig"); - } - else if (memberDto.PasswordConfig == Constants.Security.UnknownPasswordConfigJson) - { - changedCols.Add("passwordConfig"); - } + // do NOT update the password if it has not changed or if it is null or empty + if (entity.IsPropertyDirty("RawPasswordValue") && !string.IsNullOrWhiteSpace(entity.RawPasswordValue)) + { + changedCols.Add("Password"); - // do NOT update the password if it has not changed or if it is null or empty - if (entity.IsPropertyDirty("RawPasswordValue") && !string.IsNullOrWhiteSpace(entity.RawPasswordValue)) + // If the security stamp hasn't already updated we need to force it + if (entity.IsPropertyDirty("SecurityStamp") == false) { - changedCols.Add("Password"); - - // If the security stamp hasn't already updated we need to force it - if (entity.IsPropertyDirty("SecurityStamp") == false) - { - memberDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString(); - changedCols.Add("securityStampToken"); - } - - // check if we have a user config else use the default - memberDto.PasswordConfig = entity.PasswordConfiguration ?? DefaultPasswordConfigJson; - changedCols.Add("passwordConfig"); + memberDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString(); + changedCols.Add("securityStampToken"); } - // If userlogin or the email has changed then need to reset security stamp - if (changedCols.Contains("Email") || changedCols.Contains("LoginName")) - { - memberDto.EmailConfirmedDate = null; - changedCols.Add("emailConfirmedDate"); + // check if we have a user config else use the default + memberDto.PasswordConfig = entity.PasswordConfiguration ?? DefaultPasswordConfigJson; + changedCols.Add("passwordConfig"); + } - // If the security stamp hasn't already updated we need to force it - if (entity.IsPropertyDirty("SecurityStamp") == false) - { - memberDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString(); - changedCols.Add("securityStampToken"); - } - } + // If userlogin or the email has changed then need to reset security stamp + if (changedCols.Contains("Email") || changedCols.Contains("LoginName")) + { + memberDto.EmailConfirmedDate = null; + changedCols.Add("emailConfirmedDate"); - if (changedCols.Count > 0) + // If the security stamp hasn't already updated we need to force it + if (entity.IsPropertyDirty("SecurityStamp") == false) { - Database.Update(memberDto, changedCols); + memberDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString(); + changedCols.Add("securityStampToken"); } + } - ReplacePropertyValues(entity, entity.VersionId, 0, out _, out _); + if (changedCols.Count > 0) + { + Database.Update(memberDto, changedCols); + } - SetEntityTags(entity, _tagRepository, _jsonSerializer); + ReplacePropertyValues(entity, entity.VersionId, 0, out _, out _); - PersistRelations(entity); + SetEntityTags(entity, _tagRepository, _jsonSerializer); - OnUowRefreshedEntity(new MemberRefreshNotification(entity, new EventMessages())); + PersistRelations(entity); - entity.ResetDirtyProperties(); - } + OnUowRefreshedEntity(new MemberRefreshNotification(entity, new EventMessages())); - #endregion + entity.ResetDirtyProperties(); } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberTypeRepository.cs index e26e30f21b8c..9f5a64a3652e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberTypeRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberTypeRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -15,229 +12,234 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents a repository for doing CRUD operations for +/// +internal class MemberTypeRepository : ContentTypeRepositoryBase, IMemberTypeRepository { - /// - /// Represents a repository for doing CRUD operations for - /// - internal class MemberTypeRepository : ContentTypeRepositoryBase, IMemberTypeRepository - { - private readonly IShortStringHelper _shortStringHelper; + private readonly IShortStringHelper _shortStringHelper; - public MemberTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository, ILanguageRepository languageRepository, IShortStringHelper shortStringHelper) - : base(scopeAccessor, cache, logger, commonRepository, languageRepository, shortStringHelper) - { - _shortStringHelper = shortStringHelper; - } + public MemberTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, + IContentTypeCommonRepository commonRepository, ILanguageRepository languageRepository, + IShortStringHelper shortStringHelper) + : base(scopeAccessor, cache, logger, commonRepository, languageRepository, shortStringHelper) => + _shortStringHelper = shortStringHelper; - protected override bool SupportsPublishing => MemberType.SupportsPublishingConst; + protected override bool SupportsPublishing => MemberType.SupportsPublishingConst; - protected override IRepositoryCachePolicy CreateCachePolicy() - { - return new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ true); - } + protected override Guid NodeObjectTypeId => Constants.ObjectTypes.MemberType; - // every GetExists method goes cachePolicy.GetSomething which in turns goes PerformGetAll, - // since this is a FullDataSet policy - and everything is cached - // so here, - // every PerformGet/Exists just GetMany() and then filters - // except PerformGetAll which is the one really doing the job + protected override IRepositoryCachePolicy CreateCachePolicy() => + new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, + GetEntityId, /*expires:*/ true); - protected override IMemberType? PerformGet(int id) - => GetMany()?.FirstOrDefault(x => x.Id == id); + // every GetExists method goes cachePolicy.GetSomething which in turns goes PerformGetAll, + // since this is a FullDataSet policy - and everything is cached + // so here, + // every PerformGet/Exists just GetMany() and then filters + // except PerformGetAll which is the one really doing the job - protected override IMemberType? PerformGet(Guid id) - => GetMany()?.FirstOrDefault(x => x.Key == id); + protected override IMemberType? PerformGet(int id) + => GetMany()?.FirstOrDefault(x => x.Id == id); - protected override IEnumerable? PerformGetAll(params Guid[]? ids) - { - var all = GetMany(); - return ids?.Any() ?? false ? all?.Where(x => ids.Contains(x.Key)) : all; - } + protected override IMemberType? PerformGet(Guid id) + => GetMany()?.FirstOrDefault(x => x.Key == id); - protected override bool PerformExists(Guid id) - => GetMany()?.FirstOrDefault(x => x.Key == id) != null; + protected override IEnumerable? PerformGetAll(params Guid[]? ids) + { + IEnumerable? all = GetMany(); + return ids?.Any() ?? false ? all?.Where(x => ids.Contains(x.Key)) : all; + } - protected override IMemberType? PerformGet(string alias) - => GetMany()?.FirstOrDefault(x => x.Alias.InvariantEquals(alias)); + protected override bool PerformExists(Guid id) + => GetMany()?.FirstOrDefault(x => x.Key == id) != null; - protected override IEnumerable? GetAllWithFullCachePolicy() - { - return CommonRepository.GetAllTypes()?.OfType(); - } + protected override IMemberType? PerformGet(string alias) + => GetMany()?.FirstOrDefault(x => x.Alias.InvariantEquals(alias)); - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var subQuery = GetSubquery(); - var translator = new SqlTranslator(subQuery, query); - var subSql = translator.Translate(); - var sql = GetBaseQuery(false) - .WhereIn(x => x.NodeId, subSql) - .OrderBy(x => x.SortOrder); - var ids = Database.Fetch(sql).Distinct().ToArray(); - - return ids.Length > 0 ? GetMany(ids).OrderBy(x => x.Name) : Enumerable.Empty(); - } + protected override IEnumerable? GetAllWithFullCachePolicy() => + CommonRepository.GetAllTypes()?.OfType(); - protected override Sql GetBaseQuery(bool isCount) - { - if (isCount) - { - return Sql() - .SelectCount() - .From() - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId); - } - - var sql = Sql() - .Select(x => x.NodeId) - .From() - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .LeftJoin().On(left => left.ContentTypeId, right => right.NodeId) - .LeftJoin().On(left => left.PropertyTypeId, right => right.Id) - .LeftJoin().On(left => left.NodeId, right => right.DataTypeId) - .LeftJoin().On(left => left.ContentTypeNodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId); - - return sql; - } + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql subQuery = GetSubquery(); + var translator = new SqlTranslator(subQuery, query); + Sql subSql = translator.Translate(); + Sql sql = GetBaseQuery(false) + .WhereIn(x => x.NodeId, subSql) + .OrderBy(x => x.SortOrder); + var ids = Database.Fetch(sql).Distinct().ToArray(); + + return ids.Length > 0 ? GetMany(ids).OrderBy(x => x.Name) : Enumerable.Empty(); + } - protected Sql GetSubquery() + protected override Sql GetBaseQuery(bool isCount) + { + if (isCount) { - var sql = Sql() - .Select("DISTINCT(umbracoNode.id)") + return Sql() + .SelectCount() .From() .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .LeftJoin().On(left => left.ContentTypeId, right => right.NodeId) - .LeftJoin().On(left => left.PropertyTypeId, right => right.Id) - .LeftJoin().On(left => left.NodeId, right => right.DataTypeId) - .LeftJoin().On(left => left.ContentTypeNodeId, right => right.NodeId) .Where(x => x.NodeObjectType == NodeObjectTypeId); - return sql; } - protected override string GetBaseWhereClause() - { - return $"{Constants.DatabaseSchema.Tables.Node}.id = @id"; - } - - protected override IEnumerable GetDeleteClauses() - { - var l = (List) base.GetDeleteClauses(); // we know it's a list - l.Add("DELETE FROM cmsMemberType WHERE NodeId = @id"); - l.Add("DELETE FROM cmsContentType WHERE nodeId = @id"); - l.Add("DELETE FROM umbracoNode WHERE id = @id"); - return l; - } - - protected override Guid NodeObjectTypeId => Cms.Core.Constants.ObjectTypes.MemberType; - - protected override void PersistNewItem(IMemberType entity) - { - ValidateAlias(entity); + Sql sql = Sql() + .Select(x => x.NodeId) + .From() + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .LeftJoin().On(left => left.ContentTypeId, right => right.NodeId) + .LeftJoin() + .On(left => left.PropertyTypeId, right => right.Id) + .LeftJoin().On(left => left.NodeId, right => right.DataTypeId) + .LeftJoin() + .On(left => left.ContentTypeNodeId, right => right.NodeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId); + + return sql; + } - entity.AddingEntity(); + protected Sql GetSubquery() + { + Sql sql = Sql() + .Select("DISTINCT(umbracoNode.id)") + .From() + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .LeftJoin().On(left => left.ContentTypeId, right => right.NodeId) + .LeftJoin() + .On(left => left.PropertyTypeId, right => right.Id) + .LeftJoin().On(left => left.NodeId, right => right.DataTypeId) + .LeftJoin() + .On(left => left.ContentTypeNodeId, right => right.NodeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId); + return sql; + } - //set a default icon if one is not specified - if (entity.Icon.IsNullOrWhiteSpace()) - { - entity.Icon = Cms.Core.Constants.Icons.Member; - } + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Node}.id = @id"; - //By Convention we add 9 standard PropertyTypes to an Umbraco MemberType - var standardPropertyTypes = ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper); - foreach (var standardPropertyType in standardPropertyTypes) - { - entity.AddPropertyType(standardPropertyType.Value, Cms.Core.Constants.Conventions.Member.StandardPropertiesGroupAlias, Cms.Core.Constants.Conventions.Member.StandardPropertiesGroupName); - } + protected override IEnumerable GetDeleteClauses() + { + var l = (List)base.GetDeleteClauses(); // we know it's a list + l.Add("DELETE FROM cmsMemberType WHERE NodeId = @id"); + l.Add("DELETE FROM cmsContentType WHERE nodeId = @id"); + l.Add("DELETE FROM umbracoNode WHERE id = @id"); + return l; + } - EnsureExplicitDataTypeForBuiltInProperties(entity); - PersistNewBaseContentType(entity); + protected override void PersistNewItem(IMemberType entity) + { + ValidateAlias(entity); - //Handles the MemberTypeDto (cmsMemberType table) - var memberTypeDtos = ContentTypeFactory.BuildMemberPropertyTypeDtos(entity); - foreach (var memberTypeDto in memberTypeDtos) - { - Database.Insert(memberTypeDto); - } + entity.AddingEntity(); - entity.ResetDirtyProperties(); + //set a default icon if one is not specified + if (entity.Icon.IsNullOrWhiteSpace()) + { + entity.Icon = Constants.Icons.Member; } - protected override void PersistUpdatedItem(IMemberType entity) + //By Convention we add 9 standard PropertyTypes to an Umbraco MemberType + Dictionary standardPropertyTypes = + ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper); + foreach (KeyValuePair standardPropertyType in standardPropertyTypes) { - ValidateAlias(entity); + entity.AddPropertyType(standardPropertyType.Value, + Constants.Conventions.Member.StandardPropertiesGroupAlias, + Constants.Conventions.Member.StandardPropertiesGroupName); + } - //Updates Modified date - entity.UpdatingEntity(); + EnsureExplicitDataTypeForBuiltInProperties(entity); + PersistNewBaseContentType(entity); - //Look up parent to get and set the correct Path if ParentId has changed - if (entity.IsPropertyDirty("ParentId")) - { - var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); - entity.Path = string.Concat(parent.Path, ",", entity.Id); - entity.Level = parent.Level + 1; - var maxSortOrder = - Database.ExecuteScalar( - "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", - new { ParentId = entity.ParentId, NodeObjectType = NodeObjectTypeId }); - entity.SortOrder = maxSortOrder + 1; - } + //Handles the MemberTypeDto (cmsMemberType table) + IEnumerable memberTypeDtos = ContentTypeFactory.BuildMemberPropertyTypeDtos(entity); + foreach (MemberPropertyTypeDto memberTypeDto in memberTypeDtos) + { + Database.Insert(memberTypeDto); + } + + entity.ResetDirtyProperties(); + } - EnsureExplicitDataTypeForBuiltInProperties(entity); - PersistUpdatedBaseContentType(entity); + protected override void PersistUpdatedItem(IMemberType entity) + { + ValidateAlias(entity); - // remove and insert - handle cmsMemberType table - Database.Delete("WHERE NodeId = @Id", new { Id = entity.Id }); - var memberTypeDtos = ContentTypeFactory.BuildMemberPropertyTypeDtos(entity); - foreach (var memberTypeDto in memberTypeDtos) - { - Database.Insert(memberTypeDto); - } + //Updates Modified date + entity.UpdatingEntity(); - entity.ResetDirtyProperties(); + //Look up parent to get and set the correct Path if ParentId has changed + if (entity.IsPropertyDirty("ParentId")) + { + NodeDto? parent = Database.First("WHERE id = @ParentId", new {entity.ParentId}); + entity.Path = string.Concat(parent.Path, ",", entity.Id); + entity.Level = parent.Level + 1; + var maxSortOrder = + Database.ExecuteScalar( + "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", + new {entity.ParentId, NodeObjectType = NodeObjectTypeId}); + entity.SortOrder = maxSortOrder + 1; } - /// - /// Override so we can specify explicit db type's on any property types that are built-in. - /// - /// - /// - /// - /// - protected override PropertyType CreatePropertyType(string propertyEditorAlias, ValueStorageType storageType, string propertyTypeAlias) + EnsureExplicitDataTypeForBuiltInProperties(entity); + PersistUpdatedBaseContentType(entity); + + // remove and insert - handle cmsMemberType table + Database.Delete("WHERE NodeId = @Id", new {entity.Id}); + IEnumerable memberTypeDtos = ContentTypeFactory.BuildMemberPropertyTypeDtos(entity); + foreach (MemberPropertyTypeDto memberTypeDto in memberTypeDtos) { - //custom property type constructor logic to set explicit dbtype's for built in properties - var builtinProperties = ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper); - var readonlyStorageType = builtinProperties.TryGetValue(propertyTypeAlias, out var propertyType); - storageType = readonlyStorageType ? propertyType!.ValueStorageType : storageType; - return new PropertyType(_shortStringHelper, propertyEditorAlias, storageType, readonlyStorageType, propertyTypeAlias); + Database.Insert(memberTypeDto); } - /// - /// Ensure that all the built-in membership provider properties have their correct data type - /// and property editors assigned. This occurs prior to saving so that the correct values are persisted. - /// - /// - private void EnsureExplicitDataTypeForBuiltInProperties(IContentTypeBase memberType) + entity.ResetDirtyProperties(); + } + + /// + /// Override so we can specify explicit db type's on any property types that are built-in. + /// + /// + /// + /// + /// + protected override PropertyType CreatePropertyType(string propertyEditorAlias, ValueStorageType storageType, + string propertyTypeAlias) + { + //custom property type constructor logic to set explicit dbtype's for built in properties + Dictionary builtinProperties = + ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper); + var readonlyStorageType = builtinProperties.TryGetValue(propertyTypeAlias, out PropertyType? propertyType); + storageType = readonlyStorageType ? propertyType!.ValueStorageType : storageType; + return new PropertyType(_shortStringHelper, propertyEditorAlias, storageType, readonlyStorageType, + propertyTypeAlias); + } + + /// + /// Ensure that all the built-in membership provider properties have their correct data type + /// and property editors assigned. This occurs prior to saving so that the correct values are persisted. + /// + /// + private void EnsureExplicitDataTypeForBuiltInProperties(IContentTypeBase memberType) + { + Dictionary builtinProperties = + ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper); + foreach (IPropertyType propertyType in memberType.PropertyTypes) { - var builtinProperties = ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper); - foreach (var propertyType in memberType.PropertyTypes) + if (builtinProperties.ContainsKey(propertyType.Alias)) { - if (builtinProperties.ContainsKey(propertyType.Alias)) + //this reset's its current data type reference which will be re-assigned based on the property editor assigned on the next line + if (builtinProperties.TryGetValue(propertyType.Alias, out PropertyType? propDefinition) && + propDefinition != null) + { + propertyType.DataTypeId = propDefinition.DataTypeId; + propertyType.DataTypeKey = propDefinition.DataTypeKey; + } + else { - //this reset's its current data type reference which will be re-assigned based on the property editor assigned on the next line - if (builtinProperties.TryGetValue(propertyType.Alias, out var propDefinition) && propDefinition != null) - { - propertyType.DataTypeId = propDefinition.DataTypeId; - propertyType.DataTypeKey = propDefinition.DataTypeKey; - } - else - { - propertyType.DataTypeId = 0; - propertyType.DataTypeKey = default; - } + propertyType.DataTypeId = 0; + propertyType.DataTypeKey = default; } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NodeCountRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NodeCountRepository.cs index 7c910d748537..a70f5c2f6a6e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NodeCountRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NodeCountRepository.cs @@ -1,4 +1,4 @@ -using System; +using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Infrastructure.Persistence.Dtos; @@ -13,22 +13,20 @@ public class NodeCountRepository : INodeCountRepository public NodeCountRepository(IScopeAccessor scopeAccessor) => _scopeAccessor = scopeAccessor; - /// - + /// public int GetNodeCount(Guid nodeType) { - var query = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() + Sql? query = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() .SelectCount() .From() .Where(x => x.NodeObjectType == nodeType && x.Trashed == false); return _scopeAccessor.AmbientScope?.Database.ExecuteScalar(query) ?? 0; - } public int GetMediaCount() { - var query = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() + Sql? query = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() .SelectCount() .From() .InnerJoin() diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NotificationsRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NotificationsRepository.cs index be42f7b74f35..f45048baa306 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NotificationsRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NotificationsRepository.cs @@ -1,116 +1,107 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using NPoco; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +public class NotificationsRepository : INotificationsRepository { - public class NotificationsRepository : INotificationsRepository - { - private readonly IScopeAccessor _scopeAccessor; + private readonly IScopeAccessor _scopeAccessor; - public NotificationsRepository(IScopeAccessor scopeAccessor) - { - _scopeAccessor = scopeAccessor; - } + public NotificationsRepository(IScopeAccessor scopeAccessor) => _scopeAccessor = scopeAccessor; - private Scoping.IScope? AmbientScope => _scopeAccessor.AmbientScope; + private IScope? AmbientScope => _scopeAccessor.AmbientScope; - public IEnumerable? GetUsersNotifications(IEnumerable userIds, string? action, IEnumerable nodeIds, Guid objectType) + public IEnumerable? GetUsersNotifications(IEnumerable userIds, string? action, + IEnumerable nodeIds, Guid objectType) + { + var nodeIdsA = nodeIds.ToArray(); + Sql? sql = AmbientScope?.SqlContext.Sql() + .Select( + "DISTINCT umbracoNode.id nodeId, umbracoUser.id userId, umbracoNode.nodeObjectType, umbracoUser2NodeNotify.action") + .From() + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .InnerJoin().On(left => left.UserId, right => right.Id) + .Where(x => x.NodeObjectType == objectType) + .Where(x => x.Disabled == false) // only approved users + .Where(x => x.Action == action); // on the specified action + if (nodeIdsA.Length > 0) { - var nodeIdsA = nodeIds.ToArray(); - var sql = AmbientScope?.SqlContext.Sql() - .Select("DISTINCT umbracoNode.id nodeId, umbracoUser.id userId, umbracoNode.nodeObjectType, umbracoUser2NodeNotify.action") - .From() - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .InnerJoin().On(left => left.UserId, right => right.Id) - .Where(x => x.NodeObjectType == objectType) - .Where(x => x.Disabled == false) // only approved users - .Where(x => x.Action == action); // on the specified action - if (nodeIdsA.Length > 0) - sql? - .WhereIn(x => x.NodeId, nodeIdsA); // for the specified nodes sql? - .OrderBy(x => x.Id) - .OrderBy(dto => dto.NodeId); - return AmbientScope?.Database.Fetch(sql).Select(x => new Notification(x.NodeId, x.UserId, x.Action, objectType)); + .WhereIn(x => x.NodeId, nodeIdsA); // for the specified nodes } - public IEnumerable? GetUserNotifications(IUser user) - { - var sql = AmbientScope?.SqlContext.Sql() - .Select("DISTINCT umbracoNode.id AS nodeId, umbracoUser2NodeNotify.userId, umbracoNode.nodeObjectType, umbracoUser2NodeNotify.action") - .From() - .InnerJoin() - .On(dto => dto.NodeId, dto => dto.NodeId) - .Where(dto => dto.UserId == (int)user.Id) - .OrderBy(dto => dto.NodeId); - - var dtos = AmbientScope?.Database.Fetch(sql); - //need to map the results - return dtos?.Select(d => new Notification(d.NodeId, d.UserId, d.Action, d.NodeObjectType)).ToList(); - } + sql? + .OrderBy(x => x.Id) + .OrderBy(dto => dto.NodeId); + return AmbientScope?.Database.Fetch(sql) + .Select(x => new Notification(x.NodeId, x.UserId, x.Action, objectType)); + } - public IEnumerable SetNotifications(IUser user, IEntity entity, string[] actions) - { - DeleteNotifications(user, entity); - return actions.Select(action => CreateNotification(user, entity, action)).ToList(); - } + public IEnumerable? GetUserNotifications(IUser user) + { + Sql? sql = AmbientScope?.SqlContext.Sql() + .Select( + "DISTINCT umbracoNode.id AS nodeId, umbracoUser2NodeNotify.userId, umbracoNode.nodeObjectType, umbracoUser2NodeNotify.action") + .From() + .InnerJoin() + .On(dto => dto.NodeId, dto => dto.NodeId) + .Where(dto => dto.UserId == user.Id) + .OrderBy(dto => dto.NodeId); - public IEnumerable? GetEntityNotifications(IEntity entity) - { - var sql = AmbientScope?.SqlContext.Sql() - .Select("DISTINCT umbracoNode.id as nodeId, umbracoUser2NodeNotify.userId, umbracoNode.nodeObjectType, umbracoUser2NodeNotify.action") - .From() - .InnerJoin() - .On(dto => dto.NodeId, dto => dto.NodeId) - .Where(dto => dto.NodeId == entity.Id) - .OrderBy(dto => dto.NodeId); - - var dtos = AmbientScope?.Database.Fetch(sql); - //need to map the results - return dtos?.Select(d => new Notification(d.NodeId, d.UserId, d.Action, d.NodeObjectType)).ToList(); - } + List? dtos = AmbientScope?.Database.Fetch(sql); + //need to map the results + return dtos?.Select(d => new Notification(d.NodeId, d.UserId, d.Action, d.NodeObjectType)).ToList(); + } - public int DeleteNotifications(IEntity entity) - { - return AmbientScope?.Database.Delete("WHERE nodeId = @nodeId", new { nodeId = entity.Id }) ?? 0; - } + public IEnumerable SetNotifications(IUser user, IEntity entity, string[] actions) + { + DeleteNotifications(user, entity); + return actions.Select(action => CreateNotification(user, entity, action)).ToList(); + } - public int DeleteNotifications(IUser user) - { - return AmbientScope?.Database.Delete("WHERE userId = @userId", new { userId = user.Id }) ?? 0; - } + public IEnumerable? GetEntityNotifications(IEntity entity) + { + Sql? sql = AmbientScope?.SqlContext.Sql() + .Select( + "DISTINCT umbracoNode.id as nodeId, umbracoUser2NodeNotify.userId, umbracoNode.nodeObjectType, umbracoUser2NodeNotify.action") + .From() + .InnerJoin() + .On(dto => dto.NodeId, dto => dto.NodeId) + .Where(dto => dto.NodeId == entity.Id) + .OrderBy(dto => dto.NodeId); - public int DeleteNotifications(IUser user, IEntity entity) - { - // delete all settings on the node for this user - return AmbientScope?.Database.Delete("WHERE userId = @userId AND nodeId = @nodeId", new { userId = user.Id, nodeId = entity.Id }) ?? 0; - } + List? dtos = AmbientScope?.Database.Fetch(sql); + //need to map the results + return dtos?.Select(d => new Notification(d.NodeId, d.UserId, d.Action, d.NodeObjectType)).ToList(); + } - public Notification CreateNotification(IUser user, IEntity entity, string action) - { - var sql = AmbientScope?.SqlContext.Sql() - .Select("DISTINCT nodeObjectType") - .From() - .Where(nodeDto => nodeDto.NodeId == entity.Id); - var nodeType = AmbientScope?.Database.ExecuteScalar(sql); - - var dto = new User2NodeNotifyDto - { - Action = action, - NodeId = entity.Id, - UserId = user.Id - }; - AmbientScope?.Database.Insert(dto); - return new Notification(dto.NodeId, dto.UserId, dto.Action, nodeType); - } + public int DeleteNotifications(IEntity entity) => + AmbientScope?.Database.Delete("WHERE nodeId = @nodeId", new {nodeId = entity.Id}) ?? 0; + + public int DeleteNotifications(IUser user) => + AmbientScope?.Database.Delete("WHERE userId = @userId", new {userId = user.Id}) ?? 0; + + public int DeleteNotifications(IUser user, IEntity entity) => + // delete all settings on the node for this user + AmbientScope?.Database.Delete("WHERE userId = @userId AND nodeId = @nodeId", + new {userId = user.Id, nodeId = entity.Id}) ?? 0; + + public Notification CreateNotification(IUser user, IEntity entity, string action) + { + Sql? sql = AmbientScope?.SqlContext.Sql() + .Select("DISTINCT nodeObjectType") + .From() + .Where(nodeDto => nodeDto.NodeId == entity.Id); + Guid? nodeType = AmbientScope?.Database.ExecuteScalar(sql); + + var dto = new User2NodeNotifyDto {Action = action, NodeId = entity.Id, UserId = user.Id}; + AmbientScope?.Database.Insert(dto); + return new Notification(dto.NodeId, dto.UserId, dto.Action, nodeType); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PartialViewMacroRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PartialViewMacroRepository.cs index b4c8ce4f6c7e..d8c5ccd5ce44 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PartialViewMacroRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PartialViewMacroRepository.cs @@ -2,14 +2,14 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class PartialViewMacroRepository : PartialViewRepository, IPartialViewMacroRepository { - internal class PartialViewMacroRepository : PartialViewRepository, IPartialViewMacroRepository + public PartialViewMacroRepository(FileSystems fileSystems) + : base(fileSystems.MacroPartialsFileSystem) { - public PartialViewMacroRepository(FileSystems fileSystems) - : base(fileSystems.MacroPartialsFileSystem) - { } - - protected override PartialViewType ViewType => PartialViewType.PartialViewMacro; } + + protected override PartialViewType ViewType => PartialViewType.PartialViewMacro; } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PartialViewRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PartialViewRepository.cs index 9fbd5af5cd12..e9fedf868d07 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PartialViewRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PartialViewRepository.cs @@ -1,141 +1,141 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Text; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class PartialViewRepository : FileRepository, IPartialViewRepository { - internal class PartialViewRepository : FileRepository, IPartialViewRepository + public PartialViewRepository(FileSystems fileSystems) + : base(fileSystems.PartialViewsFileSystem) { - public PartialViewRepository(FileSystems fileSystems) - : base(fileSystems.PartialViewsFileSystem) - { - } + } + + protected PartialViewRepository(IFileSystem? fileSystem) + : base(fileSystem) + { + } + + protected virtual PartialViewType ViewType => PartialViewType.PartialView; - protected PartialViewRepository(IFileSystem? fileSystem) - : base(fileSystem) + public override IPartialView? Get(string? id) + { + if (FileSystem is null) { + return null; } - protected virtual PartialViewType ViewType => PartialViewType.PartialView; + // get the relative path within the filesystem + // (though... id should be relative already) + var path = FileSystem.GetRelativePath(id!); - public override IPartialView? Get(string? id) + if (FileSystem.FileExists(path) == false) { - if (FileSystem is null) - { - return null; - } - // get the relative path within the filesystem - // (though... id should be relative already) - var path = FileSystem.GetRelativePath(id!); - - if (FileSystem.FileExists(path) == false) - return null; + return null; + } - // content will be lazy-loaded when required - var created = FileSystem.GetCreated(path).UtcDateTime; - var updated = FileSystem.GetLastModified(path).UtcDateTime; - //var content = GetFileContent(path); + // content will be lazy-loaded when required + DateTime created = FileSystem.GetCreated(path).UtcDateTime; + DateTime updated = FileSystem.GetLastModified(path).UtcDateTime; + //var content = GetFileContent(path); - var view = new PartialView(ViewType, path, file => GetFileContent(file.OriginalPath)) - { - //id can be the hash - Id = path.GetHashCode(), - Key = path.EncodeAsGuid(), - //Content = content, - CreateDate = created, - UpdateDate = updated, - VirtualPath = FileSystem.GetUrl(id) - }; - - // reset dirty initial properties (U4-1946) - view.ResetDirtyProperties(false); - - return view; - } + var view = new PartialView(ViewType, path, file => GetFileContent(file.OriginalPath)) + { + //id can be the hash + Id = path.GetHashCode(), + Key = path.EncodeAsGuid(), + //Content = content, + CreateDate = created, + UpdateDate = updated, + VirtualPath = FileSystem.GetUrl(id) + }; + + // reset dirty initial properties (U4-1946) + view.ResetDirtyProperties(false); + + return view; + } - public override void Save(IPartialView entity) + public override void Save(IPartialView entity) + { + var partialView = entity as PartialView; + if (partialView != null) { - var partialView = entity as PartialView; - if (partialView != null) - partialView.ViewType = ViewType; + partialView.ViewType = ViewType; + } - base.Save(entity); + base.Save(entity); - // ensure that from now on, content is lazy-loaded - if (partialView != null && partialView.GetFileContent == null) - partialView.GetFileContent = file => GetFileContent(file.OriginalPath); + // ensure that from now on, content is lazy-loaded + if (partialView != null && partialView.GetFileContent == null) + { + partialView.GetFileContent = file => GetFileContent(file.OriginalPath); } + } - public override IEnumerable GetMany(params string[]? ids) - { - //ensure they are de-duplicated, easy win if people don't do this as this can cause many excess queries - ids = ids?.Distinct().ToArray(); + public override IEnumerable GetMany(params string[]? ids) + { + //ensure they are de-duplicated, easy win if people don't do this as this can cause many excess queries + ids = ids?.Distinct().ToArray(); - if (ids?.Any() ?? false) + if (ids?.Any() ?? false) + { + foreach (var id in ids) { - foreach (var id in ids) + IPartialView? partialView = Get(id); + if (partialView is not null) { - var partialView = Get(id); - if (partialView is not null) - { - yield return partialView; - } + yield return partialView; } } - else + } + else + { + IEnumerable files = FindAllFiles("", "*.*"); + foreach (var file in files) { - var files = FindAllFiles("", "*.*"); - foreach (var file in files) + IPartialView? partialView = Get(file); + if (partialView is not null) { - var partialView = Get(file); - if (partialView is not null) - { - yield return partialView; - } + yield return partialView; } } } + } - public Stream GetFileContentStream(string filepath) + public Stream GetFileContentStream(string filepath) + { + if (FileSystem?.FileExists(filepath) == false) { - if (FileSystem?.FileExists(filepath) == false) - { - return Stream.Null; - } - - try - { - return FileSystem?.OpenFile(filepath) ?? Stream.Null; - } - catch - { - return Stream.Null; // deal with race conds - } + return Stream.Null; } - public void SetFileContent(string filepath, Stream content) + try { - FileSystem?.AddFile(filepath, content, true); + return FileSystem?.OpenFile(filepath) ?? Stream.Null; } - - /// - /// Gets a stream that is used to write to the file - /// - /// - /// - /// - /// This ensures the stream includes a utf8 BOM - /// - protected override Stream GetContentStream(string content) + catch { - var data = Encoding.UTF8.GetBytes(content); - var withBom = Encoding.UTF8.GetPreamble().Concat(data).ToArray(); - return new MemoryStream(withBom); + return Stream.Null; // deal with race conds } } + + public void SetFileContent(string filepath, Stream content) => FileSystem?.AddFile(filepath, content, true); + + /// + /// Gets a stream that is used to write to the file + /// + /// + /// + /// + /// This ensures the stream includes a utf8 BOM + /// + protected override Stream GetContentStream(string content) + { + var data = Encoding.UTF8.GetBytes(content); + var withBom = Encoding.UTF8.GetPreamble().Concat(data).ToArray(); + return new MemoryStream(withBom); + } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PermissionRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PermissionRepository.cs index 9919707e8a1b..b2bcfc47cf9f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PermissionRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PermissionRepository.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -13,350 +10,346 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// A (sub) repository that exposes functionality to modify assigned permissions to a node +/// +/// +/// +/// This repo implements the base class so that permissions can be +/// queued to be persisted +/// like the normal repository pattern but the standard repository Get commands don't apply and will throw +/// +/// +internal class PermissionRepository : EntityRepositoryBase + where TEntity : class, IEntity { + public PermissionRepository(IScopeAccessor scopeAccessor, AppCaches cache, + ILogger> logger) + : base(scopeAccessor, cache, logger) + { + } + /// - /// A (sub) repository that exposes functionality to modify assigned permissions to a node + /// Returns explicitly defined permissions for a user group for any number of nodes /// - /// + /// + /// The group ids to lookup permissions for + /// + /// + /// /// - /// This repo implements the base class so that permissions can be - /// queued to be persisted - /// like the normal repository pattern but the standard repository Get commands don't apply and will throw - /// + /// This method will not support passing in more than 2000 group IDs when also passing in entity IDs. /// - internal class PermissionRepository : EntityRepositoryBase - where TEntity : class, IEntity + public EntityPermissionCollection GetPermissionsForEntities(int[] groupIds, params int[] entityIds) { - public PermissionRepository(IScopeAccessor scopeAccessor, AppCaches cache, - ILogger> logger) - : base(scopeAccessor, cache, logger) - { - } + var result = new EntityPermissionCollection(); - /// - /// Returns explicitly defined permissions for a user group for any number of nodes - /// - /// - /// The group ids to lookup permissions for - /// - /// - /// - /// - /// This method will not support passing in more than 2000 group IDs when also passing in entity IDs. - /// - public EntityPermissionCollection GetPermissionsForEntities(int[] groupIds, params int[] entityIds) + if (entityIds.Length == 0) { - var result = new EntityPermissionCollection(); - - if (entityIds.Length == 0) + foreach (IEnumerable group in groupIds.InGroupsOf(Constants.Sql.MaxParameterCount)) { - foreach (IEnumerable group in groupIds.InGroupsOf(Constants.Sql.MaxParameterCount)) + Sql sql = Sql() + .SelectAll() + .From() + .LeftJoin().On( + (left, right) => left.NodeId == right.NodeId && left.UserGroupId == right.UserGroupId) + .Where(dto => group.Contains(dto.UserGroupId)); + + List permissions = + AmbientScope.Database.Fetch(sql); + foreach (EntityPermission permission in ConvertToPermissionList(permissions)) { - Sql sql = Sql() - .SelectAll() - .From() - .LeftJoin().On( - (left, right) => left.NodeId == right.NodeId && left.UserGroupId == right.UserGroupId) - .Where(dto => group.Contains(dto.UserGroupId)); - - List permissions = - AmbientScope.Database.Fetch(sql); - foreach (EntityPermission permission in ConvertToPermissionList(permissions)) - { - result.Add(permission); - } + result.Add(permission); } } - else + } + else + { + foreach (IEnumerable group in entityIds.InGroupsOf(Constants.Sql.MaxParameterCount - + groupIds.Length)) { - foreach (IEnumerable group in entityIds.InGroupsOf(Constants.Sql.MaxParameterCount - - groupIds.Length)) + Sql sql = Sql() + .SelectAll() + .From() + .LeftJoin().On( + (left, right) => left.NodeId == right.NodeId && left.UserGroupId == right.UserGroupId) + .Where(dto => + groupIds.Contains(dto.UserGroupId) && group.Contains(dto.NodeId)); + + List permissions = + AmbientScope.Database.Fetch(sql); + foreach (EntityPermission permission in ConvertToPermissionList(permissions)) { - Sql sql = Sql() - .SelectAll() - .From() - .LeftJoin().On( - (left, right) => left.NodeId == right.NodeId && left.UserGroupId == right.UserGroupId) - .Where(dto => - groupIds.Contains(dto.UserGroupId) && group.Contains(dto.NodeId)); - - List permissions = - AmbientScope.Database.Fetch(sql); - foreach (EntityPermission permission in ConvertToPermissionList(permissions)) - { - result.Add(permission); - } + result.Add(permission); } } - - return result; } - /// - /// Returns permissions directly assigned to the content items for all user groups - /// - /// - /// - public IEnumerable GetPermissionsForEntities(int[] entityIds) - { - Sql sql = Sql() - .SelectAll() - .From() - .LeftJoin() - .On((left, right) => - left.NodeId == right.NodeId && left.UserGroupId == right.UserGroupId) - .Where(dto => entityIds.Contains(dto.NodeId)) - .OrderBy(dto => dto.NodeId); - - List result = AmbientScope.Database.Fetch(sql); - return ConvertToPermissionList(result); - } + return result; + } - /// - /// Returns permissions directly assigned to the content item for all user groups - /// - /// - /// - public EntityPermissionCollection GetPermissionsForEntity(int entityId) - { - Sql sql = Sql() - .SelectAll() - .From() - .LeftJoin() - .On((left, right) => - left.NodeId == right.NodeId && left.UserGroupId == right.UserGroupId) - .Where(dto => dto.NodeId == entityId) - .OrderBy(dto => dto.NodeId); - - List result = AmbientScope.Database.Fetch(sql); - return ConvertToPermissionList(result); - } + /// + /// Returns permissions directly assigned to the content items for all user groups + /// + /// + /// + public IEnumerable GetPermissionsForEntities(int[] entityIds) + { + Sql sql = Sql() + .SelectAll() + .From() + .LeftJoin() + .On((left, right) => + left.NodeId == right.NodeId && left.UserGroupId == right.UserGroupId) + .Where(dto => entityIds.Contains(dto.NodeId)) + .OrderBy(dto => dto.NodeId); + + List result = AmbientScope.Database.Fetch(sql); + return ConvertToPermissionList(result); + } - /// - /// Assigns the same permission set for a single group to any number of entities - /// - /// - /// The permissions to assign or null to remove the connection between group and entityIds - /// - /// - /// This will first clear the permissions for this user and entities and recreate them - /// - public void ReplacePermissions(int groupId, IEnumerable? permissions, params int[] entityIds) + /// + /// Returns permissions directly assigned to the content item for all user groups + /// + /// + /// + public EntityPermissionCollection GetPermissionsForEntity(int entityId) + { + Sql sql = Sql() + .SelectAll() + .From() + .LeftJoin() + .On((left, right) => + left.NodeId == right.NodeId && left.UserGroupId == right.UserGroupId) + .Where(dto => dto.NodeId == entityId) + .OrderBy(dto => dto.NodeId); + + List result = AmbientScope.Database.Fetch(sql); + return ConvertToPermissionList(result); + } + + /// + /// Assigns the same permission set for a single group to any number of entities + /// + /// + /// The permissions to assign or null to remove the connection between group and entityIds + /// + /// + /// This will first clear the permissions for this user and entities and recreate them + /// + public void ReplacePermissions(int groupId, IEnumerable? permissions, params int[] entityIds) + { + if (entityIds.Length == 0) { - if (entityIds.Length == 0) - { - return; - } + return; + } - IUmbracoDatabase db = AmbientScope.Database; + IUmbracoDatabase db = AmbientScope.Database; - foreach (IEnumerable group in entityIds.InGroupsOf(Constants.Sql.MaxParameterCount)) - { + foreach (IEnumerable group in entityIds.InGroupsOf(Constants.Sql.MaxParameterCount)) + { + db.Execute("DELETE FROM umbracoUserGroup2Node WHERE userGroupId = @groupId AND nodeId in (@nodeIds)", + new {groupId, nodeIds = group}); - db.Execute("DELETE FROM umbracoUserGroup2Node WHERE userGroupId = @groupId AND nodeId in (@nodeIds)", - new { groupId, nodeIds = group }); + db.Execute( + "DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @groupId AND nodeId in (@nodeIds)", + new {groupId, nodeIds = group}); + } - db.Execute("DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @groupId AND nodeId in (@nodeIds)", - new { groupId, nodeIds = group }); - } + if (permissions is not null) + { + var toInsert = new List(); + var toInsertPermissions = new List(); - if (permissions is not null) + foreach (var e in entityIds) { - var toInsert = new List(); - var toInsertPermissions = new List(); - - foreach (var e in entityIds) + toInsert.Add(new UserGroup2NodeDto {NodeId = e, UserGroupId = groupId}); + foreach (var p in permissions) { - toInsert.Add(new UserGroup2NodeDto() { NodeId = e, UserGroupId = groupId }); - foreach (var p in permissions) + toInsertPermissions.Add(new UserGroup2NodePermissionDto { - toInsertPermissions.Add(new UserGroup2NodePermissionDto - { - NodeId = e, Permission = p.ToString(CultureInfo.InvariantCulture), UserGroupId = groupId - }); - } + NodeId = e, Permission = p.ToString(CultureInfo.InvariantCulture), UserGroupId = groupId + }); } - - db.BulkInsertRecords(toInsert); - db.BulkInsertRecords(toInsertPermissions); } - } - /// - /// Assigns one permission for a user to many entities - /// - /// - /// - /// - public void AssignPermission(int groupId, char permission, params int[] entityIds) - { - IUmbracoDatabase db = AmbientScope.Database; - - db.Execute("DELETE FROM umbracoUserGroup2Node WHERE userGroupId = @groupId AND nodeId in (@entityIds)", - new { groupId, entityIds }); - db.Execute("DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @groupId AND permission=@permission AND nodeId in (@entityIds)", - new { groupId, permission = permission.ToString(CultureInfo.InvariantCulture), entityIds }); + db.BulkInsertRecords(toInsert); + db.BulkInsertRecords(toInsertPermissions); + } + } - UserGroup2NodeDto[] actionsPermissions = entityIds.Select(id => new UserGroup2NodeDto - { - NodeId = id, UserGroupId = groupId - }).ToArray(); + /// + /// Assigns one permission for a user to many entities + /// + /// + /// + /// + public void AssignPermission(int groupId, char permission, params int[] entityIds) + { + IUmbracoDatabase db = AmbientScope.Database; - UserGroup2NodePermissionDto[] actions = entityIds.Select(id => new UserGroup2NodePermissionDto - { - NodeId = id, Permission = permission.ToString(CultureInfo.InvariantCulture), UserGroupId = groupId - }).ToArray(); + db.Execute("DELETE FROM umbracoUserGroup2Node WHERE userGroupId = @groupId AND nodeId in (@entityIds)", + new {groupId, entityIds}); + db.Execute( + "DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @groupId AND permission=@permission AND nodeId in (@entityIds)", + new {groupId, permission = permission.ToString(CultureInfo.InvariantCulture), entityIds}); - db.BulkInsertRecords(actions); - db.BulkInsertRecords(actionsPermissions); - } + UserGroup2NodeDto[] actionsPermissions = + entityIds.Select(id => new UserGroup2NodeDto {NodeId = id, UserGroupId = groupId}).ToArray(); - /// - /// Assigns one permission to an entity for multiple groups - /// - /// - /// - /// - public void AssignEntityPermission(TEntity entity, char permission, IEnumerable groupIds) + UserGroup2NodePermissionDto[] actions = entityIds.Select(id => new UserGroup2NodePermissionDto { - IUmbracoDatabase db = AmbientScope.Database; - var groupIdsA = groupIds.ToArray(); - - db.Execute("DELETE FROM umbracoUserGroup2Node WHERE nodeId = @nodeId AND userGroupId in (@groupIds)", - new { - nodeId = entity.Id, - groupIds = groupIdsA - }); - db.Execute("DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @nodeId AND permission = @permission AND userGroupId in (@groupIds)", - new - { - nodeId = entity.Id, - permission = permission.ToString(CultureInfo.InvariantCulture), - groupIds = groupIdsA - }); + NodeId = id, Permission = permission.ToString(CultureInfo.InvariantCulture), UserGroupId = groupId + }).ToArray(); - UserGroup2NodePermissionDto[] actionsPermissions = groupIdsA.Select(id => new UserGroup2NodePermissionDto - { - NodeId = entity.Id, Permission = permission.ToString(CultureInfo.InvariantCulture), UserGroupId = id - }).ToArray(); + db.BulkInsertRecords(actions); + db.BulkInsertRecords(actionsPermissions); + } - UserGroup2NodeDto[] actions = groupIdsA.Select(id => new UserGroup2NodeDto + /// + /// Assigns one permission to an entity for multiple groups + /// + /// + /// + /// + public void AssignEntityPermission(TEntity entity, char permission, IEnumerable groupIds) + { + IUmbracoDatabase db = AmbientScope.Database; + var groupIdsA = groupIds.ToArray(); + + db.Execute("DELETE FROM umbracoUserGroup2Node WHERE nodeId = @nodeId AND userGroupId in (@groupIds)", + new {nodeId = entity.Id, groupIds = groupIdsA}); + db.Execute( + "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @nodeId AND permission = @permission AND userGroupId in (@groupIds)", + new { - NodeId = entity.Id, UserGroupId = id - }).ToArray(); + nodeId = entity.Id, + permission = permission.ToString(CultureInfo.InvariantCulture), + groupIds = groupIdsA + }); - db.BulkInsertRecords(actions); - db.BulkInsertRecords(actionsPermissions); - } + UserGroup2NodePermissionDto[] actionsPermissions = groupIdsA.Select(id => new UserGroup2NodePermissionDto + { + NodeId = entity.Id, Permission = permission.ToString(CultureInfo.InvariantCulture), UserGroupId = id + }).ToArray(); - /// - /// Assigns permissions to an entity for multiple group/permission entries - /// - /// - /// - /// - /// This will first clear the permissions for this entity then re-create them - /// - public void ReplaceEntityPermissions(EntityPermissionSet permissionSet) + UserGroup2NodeDto[] actions = groupIdsA.Select(id => new UserGroup2NodeDto { - IUmbracoDatabase db = AmbientScope.Database; + NodeId = entity.Id, UserGroupId = id + }).ToArray(); - db.Execute("DELETE FROM umbracoUserGroup2Node WHERE nodeId = @nodeId", new { nodeId = permissionSet.EntityId }); - db.Execute("DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @nodeId", new { nodeId = permissionSet.EntityId }); + db.BulkInsertRecords(actions); + db.BulkInsertRecords(actionsPermissions); + } - var toInsert = new List(); - var toInsertPermissions = new List(); - foreach (EntityPermission entityPermission in permissionSet.PermissionsSet) + /// + /// Assigns permissions to an entity for multiple group/permission entries + /// + /// + /// + /// + /// This will first clear the permissions for this entity then re-create them + /// + public void ReplaceEntityPermissions(EntityPermissionSet permissionSet) + { + IUmbracoDatabase db = AmbientScope.Database; + + db.Execute("DELETE FROM umbracoUserGroup2Node WHERE nodeId = @nodeId", new {nodeId = permissionSet.EntityId}); + db.Execute("DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @nodeId", + new {nodeId = permissionSet.EntityId}); + + var toInsert = new List(); + var toInsertPermissions = new List(); + foreach (EntityPermission entityPermission in permissionSet.PermissionsSet) + { + toInsert.Add(new UserGroup2NodeDto + { + NodeId = permissionSet.EntityId, UserGroupId = entityPermission.UserGroupId + }); + foreach (var permission in entityPermission.AssignedPermissions) { - toInsert.Add(new UserGroup2NodeDto + toInsertPermissions.Add(new UserGroup2NodePermissionDto { NodeId = permissionSet.EntityId, + Permission = permission, UserGroupId = entityPermission.UserGroupId }); - foreach (var permission in entityPermission.AssignedPermissions) - { - toInsertPermissions.Add(new UserGroup2NodePermissionDto - { - NodeId = permissionSet.EntityId, - Permission = permission, - UserGroupId = entityPermission.UserGroupId - }); - } } - - db.BulkInsertRecords(toInsert); - db.BulkInsertRecords(toInsertPermissions); } - /// - /// Used to add or update entity permissions during a content item being updated - /// - /// - protected override void PersistNewItem(ContentPermissionSet entity) => - //does the same thing as update - PersistUpdatedItem(entity); - - /// - /// Used to add or update entity permissions during a content item being updated - /// - /// - protected override void PersistUpdatedItem(ContentPermissionSet entity) - { - var asIEntity = (IEntity)entity; - if (asIEntity.HasIdentity == false) - { - throw new InvalidOperationException("Cannot create permissions for an entity without an Id"); - } + db.BulkInsertRecords(toInsert); + db.BulkInsertRecords(toInsertPermissions); + } + + /// + /// Used to add or update entity permissions during a content item being updated + /// + /// + protected override void PersistNewItem(ContentPermissionSet entity) => + //does the same thing as update + PersistUpdatedItem(entity); - ReplaceEntityPermissions(entity); + /// + /// Used to add or update entity permissions during a content item being updated + /// + /// + protected override void PersistUpdatedItem(ContentPermissionSet entity) + { + var asIEntity = (IEntity)entity; + if (asIEntity.HasIdentity == false) + { + throw new InvalidOperationException("Cannot create permissions for an entity without an Id"); } - private static EntityPermissionCollection ConvertToPermissionList( - IEnumerable result) + ReplaceEntityPermissions(entity); + } + + private static EntityPermissionCollection ConvertToPermissionList( + IEnumerable result) + { + var permissions = new EntityPermissionCollection(); + IEnumerable> nodePermissions = result.GroupBy(x => x.NodeId); + foreach (IGrouping np in nodePermissions) { - var permissions = new EntityPermissionCollection(); - IEnumerable> nodePermissions = result.GroupBy(x => x.NodeId); - foreach (IGrouping np in nodePermissions) + IEnumerable> userGroupPermissions = + np.GroupBy(x => x.UserGroupId); + foreach (IGrouping permission in userGroupPermissions) { - IEnumerable> userGroupPermissions = - np.GroupBy(x => x.UserGroupId); - foreach (IGrouping permission in userGroupPermissions) - { - var perms = permission.Select(x => x.Permission).Distinct().ToArray(); + var perms = permission.Select(x => x.Permission).Distinct().ToArray(); - // perms can contain null if there are no permissions assigned, but the node is chosen in the UI. - permissions.Add(new EntityPermission(permission.Key, np.Key, - perms.WhereNotNull().ToArray())); - } + // perms can contain null if there are no permissions assigned, but the node is chosen in the UI. + permissions.Add(new EntityPermission(permission.Key, np.Key, + perms.WhereNotNull().ToArray())); } - - return permissions; } - #region Not implemented (don't need to for the purposes of this repo) + return permissions; + } - protected override ContentPermissionSet PerformGet(int id) => - throw new InvalidOperationException("This method won't be implemented."); + #region Not implemented (don't need to for the purposes of this repo) - protected override IEnumerable PerformGetAll(params int[]? ids) => - throw new InvalidOperationException("This method won't be implemented."); + protected override ContentPermissionSet PerformGet(int id) => + throw new InvalidOperationException("This method won't be implemented."); - protected override IEnumerable PerformGetByQuery(IQuery query) => - throw new InvalidOperationException("This method won't be implemented."); + protected override IEnumerable PerformGetAll(params int[]? ids) => + throw new InvalidOperationException("This method won't be implemented."); - protected override Sql GetBaseQuery(bool isCount) => - throw new InvalidOperationException("This method won't be implemented."); + protected override IEnumerable PerformGetByQuery(IQuery query) => + throw new InvalidOperationException("This method won't be implemented."); - protected override string GetBaseWhereClause() => - throw new InvalidOperationException("This method won't be implemented."); + protected override Sql GetBaseQuery(bool isCount) => + throw new InvalidOperationException("This method won't be implemented."); - protected override IEnumerable GetDeleteClauses() => new List(); + protected override string GetBaseWhereClause() => + throw new InvalidOperationException("This method won't be implemented."); - protected override void PersistDeletedItem(ContentPermissionSet entity) => - throw new InvalidOperationException("This method won't be implemented."); + protected override IEnumerable GetDeleteClauses() => new List(); - #endregion - } + protected override void PersistDeletedItem(ContentPermissionSet entity) => + throw new InvalidOperationException("This method won't be implemented."); + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PublicAccessRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PublicAccessRepository.cs index c7f7724d6d9e..337749be4684 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PublicAccessRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PublicAccessRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -14,151 +11,147 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class PublicAccessRepository : EntityRepositoryBase, IPublicAccessRepository { - internal class PublicAccessRepository : EntityRepositoryBase, IPublicAccessRepository + public PublicAccessRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) { - public PublicAccessRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { } + } - protected override IRepositoryCachePolicy CreateCachePolicy() - { - return new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ false); - } + protected override IRepositoryCachePolicy CreateCachePolicy() => + new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, + GetEntityId, /*expires:*/ false); + + protected override PublicAccessEntry? PerformGet(Guid id) => + //return from GetAll - this will be cached as a collection + GetMany()?.FirstOrDefault(x => x.Key == id); + + protected override IEnumerable PerformGetAll(params Guid[]? ids) + { + Sql sql = GetBaseQuery(false); - protected override PublicAccessEntry? PerformGet(Guid id) + if (ids?.Any() ?? false) { - //return from GetAll - this will be cached as a collection - return GetMany()?.FirstOrDefault(x => x.Key == id); + sql.WhereIn(x => x.Id, ids); } - protected override IEnumerable PerformGetAll(params Guid[]? ids) - { - var sql = GetBaseQuery(false); + sql.OrderBy(x => x.NodeId); - if (ids?.Any() ?? false) - { - sql.WhereIn(x => x.Id, ids); - } + List? dtos = Database.FetchOneToMany(x => x.Rules, sql); + return dtos.Select(PublicAccessEntryFactory.BuildEntity); + } - sql.OrderBy(x => x.NodeId); + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); - var dtos = Database.FetchOneToMany(x => x.Rules, sql); - return dtos.Select(PublicAccessEntryFactory.BuildEntity); - } + List? dtos = Database.FetchOneToMany(x => x.Rules, sql); + return dtos.Select(PublicAccessEntryFactory.BuildEntity); + } - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); + protected override Sql GetBaseQuery(bool isCount) => + Sql() + .SelectAll() + .From() + .LeftJoin() + .On(left => left.Id, right => right.AccessId); - var dtos = Database.FetchOneToMany(x => x.Rules, sql); - return dtos.Select(PublicAccessEntryFactory.BuildEntity); - } + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Access}.id = @id"; - protected override Sql GetBaseQuery(bool isCount) + protected override IEnumerable GetDeleteClauses() + { + var list = new List { - return Sql() - .SelectAll() - .From() - .LeftJoin() - .On(left => left.Id, right => right.AccessId); - } + "DELETE FROM umbracoAccessRule WHERE accessId = @id", "DELETE FROM umbracoAccess WHERE id = @id" + }; + return list; + } - protected override string GetBaseWhereClause() + protected override void PersistNewItem(PublicAccessEntry entity) + { + entity.AddingEntity(); + foreach (PublicAccessRule rule in entity.Rules) { - return $"{Constants.DatabaseSchema.Tables.Access}.id = @id"; + rule.AddingEntity(); } - protected override IEnumerable GetDeleteClauses() + AccessDto dto = PublicAccessEntryFactory.BuildDto(entity); + + Database.Insert(dto); + //update the id so HasEntity is correct + entity.Id = entity.Key.GetHashCode(); + + foreach (AccessRuleDto rule in dto.Rules) { - var list = new List - { - "DELETE FROM umbracoAccessRule WHERE accessId = @id", - "DELETE FROM umbracoAccess WHERE id = @id" - }; - return list; + rule.AccessId = entity.Key; + Database.Insert(rule); } - protected override void PersistNewItem(PublicAccessEntry entity) + //update the id so HasEntity is correct + foreach (PublicAccessRule rule in entity.Rules) { - entity.AddingEntity(); - foreach (var rule in entity.Rules) - rule.AddingEntity(); - - var dto = PublicAccessEntryFactory.BuildDto(entity); + rule.Id = rule.Key.GetHashCode(); + } - Database.Insert(dto); - //update the id so HasEntity is correct - entity.Id = entity.Key.GetHashCode(); + entity.ResetDirtyProperties(); + } - foreach (var rule in dto.Rules) + protected override void PersistUpdatedItem(PublicAccessEntry entity) + { + entity.UpdatingEntity(); + foreach (PublicAccessRule rule in entity.Rules) + { + if (rule.HasIdentity) { - rule.AccessId = entity.Key; - Database.Insert(rule); + rule.UpdatingEntity(); } - - //update the id so HasEntity is correct - foreach (var rule in entity.Rules) - rule.Id = rule.Key.GetHashCode(); - - entity.ResetDirtyProperties(); - } - - protected override void PersistUpdatedItem(PublicAccessEntry entity) - { - entity.UpdatingEntity(); - foreach (var rule in entity.Rules) + else { - if (rule.HasIdentity) - rule.UpdatingEntity(); - else - rule.AddingEntity(); + rule.AddingEntity(); } + } - var dto = PublicAccessEntryFactory.BuildDto(entity); + AccessDto dto = PublicAccessEntryFactory.BuildDto(entity); - Database.Update(dto); + Database.Update(dto); - foreach (var removedRule in entity.RemovedRules) - { - Database.Delete("WHERE id=@Id", new { Id = removedRule }); - } + foreach (Guid removedRule in entity.RemovedRules) + { + Database.Delete("WHERE id=@Id", new {Id = removedRule}); + } - foreach (var rule in entity.Rules) + foreach (PublicAccessRule rule in entity.Rules) + { + if (rule.HasIdentity) { - if (rule.HasIdentity) + var count = Database.Update(dto.Rules.Single(x => x.Id == rule.Key)); + if (count == 0) { - var count = Database.Update(dto.Rules.Single(x => x.Id == rule.Key)); - if (count == 0) - { - throw new InvalidOperationException("No rows were updated for the access rule"); - } + throw new InvalidOperationException("No rows were updated for the access rule"); } - else + } + else + { + Database.Insert(new AccessRuleDto { - Database.Insert(new AccessRuleDto - { - Id = rule.Key, - AccessId = dto.Id, - RuleValue = rule.RuleValue, - RuleType = rule.RuleType, - CreateDate = rule.CreateDate, - UpdateDate = rule.UpdateDate - }); - //update the id so HasEntity is correct - rule.Id = rule.Key.GetHashCode(); - } + Id = rule.Key, + AccessId = dto.Id, + RuleValue = rule.RuleValue, + RuleType = rule.RuleType, + CreateDate = rule.CreateDate, + UpdateDate = rule.UpdateDate + }); + //update the id so HasEntity is correct + rule.Id = rule.Key.GetHashCode(); } - - entity.ResetDirtyProperties(); } - protected override Guid GetEntityId(PublicAccessEntry entity) - { - return entity.Key; - } + entity.ResetDirtyProperties(); } + + protected override Guid GetEntityId(PublicAccessEntry entity) => entity.Key; } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/QueryType.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/QueryType.cs index 72d7d2dfcc8a..d06af6dec5d9 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/QueryType.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/QueryType.cs @@ -1,28 +1,27 @@ -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Specifies the type of base query. +/// +public enum QueryType { /// - /// Specifies the type of base query. + /// Get one single complete item. /// - public enum QueryType - { - /// - /// Get one single complete item. - /// - Single, + Single, - /// - /// Get many complete items. - /// - Many, + /// + /// Get many complete items. + /// + Many, - /// - /// Get item identifiers only. - /// - Ids, + /// + /// Get item identifiers only. + /// + Ids, - /// - /// Count items. - /// - Count - } + /// + /// Count items. + /// + Count } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs index e49ccbdf7744..bef9a2fe7eaf 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Security.Cryptography; using Microsoft.Extensions.Logging; using NPoco; @@ -13,226 +10,226 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class RedirectUrlRepository : EntityRepositoryBase, IRedirectUrlRepository { - internal class RedirectUrlRepository : EntityRepositoryBase, IRedirectUrlRepository + public RedirectUrlRepository(IScopeAccessor scopeAccessor, AppCaches cache, + ILogger logger) + : base(scopeAccessor, cache, logger) { - public RedirectUrlRepository(IScopeAccessor scopeAccessor, AppCaches cache, - ILogger logger) - : base(scopeAccessor, cache, logger) - { - } + } - public IRedirectUrl? Get(string url, Guid contentKey, string? culture) - { - var urlHash = url.GenerateHash(); - Sql sql = GetBaseQuery(false).Where(x => - x.Url == url && x.UrlHash == urlHash && x.ContentKey == contentKey && x.Culture == culture); - RedirectUrlDto? dto = Database.Fetch(sql).FirstOrDefault(); - return dto == null ? null : Map(dto); - } + public IRedirectUrl? Get(string url, Guid contentKey, string? culture) + { + var urlHash = url.GenerateHash(); + Sql sql = GetBaseQuery(false).Where(x => + x.Url == url && x.UrlHash == urlHash && x.ContentKey == contentKey && x.Culture == culture); + RedirectUrlDto? dto = Database.Fetch(sql).FirstOrDefault(); + return dto == null ? null : Map(dto); + } - public void DeleteAll() => Database.Execute("DELETE FROM umbracoRedirectUrl"); + public void DeleteAll() => Database.Execute("DELETE FROM umbracoRedirectUrl"); - public void DeleteContentUrls(Guid contentKey) => - Database.Execute("DELETE FROM umbracoRedirectUrl WHERE contentKey=@contentKey", new { contentKey }); + public void DeleteContentUrls(Guid contentKey) => + Database.Execute("DELETE FROM umbracoRedirectUrl WHERE contentKey=@contentKey", new {contentKey}); - public void Delete(Guid id) => Database.Delete(id); + public void Delete(Guid id) => Database.Delete(id); - public IRedirectUrl? GetMostRecentUrl(string url) - { - var urlHash = url.GenerateHash(); - Sql sql = GetBaseQuery(false) - .Where(x => x.Url == url && x.UrlHash == urlHash) - .OrderByDescending(x => x.CreateDateUtc); - List dtos = Database.Fetch(sql); - RedirectUrlDto? dto = dtos.FirstOrDefault(); - return dto == null ? null : Map(dto); - } + public IRedirectUrl? GetMostRecentUrl(string url) + { + var urlHash = url.GenerateHash(); + Sql sql = GetBaseQuery(false) + .Where(x => x.Url == url && x.UrlHash == urlHash) + .OrderByDescending(x => x.CreateDateUtc); + List dtos = Database.Fetch(sql); + RedirectUrlDto? dto = dtos.FirstOrDefault(); + return dto == null ? null : Map(dto); + } - public IRedirectUrl? GetMostRecentUrl(string url, string culture) + public IRedirectUrl? GetMostRecentUrl(string url, string culture) + { + if (string.IsNullOrWhiteSpace(culture)) { - if (string.IsNullOrWhiteSpace(culture)) - { - return GetMostRecentUrl(url); - } - - var urlHash = url.GenerateHash(); - Sql sql = GetBaseQuery(false) - .Where(x => x.Url == url && x.UrlHash == urlHash && - (x.Culture == culture.ToLower() || x.Culture == null || x.Culture == string.Empty)) - .OrderByDescending(x => x.CreateDateUtc); - List dtos = Database.Fetch(sql); - RedirectUrlDto? dto = dtos.FirstOrDefault(f => f.Culture == culture.ToLower()); - - if (dto == null) - { - dto = dtos.FirstOrDefault(f => string.IsNullOrWhiteSpace(f.Culture)); - } - - return dto == null ? null : Map(dto); + return GetMostRecentUrl(url); } - public IEnumerable GetContentUrls(Guid contentKey) - { - Sql sql = GetBaseQuery(false) - .Where(x => x.ContentKey == contentKey) - .OrderByDescending(x => x.CreateDateUtc); - List dtos = Database.Fetch(sql); - return dtos.Select(Map).WhereNotNull(); - } + var urlHash = url.GenerateHash(); + Sql sql = GetBaseQuery(false) + .Where(x => x.Url == url && x.UrlHash == urlHash && + (x.Culture == culture.ToLower() || x.Culture == null || + x.Culture == string.Empty)) + .OrderByDescending(x => x.CreateDateUtc); + List dtos = Database.Fetch(sql); + RedirectUrlDto? dto = dtos.FirstOrDefault(f => f.Culture == culture.ToLower()); - public IEnumerable GetAllUrls(long pageIndex, int pageSize, out long total) + if (dto == null) { - Sql sql = GetBaseQuery(false) - .OrderByDescending(x => x.CreateDateUtc); - Page result = Database.Page(pageIndex + 1, pageSize, sql); - total = Convert.ToInt32(result.TotalItems); - return result.Items.Select(Map).WhereNotNull(); + dto = dtos.FirstOrDefault(f => string.IsNullOrWhiteSpace(f.Culture)); } - public IEnumerable GetAllUrls(int rootContentId, long pageIndex, int pageSize, out long total) - { - Sql sql = GetBaseQuery(false) - .Where( - string.Format("{0}.{1} LIKE @path", SqlSyntax.GetQuotedTableName("umbracoNode"), - SqlSyntax.GetQuotedColumnName("path")), new { path = "%," + rootContentId + ",%" }) - .OrderByDescending(x => x.CreateDateUtc); - Page result = Database.Page(pageIndex + 1, pageSize, sql); - total = Convert.ToInt32(result.TotalItems); - - IEnumerable rules = result.Items.Select(Map).WhereNotNull(); - return rules; - } + return dto == null ? null : Map(dto); + } - public IEnumerable SearchUrls(string searchTerm, long pageIndex, int pageSize, out long total) - { - Sql sql = GetBaseQuery(false) - .Where( - string.Format("{0}.{1} LIKE @url", SqlSyntax.GetQuotedTableName("umbracoRedirectUrl"), - SqlSyntax.GetQuotedColumnName("Url")), - new { url = "%" + searchTerm.Trim().ToLowerInvariant() + "%" }) - .OrderByDescending(x => x.CreateDateUtc); - Page result = Database.Page(pageIndex + 1, pageSize, sql); - total = Convert.ToInt32(result.TotalItems); - - IEnumerable rules = result.Items.Select(Map).WhereNotNull(); - return rules; - } + public IEnumerable GetContentUrls(Guid contentKey) + { + Sql sql = GetBaseQuery(false) + .Where(x => x.ContentKey == contentKey) + .OrderByDescending(x => x.CreateDateUtc); + List dtos = Database.Fetch(sql); + return dtos.Select(Map).WhereNotNull(); + } - protected override int PerformCount(IQuery query) => - throw new NotSupportedException("This repository does not support this method."); + public IEnumerable GetAllUrls(long pageIndex, int pageSize, out long total) + { + Sql sql = GetBaseQuery(false) + .OrderByDescending(x => x.CreateDateUtc); + Page result = Database.Page(pageIndex + 1, pageSize, sql); + total = Convert.ToInt32(result.TotalItems); + return result.Items.Select(Map).WhereNotNull(); + } - protected override bool PerformExists(Guid id) => PerformGet(id) != null; + public IEnumerable GetAllUrls(int rootContentId, long pageIndex, int pageSize, out long total) + { + Sql sql = GetBaseQuery(false) + .Where( + string.Format("{0}.{1} LIKE @path", SqlSyntax.GetQuotedTableName("umbracoNode"), + SqlSyntax.GetQuotedColumnName("path")), new {path = "%," + rootContentId + ",%"}) + .OrderByDescending(x => x.CreateDateUtc); + Page result = Database.Page(pageIndex + 1, pageSize, sql); + total = Convert.ToInt32(result.TotalItems); + + IEnumerable rules = result.Items.Select(Map).WhereNotNull(); + return rules; + } - protected override IRedirectUrl? PerformGet(Guid id) - { - Sql sql = GetBaseQuery(false).Where(x => x.Id == id); - RedirectUrlDto? dto = Database.Fetch(sql.SelectTop(1)).FirstOrDefault(); - return dto == null ? null : Map(dto); - } + public IEnumerable SearchUrls(string searchTerm, long pageIndex, int pageSize, out long total) + { + Sql sql = GetBaseQuery(false) + .Where( + string.Format("{0}.{1} LIKE @url", SqlSyntax.GetQuotedTableName("umbracoRedirectUrl"), + SqlSyntax.GetQuotedColumnName("Url")), + new {url = "%" + searchTerm.Trim().ToLowerInvariant() + "%"}) + .OrderByDescending(x => x.CreateDateUtc); + Page result = Database.Page(pageIndex + 1, pageSize, sql); + total = Convert.ToInt32(result.TotalItems); + + IEnumerable rules = result.Items.Select(Map).WhereNotNull(); + return rules; + } + + protected override int PerformCount(IQuery query) => + throw new NotSupportedException("This repository does not support this method."); + + protected override bool PerformExists(Guid id) => PerformGet(id) != null; + + protected override IRedirectUrl? PerformGet(Guid id) + { + Sql sql = GetBaseQuery(false).Where(x => x.Id == id); + RedirectUrlDto? dto = Database.Fetch(sql.SelectTop(1)).FirstOrDefault(); + return dto == null ? null : Map(dto); + } - protected override IEnumerable PerformGetAll(params Guid[]? ids) + protected override IEnumerable PerformGetAll(params Guid[]? ids) + { + if (ids?.Length > Constants.Sql.MaxParameterCount) { - if (ids?.Length > Constants.Sql.MaxParameterCount) - { - throw new NotSupportedException( - $"This repository does not support more than {Constants.Sql.MaxParameterCount} ids."); - } - - Sql sql = GetBaseQuery(false).WhereIn(x => x.Id, ids); - List dtos = Database.Fetch(sql); - return dtos.WhereNotNull().Select(Map).WhereNotNull(); + throw new NotSupportedException( + $"This repository does not support more than {Constants.Sql.MaxParameterCount} ids."); } - protected override IEnumerable PerformGetByQuery(IQuery query) => - throw new NotSupportedException("This repository does not support this method."); + Sql sql = GetBaseQuery(false).WhereIn(x => x.Id, ids); + List dtos = Database.Fetch(sql); + return dtos.WhereNotNull().Select(Map).WhereNotNull(); + } - protected override Sql GetBaseQuery(bool isCount) + protected override IEnumerable PerformGetByQuery(IQuery query) => + throw new NotSupportedException("This repository does not support this method."); + + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); + if (isCount) { - Sql sql = Sql(); - if (isCount) - { - sql.Select(@"COUNT(*) + sql.Select(@"COUNT(*) FROM umbracoRedirectUrl JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID"); - } - else - { - sql.Select(@"umbracoRedirectUrl.*, umbracoNode.id AS contentId + } + else + { + sql.Select(@"umbracoRedirectUrl.*, umbracoNode.id AS contentId FROM umbracoRedirectUrl JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID"); - } - - return sql; } - protected override string GetBaseWhereClause() => "id = @id"; + return sql; + } + + protected override string GetBaseWhereClause() => "id = @id"; - protected override IEnumerable GetDeleteClauses() + protected override IEnumerable GetDeleteClauses() + { + var list = new List {"DELETE FROM umbracoRedirectUrl WHERE id = @id"}; + return list; + } + + protected override void PersistNewItem(IRedirectUrl entity) + { + RedirectUrlDto? dto = Map(entity); + Database.Insert(dto); + entity.Id = entity.Key.GetHashCode(); + } + + protected override void PersistUpdatedItem(IRedirectUrl entity) + { + RedirectUrlDto? dto = Map(entity); + if (dto is not null) { - var list = new List { "DELETE FROM umbracoRedirectUrl WHERE id = @id" }; - return list; + Database.Update(dto); } + } - protected override void PersistNewItem(IRedirectUrl entity) + private static RedirectUrlDto? Map(IRedirectUrl redirectUrl) + { + if (redirectUrl == null) { - RedirectUrlDto? dto = Map(entity); - Database.Insert(dto); - entity.Id = entity.Key.GetHashCode(); + return null; } - protected override void PersistUpdatedItem(IRedirectUrl entity) + return new RedirectUrlDto { - RedirectUrlDto? dto = Map(entity); - if (dto is not null) - { - Database.Update(dto); - } - } + Id = redirectUrl.Key, + ContentKey = redirectUrl.ContentKey, + CreateDateUtc = redirectUrl.CreateDateUtc, + Url = redirectUrl.Url, + Culture = redirectUrl.Culture, + UrlHash = redirectUrl.Url.GenerateHash() + }; + } - private static RedirectUrlDto? Map(IRedirectUrl redirectUrl) + private static IRedirectUrl? Map(RedirectUrlDto dto) + { + if (dto == null) { - if (redirectUrl == null) - { - return null; - } - - return new RedirectUrlDto - { - Id = redirectUrl.Key, - ContentKey = redirectUrl.ContentKey, - CreateDateUtc = redirectUrl.CreateDateUtc, - Url = redirectUrl.Url, - Culture = redirectUrl.Culture, - UrlHash = redirectUrl.Url.GenerateHash() - }; + return null; } - private static IRedirectUrl? Map(RedirectUrlDto dto) + var url = new RedirectUrl(); + try + { + url.DisableChangeTracking(); + url.Key = dto.Id; + url.Id = dto.Id.GetHashCode(); + url.ContentId = dto.ContentId; + url.ContentKey = dto.ContentKey; + url.CreateDateUtc = dto.CreateDateUtc; + url.Culture = dto.Culture; + url.Url = dto.Url; + return url; + } + finally { - if (dto == null) - { - return null; - } - - var url = new RedirectUrl(); - try - { - url.DisableChangeTracking(); - url.Key = dto.Id; - url.Id = dto.Id.GetHashCode(); - url.ContentId = dto.ContentId; - url.ContentKey = dto.ContentKey; - url.CreateDateUtc = dto.CreateDateUtc; - url.Culture = dto.Culture; - url.Url = dto.Url; - return url; - } - finally - { - url.EnableChangeTracking(); - } + url.EnableChangeTracking(); } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs index d7e65adaf49a..6efc7505ee91 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -19,176 +15,222 @@ using Umbraco.Extensions; using static Umbraco.Cms.Core.Persistence.SqlExtensionsStatics; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents a repository for doing CRUD operations for +/// +internal class RelationRepository : EntityRepositoryBase, IRelationRepository { - /// - /// Represents a repository for doing CRUD operations for - /// - internal class RelationRepository : EntityRepositoryBase, IRelationRepository + private readonly IEntityRepositoryExtended _entityRepository; + private readonly IRelationTypeRepository _relationTypeRepository; + + public RelationRepository(IScopeAccessor scopeAccessor, ILogger logger, + IRelationTypeRepository relationTypeRepository, IEntityRepositoryExtended entityRepository) + : base(scopeAccessor, AppCaches.NoCache, logger) { - private readonly IRelationTypeRepository _relationTypeRepository; - private readonly IEntityRepositoryExtended _entityRepository; + _relationTypeRepository = relationTypeRepository; + _entityRepository = entityRepository; + } - public RelationRepository(IScopeAccessor scopeAccessor, ILogger logger, IRelationTypeRepository relationTypeRepository, IEntityRepositoryExtended entityRepository) - : base(scopeAccessor, AppCaches.NoCache, logger) - { - _relationTypeRepository = relationTypeRepository; - _entityRepository = entityRepository; - } + public IEnumerable GetPagedParentEntitiesByChildId(int childId, long pageIndex, int pageSize, + out long totalRecords, params Guid[] entityTypes) => GetPagedParentEntitiesByChildId(childId, pageIndex, + pageSize, out totalRecords, new int[0], entityTypes); - #region Overrides of RepositoryBase + public IEnumerable GetPagedChildEntitiesByParentId(int parentId, long pageIndex, int pageSize, + out long totalRecords, params Guid[] entityTypes) => GetPagedChildEntitiesByParentId(parentId, pageIndex, + pageSize, out totalRecords, new int[0], entityTypes); - protected override IRelation? PerformGet(int id) + public void Save(IEnumerable relations) + { + foreach (IGrouping hasIdentityGroup in relations.GroupBy(r => r.HasIdentity)) { - var sql = GetBaseQuery(false); - sql.Where(GetBaseWhereClause(), new { id }); - - var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); - if (dto == null) - return null; - - var relationType = _relationTypeRepository.Get(dto.RelationType); - if (relationType == null) - throw new InvalidOperationException(string.Format("RelationType with Id: {0} doesn't exist", dto.RelationType)); + if (hasIdentityGroup.Key) + { + // Do updates, we can't really do a bulk update so this is still a 1 by 1 operation + // however we can bulk populate the object types. It might be possible to bulk update + // with SQL but would be pretty ugly and we're not really too worried about that for perf, + // it's the bulk inserts we care about. + IRelation[] asArray = hasIdentityGroup.ToArray(); + foreach (IRelation relation in hasIdentityGroup) + { + relation.UpdatingEntity(); + RelationDto dto = RelationFactory.BuildDto(relation); + Database.Update(dto); + } - return DtoToEntity(dto, relationType); - } + PopulateObjectTypes(asArray); + } + else + { + // Do bulk inserts + var entitiesAndDtos = hasIdentityGroup.ToDictionary( + r => // key = entity + { + r.AddingEntity(); + return r; + }, + RelationFactory.BuildDto); // value = DTO - protected override IEnumerable PerformGetAll(params int[]? ids) - { - var sql = GetBaseQuery(false); - if (ids?.Length > 0) - sql.WhereIn(x => x.Id, ids); - sql.OrderBy(x => x.RelationType); - var dtos = Database.Fetch(sql); - return DtosToEntities(dtos); - } - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - sql.OrderBy(x => x.RelationType); - var dtos = Database.Fetch(sql); - return DtosToEntities(dtos); - } + foreach (RelationDto dto in entitiesAndDtos.Values) + { + Database.Insert(dto); + } - private IEnumerable DtosToEntities(IEnumerable dtos) - { - //NOTE: This is N+1, BUT ALL relation types are cached so shouldn't matter + // All dtos now have IDs assigned + foreach (KeyValuePair de in entitiesAndDtos) + { + // re-assign ID to the entity + de.Key.Id = de.Value.Id; + } - return dtos.Select(x => DtoToEntity(x, _relationTypeRepository.Get(x.RelationType))).WhereNotNull().ToList(); + PopulateObjectTypes(entitiesAndDtos.Keys.ToArray()); + } } + } - private static IRelation? DtoToEntity(RelationDto dto, IRelationType? relationType) + public void SaveBulk(IEnumerable relations) + { + foreach (IGrouping hasIdentityGroup in relations.GroupBy(r => r.HasIdentity)) { - if (relationType is null) + if (hasIdentityGroup.Key) { - return null; + // Do updates, we can't really do a bulk update so this is still a 1 by 1 operation + // however we can bulk populate the object types. It might be possible to bulk update + // with SQL but would be pretty ugly and we're not really too worried about that for perf, + // it's the bulk inserts we care about. + foreach (ReadOnlyRelation relation in hasIdentityGroup) + { + RelationDto dto = RelationFactory.BuildDto(relation); + Database.Update(dto); + } } - var entity = RelationFactory.BuildEntity(dto, relationType); - - // reset dirty initial properties (U4-1946) - entity.ResetDirtyProperties(false); + else + { + // Do bulk inserts + IEnumerable dtos = hasIdentityGroup.Select(RelationFactory.BuildDto); - return entity; + Database.InsertBulk(dtos); + } } + } - #endregion - - #region Overrides of EntityRepositoryBase + public IEnumerable GetPagedRelationsByQuery(IQuery? query, long pageIndex, int pageSize, + out long totalRecords, Ordering? ordering) + { + Sql sql = GetBaseQuery(false); - protected override Sql GetBaseQuery(bool isCount) + if (ordering == null || ordering.IsEmpty) { - if (isCount) - { - return Sql().SelectCount().From(); - } + ordering = Ordering.By(SqlSyntax.GetQuotedColumn(Constants.DatabaseSchema.Tables.Relation, "id")); + } - var sql = Sql().Select() - .AndSelect("uchild", x => Alias(x.NodeObjectType, "childObjectType")) - .AndSelect("uparent", x => Alias(x.NodeObjectType, "parentObjectType")) - .From() - .InnerJoin("uchild").On((rel, node) => rel.ChildId == node.NodeId, aliasRight: "uchild") - .InnerJoin("uparent").On((rel, node) => rel.ParentId == node.NodeId, aliasRight: "uparent"); + var translator = new SqlTranslator(sql, query); + sql = translator.Translate(); + // apply ordering + ApplyOrdering(ref sql, ordering); - return sql; - } + var pageIndexToFetch = pageIndex + 1; + Page? page = Database.Page(pageIndexToFetch, pageSize, sql); + List? dtos = page.Items; + totalRecords = page.TotalItems; - protected override string GetBaseWhereClause() - { - return $"{Constants.DatabaseSchema.Tables.Relation}.id = @id"; - } + var relTypes = _relationTypeRepository.GetMany(dtos.Select(x => x.RelationType).Distinct().ToArray())? + .ToDictionary(x => x.Id, x => x); - protected override IEnumerable GetDeleteClauses() + var result = dtos.Select(r => { - var list = new List - { - "DELETE FROM umbracoRelation WHERE id = @id" - }; - return list; - } + if (relTypes is null || !relTypes.TryGetValue(r.RelationType, out IRelationType? relType)) + { + throw new InvalidOperationException(string.Format("RelationType with Id: {0} doesn't exist", + r.RelationType)); + } - #endregion + return DtoToEntity(r, relType); + }).WhereNotNull().ToList(); - #region Unit of Work Implementation + return result; + } - protected override void PersistNewItem(IRelation entity) + + public void DeleteByParent(int parentId, params string[] relationTypeAliases) + { + // HACK: SQLite - hard to replace this without provider specific repositories/another ORM. + if (Database.DatabaseType.IsSqlite()) { - entity.AddingEntity(); + Sql? query = Sql().Append(@"delete from umbracoRelation"); - var dto = RelationFactory.BuildDto(entity); + Sql subQuery = Sql().Select(x => x.Id) + .From() + .InnerJoin().On(x => x.RelationType, x => x.Id) + .Where(x => x.ParentId == parentId); - var id = Convert.ToInt32(Database.Insert(dto)); + if (relationTypeAliases.Length > 0) + { + subQuery.WhereIn(x => x.Alias, relationTypeAliases); + } - entity.Id = id; - PopulateObjectTypes(entity); + Sql fullQuery = query.WhereIn(x => x.Id, subQuery); - entity.ResetDirtyProperties(); + Database.Execute(fullQuery); } - - protected override void PersistUpdatedItem(IRelation entity) + else { - entity.UpdatingEntity(); - - var dto = RelationFactory.BuildDto(entity); - Database.Update(dto); - - PopulateObjectTypes(entity); + if (relationTypeAliases.Length > 0) + { + SqlTemplate template = SqlContext.Templates.Get( + Constants.SqlTemplates.RelationRepository.DeleteByParentIn, + tsql => Sql().Delete() + .From() + .InnerJoin().On(x => x.RelationType, x => x.Id) + .Where(x => x.ParentId == SqlTemplate.Arg("parentId")) + .WhereIn(x => x.Alias, SqlTemplate.ArgIn("relationTypeAliases"))); - entity.ResetDirtyProperties(); - } + Sql sql = template.Sql(parentId, relationTypeAliases); - #endregion + Database.Execute(sql); + } + else + { + SqlTemplate template = SqlContext.Templates.Get( + Constants.SqlTemplates.RelationRepository.DeleteByParentAll, + tsql => Sql().Delete() + .From() + .InnerJoin().On(x => x.RelationType, x => x.Id) + .Where(x => x.ParentId == SqlTemplate.Arg("parentId"))); - /// - /// Used for joining the entity query with relations for the paging methods - /// - /// - private void SqlJoinRelations(Sql sql) - { - // add left joins for relation tables (this joins on both child or parent, so beware that this will normally return entities for - // both sides of the relation type unless the IUmbracoEntity query passed in filters one side out). - sql.LeftJoin().On((left, right) => left.NodeId == right.ChildId || left.NodeId == right.ParentId); - sql.LeftJoin().On((left, right) => left.RelationType == right.Id); - } + Sql sql = template.Sql(parentId); - public IEnumerable GetPagedParentEntitiesByChildId(int childId, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes) - { - return GetPagedParentEntitiesByChildId(childId, pageIndex, pageSize, out totalRecords, new int[0], entityTypes); + Database.Execute(sql); + } } + } - public IEnumerable GetPagedParentEntitiesByChildId(int childId, long pageIndex, int pageSize, out long totalRecords, int[] relationTypes, params Guid[] entityTypes) - { - // var contentObjectTypes = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media, Constants.ObjectTypes.Member } - // we could pass in the contentObjectTypes so that the entity repository sql is configured to do full entity lookups so that we get the full data - // required to populate content, media or members, else we get the bare minimum data needed to populate an entity. BUT if we do this it - // means that the SQL is less efficient and returns data that is probably not needed for what we need this lookup for. For the time being we - // will just return the bare minimum entity data. + /// + /// Used for joining the entity query with relations for the paging methods + /// + /// + private void SqlJoinRelations(Sql sql) + { + // add left joins for relation tables (this joins on both child or parent, so beware that this will normally return entities for + // both sides of the relation type unless the IUmbracoEntity query passed in filters one side out). + sql.LeftJoin() + .On((left, right) => left.NodeId == right.ChildId || left.NodeId == right.ParentId); + sql.LeftJoin() + .On((left, right) => left.RelationType == right.Id); + } - return _entityRepository.GetPagedResultsByQuery(Query(), entityTypes, pageIndex, pageSize, out totalRecords, null, null, sql => + public IEnumerable GetPagedParentEntitiesByChildId(int childId, long pageIndex, int pageSize, + out long totalRecords, int[] relationTypes, params Guid[] entityTypes) => + // var contentObjectTypes = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media, Constants.ObjectTypes.Member } + // we could pass in the contentObjectTypes so that the entity repository sql is configured to do full entity lookups so that we get the full data + // required to populate content, media or members, else we get the bare minimum data needed to populate an entity. BUT if we do this it + // means that the SQL is less efficient and returns data that is probably not needed for what we need this lookup for. For the time being we + // will just return the bare minimum entity data. + _entityRepository.GetPagedResultsByQuery(Query(), entityTypes, pageIndex, pageSize, + out totalRecords, null, null, sql => { SqlJoinRelations(sql); @@ -200,22 +242,16 @@ public IEnumerable GetPagedParentEntitiesByChildId(int childId, sql.WhereIn(rel => rel.RelationType, relationTypes); } }); - } - - public IEnumerable GetPagedChildEntitiesByParentId(int parentId, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes) - { - return GetPagedChildEntitiesByParentId(parentId, pageIndex, pageSize, out totalRecords, new int[0], entityTypes); - } - public IEnumerable GetPagedChildEntitiesByParentId(int parentId, long pageIndex, int pageSize, out long totalRecords, int[] relationTypes, params Guid[] entityTypes) - { - // var contentObjectTypes = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media, Constants.ObjectTypes.Member } - // we could pass in the contentObjectTypes so that the entity repository sql is configured to do full entity lookups so that we get the full data - // required to populate content, media or members, else we get the bare minimum data needed to populate an entity. BUT if we do this it - // means that the SQL is less efficient and returns data that is probably not needed for what we need this lookup for. For the time being we - // will just return the bare minimum entity data. - - return _entityRepository.GetPagedResultsByQuery(Query(), entityTypes, pageIndex, pageSize, out totalRecords, null, null, sql => + public IEnumerable GetPagedChildEntitiesByParentId(int parentId, long pageIndex, int pageSize, + out long totalRecords, int[] relationTypes, params Guid[] entityTypes) => + // var contentObjectTypes = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media, Constants.ObjectTypes.Member } + // we could pass in the contentObjectTypes so that the entity repository sql is configured to do full entity lookups so that we get the full data + // required to populate content, media or members, else we get the bare minimum data needed to populate an entity. BUT if we do this it + // means that the SQL is less efficient and returns data that is probably not needed for what we need this lookup for. For the time being we + // will just return the bare minimum entity data. + _entityRepository.GetPagedResultsByQuery(Query(), entityTypes, pageIndex, pageSize, + out totalRecords, null, null, sql => { SqlJoinRelations(sql); @@ -227,241 +263,212 @@ public IEnumerable GetPagedChildEntitiesByParentId(int parentId, sql.WhereIn(rel => rel.RelationType, relationTypes); } }); - } - - public void Save(IEnumerable relations) - { - foreach (var hasIdentityGroup in relations.GroupBy(r => r.HasIdentity)) - { - if (hasIdentityGroup.Key) - { - // Do updates, we can't really do a bulk update so this is still a 1 by 1 operation - // however we can bulk populate the object types. It might be possible to bulk update - // with SQL but would be pretty ugly and we're not really too worried about that for perf, - // it's the bulk inserts we care about. - var asArray = hasIdentityGroup.ToArray(); - foreach (var relation in hasIdentityGroup) - { - relation.UpdatingEntity(); - var dto = RelationFactory.BuildDto(relation); - Database.Update(dto); - } - PopulateObjectTypes(asArray); - } - else - { - // Do bulk inserts - var entitiesAndDtos = hasIdentityGroup.ToDictionary( - r => // key = entity - { - r.AddingEntity(); - return r; - }, - RelationFactory.BuildDto); // value = DTO + /// + /// Used to populate the object types after insert/update + /// + /// + private void PopulateObjectTypes(params IRelation[] entities) + { + IEnumerable entityIds = + entities.Select(x => x.ParentId).Concat(entities.Select(y => y.ChildId)).Distinct(); - foreach (var dto in entitiesAndDtos.Values) - { - Database.Insert(dto); - } + var nodes = Database.Fetch(Sql().Select().From() + .WhereIn(x => x.NodeId, entityIds)) + .ToDictionary(x => x.NodeId, x => x.NodeObjectType); - // All dtos now have IDs assigned - foreach (var de in entitiesAndDtos) - { - // re-assign ID to the entity - de.Key.Id = de.Value.Id; - } + foreach (IRelation e in entities) + { + if (nodes.TryGetValue(e.ParentId, out Guid? parentObjectType)) + { + e.ParentObjectType = parentObjectType.GetValueOrDefault(); + } - PopulateObjectTypes(entitiesAndDtos.Keys.ToArray()); - } + if (nodes.TryGetValue(e.ChildId, out Guid? childObjectType)) + { + e.ChildObjectType = childObjectType.GetValueOrDefault(); } } + } - public void SaveBulk(IEnumerable relations) + private void ApplyOrdering(ref Sql sql, Ordering ordering) + { + if (sql == null) { - foreach (var hasIdentityGroup in relations.GroupBy(r => r.HasIdentity)) - { - if (hasIdentityGroup.Key) - { - // Do updates, we can't really do a bulk update so this is still a 1 by 1 operation - // however we can bulk populate the object types. It might be possible to bulk update - // with SQL but would be pretty ugly and we're not really too worried about that for perf, - // it's the bulk inserts we care about. - foreach (var relation in hasIdentityGroup) - { - var dto = RelationFactory.BuildDto(relation); - Database.Update(dto); - } - } - else - { - // Do bulk inserts - var dtos = hasIdentityGroup.Select(RelationFactory.BuildDto); + throw new ArgumentNullException(nameof(sql)); + } - Database.InsertBulk(dtos); + if (ordering == null) + { + throw new ArgumentNullException(nameof(ordering)); + } - } - } + // TODO: although this works for name, it probably doesn't work for others without an alias of some sort + var orderBy = ordering.OrderBy; + + if (ordering.Direction == Direction.Ascending) + { + sql.OrderBy(orderBy); + } + else + { + sql.OrderByDescending(orderBy); } + } + + #region Overrides of RepositoryBase - public IEnumerable GetPagedRelationsByQuery(IQuery? query, long pageIndex, int pageSize, out long totalRecords, Ordering? ordering) + protected override IRelation? PerformGet(int id) + { + Sql sql = GetBaseQuery(false); + sql.Where(GetBaseWhereClause(), new {id}); + + RelationDto? dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); + if (dto == null) { - var sql = GetBaseQuery(false); + return null; + } - if (ordering == null || ordering.IsEmpty) - ordering = Ordering.By(SqlSyntax.GetQuotedColumn(Cms.Core.Constants.DatabaseSchema.Tables.Relation, "id")); + IRelationType? relationType = _relationTypeRepository.Get(dto.RelationType); + if (relationType == null) + { + throw new InvalidOperationException(string.Format("RelationType with Id: {0} doesn't exist", + dto.RelationType)); + } - var translator = new SqlTranslator(sql, query); - sql = translator.Translate(); + return DtoToEntity(dto, relationType); + } - // apply ordering - ApplyOrdering(ref sql, ordering); + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = GetBaseQuery(false); + if (ids?.Length > 0) + { + sql.WhereIn(x => x.Id, ids); + } - var pageIndexToFetch = pageIndex + 1; - var page = Database.Page(pageIndexToFetch, pageSize, sql); - var dtos = page.Items; - totalRecords = page.TotalItems; + sql.OrderBy(x => x.RelationType); + List? dtos = Database.Fetch(sql); + return DtosToEntities(dtos); + } - var relTypes = _relationTypeRepository.GetMany(dtos.Select(x => x.RelationType).Distinct().ToArray())? - .ToDictionary(x => x.Id, x => x); + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + sql.OrderBy(x => x.RelationType); + List? dtos = Database.Fetch(sql); + return DtosToEntities(dtos); + } - var result = dtos.Select(r => - { - if (relTypes is null || !relTypes.TryGetValue(r.RelationType, out var relType)) - throw new InvalidOperationException(string.Format("RelationType with Id: {0} doesn't exist", r.RelationType)); - return DtoToEntity(r, relType); - }).WhereNotNull().ToList(); + private IEnumerable DtosToEntities(IEnumerable dtos) => + //NOTE: This is N+1, BUT ALL relation types are cached so shouldn't matter + dtos.Select(x => DtoToEntity(x, _relationTypeRepository.Get(x.RelationType))).WhereNotNull().ToList(); - return result; + private static IRelation? DtoToEntity(RelationDto dto, IRelationType? relationType) + { + if (relationType is null) + { + return null; } + IRelation entity = RelationFactory.BuildEntity(dto, relationType); - public void DeleteByParent(int parentId, params string[] relationTypeAliases) - { - // HACK: SQLite - hard to replace this without provider specific repositories/another ORM. - if (Database.DatabaseType.IsSqlite()) - { - var query = Sql().Append(@"delete from umbracoRelation"); + // reset dirty initial properties (U4-1946) + entity.ResetDirtyProperties(false); - var subQuery = Sql().Select(x => x.Id) - .From() - .InnerJoin().On(x => x.RelationType, x => x.Id) - .Where(x => x.ParentId == parentId); + return entity; + } - if (relationTypeAliases.Length > 0) - { - subQuery.WhereIn(x => x.Alias, relationTypeAliases); - } + #endregion - var fullQuery = query.WhereIn(x => x.Id, subQuery); + #region Overrides of EntityRepositoryBase - Database.Execute(fullQuery); - } - else - { - if (relationTypeAliases.Length > 0) - { - var template = SqlContext.Templates.Get( - Cms.Core.Constants.SqlTemplates.RelationRepository.DeleteByParentIn, - tsql => Sql().Delete() - .From() - .InnerJoin().On(x => x.RelationType, x => x.Id) - .Where(x => x.ParentId == SqlTemplate.Arg("parentId")) - .WhereIn(x => x.Alias, SqlTemplate.ArgIn("relationTypeAliases"))); + protected override Sql GetBaseQuery(bool isCount) + { + if (isCount) + { + return Sql().SelectCount().From(); + } - var sql = template.Sql(parentId, relationTypeAliases); + Sql sql = Sql().Select() + .AndSelect("uchild", x => Alias(x.NodeObjectType, "childObjectType")) + .AndSelect("uparent", x => Alias(x.NodeObjectType, "parentObjectType")) + .From() + .InnerJoin("uchild") + .On((rel, node) => rel.ChildId == node.NodeId, aliasRight: "uchild") + .InnerJoin("uparent") + .On((rel, node) => rel.ParentId == node.NodeId, aliasRight: "uparent"); - Database.Execute(sql); - } - else - { - var template = SqlContext.Templates.Get( - Cms.Core.Constants.SqlTemplates.RelationRepository.DeleteByParentAll, - tsql => Sql().Delete() - .From() - .InnerJoin().On(x => x.RelationType, x => x.Id) - .Where(x => x.ParentId == SqlTemplate.Arg("parentId"))); - var sql = template.Sql(parentId); + return sql; + } - Database.Execute(sql); - } - } - } + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Relation}.id = @id"; - /// - /// Used to populate the object types after insert/update - /// - /// - private void PopulateObjectTypes(params IRelation[] entities) - { - var entityIds = entities.Select(x => x.ParentId).Concat(entities.Select(y => y.ChildId)).Distinct(); + protected override IEnumerable GetDeleteClauses() + { + var list = new List {"DELETE FROM umbracoRelation WHERE id = @id"}; + return list; + } - var nodes = Database.Fetch(Sql().Select().From() - .WhereIn(x => x.NodeId, entityIds)) - .ToDictionary(x => x.NodeId, x => x.NodeObjectType); + #endregion - foreach (var e in entities) - { - if (nodes.TryGetValue(e.ParentId, out var parentObjectType)) - { - e.ParentObjectType = parentObjectType.GetValueOrDefault(); - } - if (nodes.TryGetValue(e.ChildId, out var childObjectType)) - { - e.ChildObjectType = childObjectType.GetValueOrDefault(); - } - } - } + #region Unit of Work Implementation - private void ApplyOrdering(ref Sql sql, Ordering ordering) - { - if (sql == null) throw new ArgumentNullException(nameof(sql)); - if (ordering == null) throw new ArgumentNullException(nameof(ordering)); + protected override void PersistNewItem(IRelation entity) + { + entity.AddingEntity(); - // TODO: although this works for name, it probably doesn't work for others without an alias of some sort - var orderBy = ordering.OrderBy; + RelationDto dto = RelationFactory.BuildDto(entity); - if (ordering.Direction == Direction.Ascending) - sql.OrderBy(orderBy); - else - sql.OrderByDescending(orderBy); - } + var id = Convert.ToInt32(Database.Insert(dto)); + + entity.Id = id; + PopulateObjectTypes(entity); + + entity.ResetDirtyProperties(); } - internal class RelationItemDto + protected override void PersistUpdatedItem(IRelation entity) { - [Column(Name = "nodeId")] - public int ChildNodeId { get; set; } + entity.UpdatingEntity(); - [Column(Name = "nodeKey")] - public Guid ChildNodeKey { get; set; } + RelationDto dto = RelationFactory.BuildDto(entity); + Database.Update(dto); - [Column(Name = "nodeName")] - public string? ChildNodeName { get; set; } + PopulateObjectTypes(entity); - [Column(Name = "nodeObjectType")] - public Guid ChildNodeObjectType { get; set; } + entity.ResetDirtyProperties(); + } - [Column(Name = "contentTypeIcon")] - public string? ChildContentTypeIcon { get; set; } + #endregion +} - [Column(Name = "contentTypeAlias")] - public string? ChildContentTypeAlias { get; set; } +internal class RelationItemDto +{ + [Column(Name = "nodeId")] public int ChildNodeId { get; set; } - [Column(Name = "contentTypeName")] - public string? ChildContentTypeName { get; set; } + [Column(Name = "nodeKey")] public Guid ChildNodeKey { get; set; } - [Column(Name = "relationTypeName")] - public string? RelationTypeName { get; set; } + [Column(Name = "nodeName")] public string? ChildNodeName { get; set; } - [Column(Name = "relationTypeAlias")] - public string? RelationTypeAlias { get; set; } + [Column(Name = "nodeObjectType")] public Guid ChildNodeObjectType { get; set; } - [Column(Name = "relationTypeIsDependency")] - public bool RelationTypeIsDependency { get; set; } + [Column(Name = "contentTypeIcon")] public string? ChildContentTypeIcon { get; set; } - [Column(Name = "relationTypeIsBidirectional")] - public bool RelationTypeIsBidirectional { get; set; } - } + [Column(Name = "contentTypeAlias")] public string? ChildContentTypeAlias { get; set; } + + [Column(Name = "contentTypeName")] public string? ChildContentTypeName { get; set; } + + [Column(Name = "relationTypeName")] public string? RelationTypeName { get; set; } + + [Column(Name = "relationTypeAlias")] public string? RelationTypeAlias { get; set; } + + [Column(Name = "relationTypeIsDependency")] + public bool RelationTypeIsDependency { get; set; } + + [Column(Name = "relationTypeIsBidirectional")] + public bool RelationTypeIsBidirectional { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationTypeRepository.cs index 0d1b2583744f..f6b1bf52d638 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationTypeRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationTypeRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -15,151 +12,146 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents a repository for doing CRUD operations for +/// +internal class RelationTypeRepository : EntityRepositoryBase, IRelationTypeRepository { - /// - /// Represents a repository for doing CRUD operations for - /// - internal class RelationTypeRepository : EntityRepositoryBase, IRelationTypeRepository + public RelationTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) { - public RelationTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { } - - protected override IRepositoryCachePolicy CreateCachePolicy() - { - return new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ true); - } - - #region Overrides of RepositoryBase + } - protected override IRelationType? PerformGet(int id) - { - // use the underlying GetAll which will force cache all content types - return GetMany()?.FirstOrDefault(x => x.Id == id); - } + protected override IRepositoryCachePolicy CreateCachePolicy() => + new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, + GetEntityId, /*expires:*/ true); - public IRelationType? Get(Guid id) + private void CheckNullObjectTypeValues(IRelationType entity) + { + if (entity.ParentObjectType.HasValue && entity.ParentObjectType == Guid.Empty) { - // use the underlying GetAll which will force cache all content types - return GetMany()?.FirstOrDefault(x => x.Key == id); + entity.ParentObjectType = null; } - public bool Exists(Guid id) + if (entity.ChildObjectType.HasValue && entity.ChildObjectType == Guid.Empty) { - return Get(id) != null; + entity.ChildObjectType = null; } + } - protected override IEnumerable PerformGetAll(params int[]? ids) - { - var sql = GetBaseQuery(false); + #region Overrides of RepositoryBase - var dtos = Database.Fetch(sql); + protected override IRelationType? PerformGet(int id) => + // use the underlying GetAll which will force cache all content types + GetMany()?.FirstOrDefault(x => x.Id == id); - return dtos.Select(x => DtoToEntity(x)); - } + public IRelationType? Get(Guid id) => + // use the underlying GetAll which will force cache all content types + GetMany()?.FirstOrDefault(x => x.Key == id); - public IEnumerable GetMany(params Guid[]? ids) - { - // should not happen due to the cache policy - if (ids?.Any() ?? false) - throw new NotImplementedException(); + public bool Exists(Guid id) => Get(id) != null; - return GetMany(new int[0]); - } + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = GetBaseQuery(false); - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); + List? dtos = Database.Fetch(sql); - var dtos = Database.Fetch(sql); + return dtos.Select(x => DtoToEntity(x)); + } - return dtos.Select(x => DtoToEntity(x)); + public IEnumerable GetMany(params Guid[]? ids) + { + // should not happen due to the cache policy + if (ids?.Any() ?? false) + { + throw new NotImplementedException(); } - private static IRelationType DtoToEntity(RelationTypeDto dto) - { - var entity = RelationTypeFactory.BuildEntity(dto); + return GetMany(new int[0]); + } - // reset dirty initial properties (U4-1946) - ((BeingDirtyBase) entity).ResetDirtyProperties(false); + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); - return entity; - } + List? dtos = Database.Fetch(sql); - #endregion + return dtos.Select(x => DtoToEntity(x)); + } - #region Overrides of EntityRepositoryBase + private static IRelationType DtoToEntity(RelationTypeDto dto) + { + IRelationType entity = RelationTypeFactory.BuildEntity(dto); - protected override Sql GetBaseQuery(bool isCount) - { - var sql = Sql(); + // reset dirty initial properties (U4-1946) + ((BeingDirtyBase)entity).ResetDirtyProperties(false); - sql = isCount - ? sql.SelectCount() - : sql.Select(); + return entity; + } - sql - .From(); + #endregion - return sql; - } + #region Overrides of EntityRepositoryBase - protected override string GetBaseWhereClause() - { - return $"{Constants.DatabaseSchema.Tables.RelationType}.id = @id"; - } + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); - protected override IEnumerable GetDeleteClauses() - { - var list = new List - { - "DELETE FROM umbracoRelation WHERE relType = @id", - "DELETE FROM umbracoRelationType WHERE id = @id" - }; - return list; - } + sql = isCount + ? sql.SelectCount() + : sql.Select(); - #endregion + sql + .From(); - #region Unit of Work Implementation + return sql; + } + + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.RelationType}.id = @id"; - protected override void PersistNewItem(IRelationType entity) + protected override IEnumerable GetDeleteClauses() + { + var list = new List { - entity.AddingEntity(); + "DELETE FROM umbracoRelation WHERE relType = @id", "DELETE FROM umbracoRelationType WHERE id = @id" + }; + return list; + } - CheckNullObjectTypeValues(entity); + #endregion - var dto = RelationTypeFactory.BuildDto(entity); + #region Unit of Work Implementation - var id = Convert.ToInt32(Database.Insert(dto)); - entity.Id = id; + protected override void PersistNewItem(IRelationType entity) + { + entity.AddingEntity(); - entity.ResetDirtyProperties(); - } + CheckNullObjectTypeValues(entity); - protected override void PersistUpdatedItem(IRelationType entity) - { - entity.UpdatingEntity(); + RelationTypeDto dto = RelationTypeFactory.BuildDto(entity); - CheckNullObjectTypeValues(entity); + var id = Convert.ToInt32(Database.Insert(dto)); + entity.Id = id; - var dto = RelationTypeFactory.BuildDto(entity); - Database.Update(dto); + entity.ResetDirtyProperties(); + } - entity.ResetDirtyProperties(); - } + protected override void PersistUpdatedItem(IRelationType entity) + { + entity.UpdatingEntity(); - #endregion + CheckNullObjectTypeValues(entity); - private void CheckNullObjectTypeValues(IRelationType entity) - { - if (entity.ParentObjectType.HasValue && entity.ParentObjectType == Guid.Empty) - entity.ParentObjectType = null; - if (entity.ChildObjectType.HasValue && entity.ChildObjectType == Guid.Empty) - entity.ChildObjectType = null; - } + RelationTypeDto dto = RelationTypeFactory.BuildDto(entity); + Database.Update(dto); + + entity.ResetDirtyProperties(); } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryBase.cs index fdcf11304b07..7a1f4a2677a9 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryBase.cs @@ -1,4 +1,3 @@ -using System; using NPoco; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Persistence; @@ -6,77 +5,76 @@ using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Cms.Infrastructure.Scoping; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Base repository class for all instances +/// +public abstract class RepositoryBase : IRepository { /// - /// Base repository class for all instances + /// Initializes a new instance of the class. /// - public abstract class RepositoryBase : IRepository + protected RepositoryBase(IScopeAccessor scopeAccessor, AppCaches appCaches) { - /// - /// Initializes a new instance of the class. - /// - protected RepositoryBase(IScopeAccessor scopeAccessor, AppCaches appCaches) - { - ScopeAccessor = scopeAccessor ?? throw new ArgumentNullException(nameof(scopeAccessor)); - AppCaches = appCaches ?? throw new ArgumentNullException(nameof(appCaches)); - } + ScopeAccessor = scopeAccessor ?? throw new ArgumentNullException(nameof(scopeAccessor)); + AppCaches = appCaches ?? throw new ArgumentNullException(nameof(appCaches)); + } - /// - /// Gets the - /// - protected AppCaches AppCaches { get; } + /// + /// Gets the + /// + protected AppCaches AppCaches { get; } - /// - /// Gets the - /// - protected IScopeAccessor ScopeAccessor { get; } + /// + /// Gets the + /// + protected IScopeAccessor ScopeAccessor { get; } - /// - /// Gets the AmbientScope - /// - protected IScope AmbientScope + /// + /// Gets the AmbientScope + /// + protected IScope AmbientScope + { + get { - get + IScope? scope = ScopeAccessor.AmbientScope; + if (scope == null) { - IScope? scope = ScopeAccessor.AmbientScope; - if (scope == null) - { - throw new InvalidOperationException("Cannot run a repository without an ambient scope."); - } - - return scope; + throw new InvalidOperationException("Cannot run a repository without an ambient scope."); } + + return scope; } + } - /// - /// Gets the repository's database. - /// - protected IUmbracoDatabase Database => AmbientScope.Database; + /// + /// Gets the repository's database. + /// + protected IUmbracoDatabase Database => AmbientScope.Database; - /// - /// Gets the Sql context. - /// - protected ISqlContext SqlContext => AmbientScope.SqlContext; + /// + /// Gets the Sql context. + /// + protected ISqlContext SqlContext => AmbientScope.SqlContext; - /// - /// Gets the - /// - protected ISqlSyntaxProvider SqlSyntax => SqlContext.SqlSyntax; + /// + /// Gets the + /// + protected ISqlSyntaxProvider SqlSyntax => SqlContext.SqlSyntax; - /// - /// Creates an expression - /// - protected Sql Sql() => SqlContext.Sql(); + /// + /// Creates an expression + /// + protected Sql Sql() => SqlContext.Sql(); - /// - /// Creates a expression - /// - protected Sql Sql(string sql, params object[] args) => SqlContext.Sql(sql, args); + /// + /// Creates a expression + /// + protected Sql Sql(string sql, params object[] args) => SqlContext.Sql(sql, args); - /// - /// Creates a new query expression - /// - protected IQuery Query() => SqlContext.Query(); - } + /// + /// Creates a new query expression + /// + protected IQuery Query() => SqlContext.Query(); } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ScriptRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ScriptRepository.cs index e50f29fd872c..493f5e529df6 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ScriptRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ScriptRepository.cs @@ -1,102 +1,103 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents the Script Repository +/// +internal class ScriptRepository : FileRepository, IScriptRepository { - /// - /// Represents the Script Repository - /// - internal class ScriptRepository : FileRepository, IScriptRepository + public ScriptRepository(FileSystems fileSystems) + : base(fileSystems.ScriptsFileSystem) + { + } + + #region Implementation of IRepository + + public override IScript? Get(string? id) { - public ScriptRepository(FileSystems fileSystems) - : base(fileSystems.ScriptsFileSystem) + if (id is null || FileSystem is null) { + return null; } - #region Implementation of IRepository + // get the relative path within the filesystem + // (though... id should be relative already) + var path = FileSystem.GetRelativePath(id); - public override IScript? Get(string? id) + if (FileSystem.FileExists(path) == false) { - if (id is null || FileSystem is null) - { - return null; - } - // get the relative path within the filesystem - // (though... id should be relative already) - var path = FileSystem.GetRelativePath(id); + return null; + } - if (FileSystem.FileExists(path) == false) - return null; + // content will be lazy-loaded when required + DateTime created = FileSystem.GetCreated(path).UtcDateTime; + DateTime updated = FileSystem.GetLastModified(path).UtcDateTime; + //var content = GetFileContent(path); - // content will be lazy-loaded when required - var created = FileSystem.GetCreated(path).UtcDateTime; - var updated = FileSystem.GetLastModified(path).UtcDateTime; - //var content = GetFileContent(path); + var script = new Script(path, file => GetFileContent(file.OriginalPath)) + { + //id can be the hash + Id = path.GetHashCode(), + Key = path.EncodeAsGuid(), + //Content = content, + CreateDate = created, + UpdateDate = updated, + VirtualPath = FileSystem.GetUrl(path) + }; - var script = new Script(path, file => GetFileContent(file.OriginalPath)) - { - //id can be the hash - Id = path.GetHashCode(), - Key = path.EncodeAsGuid(), - //Content = content, - CreateDate = created, - UpdateDate = updated, - VirtualPath = FileSystem.GetUrl(path) - }; - - // reset dirty initial properties (U4-1946) - script.ResetDirtyProperties(false); - - return script; - } + // reset dirty initial properties (U4-1946) + script.ResetDirtyProperties(false); - public override void Save(IScript entity) - { - // TODO: Casting :/ Review GetFileContent and it's usages, need to look into it later - var script = (Script) entity; + return script; + } - base.Save(script); + public override void Save(IScript entity) + { + // TODO: Casting :/ Review GetFileContent and it's usages, need to look into it later + var script = (Script)entity; - // ensure that from now on, content is lazy-loaded - if (script.GetFileContent == null) - script.GetFileContent = file => GetFileContent(file.OriginalPath); - } + base.Save(script); - public override IEnumerable GetMany(params string[]? ids) + // ensure that from now on, content is lazy-loaded + if (script.GetFileContent == null) { - //ensure they are de-duplicated, easy win if people don't do this as this can cause many excess queries - ids = ids?.Distinct().ToArray(); + script.GetFileContent = file => GetFileContent(file.OriginalPath); + } + } + + public override IEnumerable GetMany(params string[]? ids) + { + //ensure they are de-duplicated, easy win if people don't do this as this can cause many excess queries + ids = ids?.Distinct().ToArray(); - if (ids?.Any() ?? false) + if (ids?.Any() ?? false) + { + foreach (var id in ids) { - foreach (var id in ids) + IScript? script = Get(id); + if (script is not null) { - IScript? script = Get(id); - if (script is not null) - { - yield return script; - } + yield return script; } } - else + } + else + { + IEnumerable files = FindAllFiles("", "*.*"); + foreach (var file in files) { - var files = FindAllFiles("", "*.*"); - foreach (var file in files) + IScript? script = Get(file); + if (script is not null) { - IScript? script = Get(file); - if (script is not null) - { - yield return script; - } + yield return script; } } } - - #endregion } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ServerRegistrationRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ServerRegistrationRepository.cs index 2a0bc9bfa17b..7e6ea4096dc0 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ServerRegistrationRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ServerRegistrationRepository.cs @@ -1,129 +1,111 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class ServerRegistrationRepository : EntityRepositoryBase, + IServerRegistrationRepository { - internal class ServerRegistrationRepository : EntityRepositoryBase, IServerRegistrationRepository + public ServerRegistrationRepository(IScopeAccessor scopeAccessor, ILogger logger) + : base(scopeAccessor, AppCaches.NoCache, logger) + { + } + + public void ClearCache() => CachePolicy.ClearAll(); + + public void DeactiveStaleServers(TimeSpan staleTimeout) + { + DateTime timeoutDate = DateTime.Now.Subtract(staleTimeout); + + Database.Update( + "SET isActive=0, isSchedulingPublisher=0 WHERE lastNotifiedDate < @timeoutDate", new + { + /*timeoutDate =*/ + timeoutDate + }); + ClearCache(); + } + + protected override IRepositoryCachePolicy CreateCachePolicy() => + // TODO: what are we doing with cache here? + // why are we using disabled cache helper up there? + // + // 7.6 says: + // note: this means that the ServerRegistrationRepository does *not* implement scoped cache, + // and this is because the repository is special and should not participate in scopes + // (cleanup in v8) + // + new FullDataSetRepositoryCachePolicy(AppCaches.RuntimeCache, ScopeAccessor, + GetEntityId, /*expires:*/ false); + + protected override int PerformCount(IQuery query) => + throw new NotSupportedException("This repository does not support this method."); + + protected override bool PerformExists(int id) => + // use the underlying GetAll which force-caches all registrations + GetMany()?.Any(x => x.Id == id) ?? false; + + protected override IServerRegistration? PerformGet(int id) => + // use the underlying GetAll which force-caches all registrations + GetMany()?.FirstOrDefault(x => x.Id == id); + + protected override IEnumerable PerformGetAll(params int[]? ids) => + Database.Fetch("WHERE id > 0") + .Select(x => ServerRegistrationFactory.BuildEntity(x)); + + protected override IEnumerable PerformGetByQuery(IQuery query) => + throw new NotSupportedException("This repository does not support this method."); + + protected override Sql GetBaseQuery(bool isCount) { - public ServerRegistrationRepository(IScopeAccessor scopeAccessor, ILogger logger) - : base(scopeAccessor, AppCaches.NoCache, logger) - { } - - protected override IRepositoryCachePolicy CreateCachePolicy() - { - // TODO: what are we doing with cache here? - // why are we using disabled cache helper up there? - // - // 7.6 says: - // note: this means that the ServerRegistrationRepository does *not* implement scoped cache, - // and this is because the repository is special and should not participate in scopes - // (cleanup in v8) - // - return new FullDataSetRepositoryCachePolicy(AppCaches.RuntimeCache, ScopeAccessor, GetEntityId, /*expires:*/ false); - } - - public void ClearCache() - { - CachePolicy.ClearAll(); - } - - protected override int PerformCount(IQuery query) - { - throw new NotSupportedException("This repository does not support this method."); - } - - protected override bool PerformExists(int id) - { - // use the underlying GetAll which force-caches all registrations - return GetMany()?.Any(x => x.Id == id) ?? false; - } - - protected override IServerRegistration? PerformGet(int id) - { - // use the underlying GetAll which force-caches all registrations - return GetMany()?.FirstOrDefault(x => x.Id == id); - } - - protected override IEnumerable PerformGetAll(params int[]? ids) - { - return Database.Fetch("WHERE id > 0") - .Select(x => ServerRegistrationFactory.BuildEntity(x)); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - throw new NotSupportedException("This repository does not support this method."); - } - - protected override Sql GetBaseQuery(bool isCount) - { - var sql = Sql(); - - sql = isCount - ? sql.SelectCount() - : sql.Select(); - - sql - .From(); - - return sql; - } - - protected override string GetBaseWhereClause() - { - return "id = @id"; - } - - protected override IEnumerable GetDeleteClauses() - { - var list = new List - { - "DELETE FROM umbracoServer WHERE id = @id" - }; - return list; - } - - protected override void PersistNewItem(IServerRegistration entity) - { - entity.AddingEntity(); - - var dto = ServerRegistrationFactory.BuildDto(entity); - - var id = Convert.ToInt32(Database.Insert(dto)); - entity.Id = id; - - entity.ResetDirtyProperties(); - } - - protected override void PersistUpdatedItem(IServerRegistration entity) - { - entity.UpdatingEntity(); - - var dto = ServerRegistrationFactory.BuildDto(entity); - - Database.Update(dto); - - entity.ResetDirtyProperties(); - } - - public void DeactiveStaleServers(TimeSpan staleTimeout) - { - var timeoutDate = DateTime.Now.Subtract(staleTimeout); - - Database.Update("SET isActive=0, isSchedulingPublisher=0 WHERE lastNotifiedDate < @timeoutDate", new { /*timeoutDate =*/ timeoutDate }); - ClearCache(); - } + Sql sql = Sql(); + + sql = isCount + ? sql.SelectCount() + : sql.Select(); + + sql + .From(); + + return sql; + } + + protected override string GetBaseWhereClause() => "id = @id"; + + protected override IEnumerable GetDeleteClauses() + { + var list = new List {"DELETE FROM umbracoServer WHERE id = @id"}; + return list; + } + + protected override void PersistNewItem(IServerRegistration entity) + { + entity.AddingEntity(); + + ServerRegistrationDto dto = ServerRegistrationFactory.BuildDto(entity); + + var id = Convert.ToInt32(Database.Insert(dto)); + entity.Id = id; + + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IServerRegistration entity) + { + entity.UpdatingEntity(); + + ServerRegistrationDto dto = ServerRegistrationFactory.BuildDto(entity); + + Database.Update(dto); + + entity.ResetDirtyProperties(); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimilarNodeName.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimilarNodeName.cs index 2621461a85f2..463263a8f0e1 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimilarNodeName.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimilarNodeName.cs @@ -1,242 +1,232 @@ -using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Text.RegularExpressions; using Umbraco.Extensions; using static Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement.SimilarNodeName; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class SimilarNodeName { - internal class SimilarNodeName + public int Id { get; set; } + public string? Name { get; set; } + + public static string? GetUniqueName(IEnumerable names, int nodeId, string? nodeName) { - public int Id { get; set; } - public string? Name { get; set; } + IEnumerable items = names + .Where(x => x.Id != nodeId) // ignore same node + .Select(x => x.Name); - public static string? GetUniqueName(IEnumerable names, int nodeId, string? nodeName) - { - var items = names - .Where(x => x.Id != nodeId) // ignore same node - .Select(x => x.Name); + var uniqueName = GetUniqueName(items, nodeName); + + return uniqueName; + } + + public static string? GetUniqueName(IEnumerable names, string? name) + { + var model = new StructuredName(name); + IEnumerable items = names + .Where(x => x?.InvariantStartsWith(model.Text) ?? false) // ignore non-matching names + .Select(x => new StructuredName(x)); - var uniqueName = GetUniqueName(items, nodeName); + // name is empty, and there are no other names with suffixes, so just return " (1)" + if (model.IsEmptyName() && !items.Any()) + { + model.Suffix = StructuredName.INITIAL_SUFFIX; - return uniqueName; + return model.FullName; } - public static string? GetUniqueName(IEnumerable names, string? name) + // name is empty, and there are other names with suffixes + if (model.IsEmptyName() && items.SuffixedNameExists()) { - var model = new StructuredName(name); - var items = names - .Where(x => x?.InvariantStartsWith(model.Text) ?? false) // ignore non-matching names - .Select(x => new StructuredName(x)); + var emptyNameSuffix = GetSuffixNumber(items); - // name is empty, and there are no other names with suffixes, so just return " (1)" - if (model.IsEmptyName() && !items.Any()) + if (emptyNameSuffix > 0) { - model.Suffix = StructuredName.INITIAL_SUFFIX; + model.Suffix = (uint?)emptyNameSuffix; return model.FullName; } + } - // name is empty, and there are other names with suffixes - if (model.IsEmptyName() && items.SuffixedNameExists()) - { - var emptyNameSuffix = GetSuffixNumber(items); - - if (emptyNameSuffix > 0) - { - model.Suffix = (uint?)emptyNameSuffix; + // no suffix - name without suffix does NOT exist - we can just use the name without suffix. + if (!model.Suffix.HasValue && !items.SimpleNameExists(model.Text)) + { + model.Suffix = StructuredName.NO_SUFFIX; - return model.FullName; - } - } + return model.FullName; + } - // no suffix - name without suffix does NOT exist - we can just use the name without suffix. - if (!model.Suffix.HasValue && !items.SimpleNameExists(model.Text)) - { - model.Suffix = StructuredName.NO_SUFFIX; + // suffix - name with suffix does NOT exist + // We can just return the full name as it is as there's no conflict. + if (model.Suffix.HasValue && !items.SimpleNameExists(model.FullName)) + { + return model.FullName; + } - return model.FullName; - } + // no suffix - name without suffix does NOT exist, AND name with suffix does NOT exist + if (!model.Suffix.HasValue && !items.SimpleNameExists(model.Text) && !items.SuffixedNameExists()) + { + model.Suffix = StructuredName.NO_SUFFIX; - // suffix - name with suffix does NOT exist - // We can just return the full name as it is as there's no conflict. - if (model.Suffix.HasValue && !items.SimpleNameExists(model.FullName)) - { - return model.FullName; - } + return model.FullName; + } - // no suffix - name without suffix does NOT exist, AND name with suffix does NOT exist - if (!model.Suffix.HasValue && !items.SimpleNameExists(model.Text) && !items.SuffixedNameExists()) - { - model.Suffix = StructuredName.NO_SUFFIX; + // no suffix - name without suffix exists, however name with suffix does NOT exist + if (!model.Suffix.HasValue && items.SimpleNameExists(model.Text) && !items.SuffixedNameExists()) + { + var firstSuffix = GetFirstSuffix(items); + model.Suffix = (uint?)firstSuffix; - return model.FullName; - } + return model.FullName; + } - // no suffix - name without suffix exists, however name with suffix does NOT exist - if (!model.Suffix.HasValue && items.SimpleNameExists(model.Text) && !items.SuffixedNameExists()) - { - var firstSuffix = GetFirstSuffix(items); - model.Suffix = (uint?)firstSuffix; + // no suffix - name without suffix exists, AND name with suffix does exist + if (!model.Suffix.HasValue && items.SimpleNameExists(model.Text) && items.SuffixedNameExists()) + { + var nextSuffix = GetSuffixNumber(items); + model.Suffix = (uint?)nextSuffix; - return model.FullName; - } + return model.FullName; + } - // no suffix - name without suffix exists, AND name with suffix does exist - if (!model.Suffix.HasValue && items.SimpleNameExists(model.Text) && items.SuffixedNameExists()) - { - var nextSuffix = GetSuffixNumber(items); - model.Suffix = (uint?)nextSuffix; + // no suffix - name without suffix does NOT exist, however name with suffix exists + if (!model.Suffix.HasValue && !items.SimpleNameExists(model.Text) && items.SuffixedNameExists()) + { + var nextSuffix = GetSuffixNumber(items); + model.Suffix = (uint?)nextSuffix; - return model.FullName; - } + return model.FullName; + } - // no suffix - name without suffix does NOT exist, however name with suffix exists - if (!model.Suffix.HasValue && !items.SimpleNameExists(model.Text) && items.SuffixedNameExists()) - { - var nextSuffix = GetSuffixNumber(items); - model.Suffix = (uint?)nextSuffix; + // has suffix - name without suffix exists + if (model.Suffix.HasValue && items.SimpleNameExists(model.Text)) + { + var nextSuffix = GetSuffixNumber(items); + model.Suffix = (uint?)nextSuffix; - return model.FullName; - } + return model.FullName; + } - // has suffix - name without suffix exists - if (model.Suffix.HasValue && items.SimpleNameExists(model.Text)) - { - var nextSuffix = GetSuffixNumber(items); - model.Suffix = (uint?)nextSuffix; + // has suffix - name without suffix does NOT exist + // a case where the user added the suffix, so add a secondary suffix + if (model.Suffix.HasValue && !items.SimpleNameExists(model.Text)) + { + model.Text = model.FullName; + model.Suffix = StructuredName.NO_SUFFIX; - return model.FullName; - } + // filter items based on full name with suffix + items = items.Where(x => x.Text.InvariantStartsWith(model.FullName)); + var secondarySuffix = GetFirstSuffix(items); + model.Suffix = (uint?)secondarySuffix; - // has suffix - name without suffix does NOT exist - // a case where the user added the suffix, so add a secondary suffix - if (model.Suffix.HasValue && !items.SimpleNameExists(model.Text)) - { - model.Text = model.FullName; - model.Suffix = StructuredName.NO_SUFFIX; + return model.FullName; + } - // filter items based on full name with suffix - items = items.Where(x => x.Text.InvariantStartsWith(model.FullName)); - var secondarySuffix = GetFirstSuffix(items); - model.Suffix = (uint?)secondarySuffix; + // has suffix - name without suffix also exists, therefore we simply increment + if (model.Suffix.HasValue && items.SimpleNameExists(model.Text)) + { + var nextSuffix = GetSuffixNumber(items); + model.Suffix = (uint?)nextSuffix; - return model.FullName; - } + return model.FullName; + } - // has suffix - name without suffix also exists, therefore we simply increment - if (model.Suffix.HasValue && items.SimpleNameExists(model.Text)) - { - var nextSuffix = GetSuffixNumber(items); - model.Suffix = (uint?)nextSuffix; + return name; + } - return model.FullName; - } + private static int GetFirstSuffix(IEnumerable items) + { + const int suffixStart = 1; - return name; + if (!items.Any(x => x.Suffix == suffixStart)) + { + // none of the suffixes are the same as suffixStart, so we can use suffixStart! + return suffixStart; } - private static int GetFirstSuffix(IEnumerable items) - { - const int suffixStart = 1; + return GetSuffixNumber(items); + } - if (!items.Any(x => x.Suffix == suffixStart)) + private static int GetSuffixNumber(IEnumerable items) + { + var current = 1; + foreach (StructuredName item in items.OrderBy(x => x.Suffix)) + { + if (item.Suffix == current) { - // none of the suffixes are the same as suffixStart, so we can use suffixStart! - return suffixStart; + current++; } - - return GetSuffixNumber(items); - } - - private static int GetSuffixNumber(IEnumerable items) - { - int current = 1; - foreach (var item in items.OrderBy(x => x.Suffix)) + else if (item.Suffix > current) { - if (item.Suffix == current) - { - current++; - } - else if (item.Suffix > current) - { - // do nothing - we found our number! - // eg. when suffixes are 1 & 3, then this method is required to generate 2 - break; - } + // do nothing - we found our number! + // eg. when suffixes are 1 & 3, then this method is required to generate 2 + break; } - - return current; } - internal class StructuredName + return current; + } + + internal class StructuredName + { + private const string SPACE_CHARACTER = " "; + private const string SUFFIXED_PATTERN = @"(.*) \(([1-9]\d*)\)$"; + internal const uint INITIAL_SUFFIX = 1; + internal static readonly uint? NO_SUFFIX = default; + + internal StructuredName(string? name) { - const string SPACE_CHARACTER = " "; - const string SUFFIXED_PATTERN = @"(.*) \(([1-9]\d*)\)$"; - internal const uint INITIAL_SUFFIX = 1; - internal static readonly uint? NO_SUFFIX = default; - - internal string Text { get; set; } - internal uint? Suffix { get; set; } - public string FullName + if (string.IsNullOrWhiteSpace(name)) { - get - { - string text = (Text == SPACE_CHARACTER) ? Text.Trim() : Text; + Text = SPACE_CHARACTER; - return Suffix > 0 ? $"{text} ({Suffix})" : text; - } + return; } - internal StructuredName(string? name) + var rg = new Regex(SUFFIXED_PATTERN); + MatchCollection matches = rg.Matches(name); + if (matches.Count > 0) { - if (string.IsNullOrWhiteSpace(name)) - { - Text = SPACE_CHARACTER; - - return; - } - - var rg = new Regex(SUFFIXED_PATTERN); - var matches = rg.Matches(name); - if (matches.Count > 0) - { - var match = matches[0]; - Text = match.Groups[1].Value; - int number = int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out number) ? number : 0; - Suffix = (uint?)(number); - - return; - } - else - { - Text = name; - } - } + Match match = matches[0]; + Text = match.Groups[1].Value; + int number = int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, + out number) + ? number + : 0; + Suffix = (uint?)number; - internal bool IsEmptyName() - { - return string.IsNullOrWhiteSpace(Text); + return; } - } - } - internal static class ListExtensions - { - internal static bool Contains(this IEnumerable items, StructuredName model) - { - return items.Any(x => x.FullName.InvariantEquals(model.FullName)); + Text = name; } - internal static bool SimpleNameExists(this IEnumerable items, string name) - { - return items.Any(x => x.FullName.InvariantEquals(name)); - } + internal string Text { get; set; } + internal uint? Suffix { get; set; } - internal static bool SuffixedNameExists(this IEnumerable items) + public string FullName { - return items.Any(x => x.Suffix.HasValue); + get + { + var text = Text == SPACE_CHARACTER ? Text.Trim() : Text; + + return Suffix > 0 ? $"{text} ({Suffix})" : text; + } } + + internal bool IsEmptyName() => string.IsNullOrWhiteSpace(Text); } } + +internal static class ListExtensions +{ + internal static bool Contains(this IEnumerable items, StructuredName model) => + items.Any(x => x.FullName.InvariantEquals(model.FullName)); + + internal static bool SimpleNameExists(this IEnumerable items, string name) => + items.Any(x => x.FullName.InvariantEquals(name)); + + internal static bool SuffixedNameExists(this IEnumerable items) => + items.Any(x => x.Suffix.HasValue); +} diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimpleGetRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimpleGetRepository.cs index bf798b2845bb..c855b84acdb9 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimpleGetRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimpleGetRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core.Cache; @@ -10,87 +7,84 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +// TODO: Obsolete this, change all implementations of this like in Dictionary to just use custom Cache policies like in the member repository. + +/// +/// Simple abstract ReadOnly repository used to simply have PerformGet and PeformGetAll with an underlying cache +/// +internal abstract class SimpleGetRepository : EntityRepositoryBase + where TEntity : class, IEntity + where TDto : class { - // TODO: Obsolete this, change all implementations of this like in Dictionary to just use custom Cache policies like in the member repository. - - /// - /// Simple abstract ReadOnly repository used to simply have PerformGet and PeformGetAll with an underlying cache - /// - internal abstract class SimpleGetRepository : EntityRepositoryBase - where TEntity : class, IEntity - where TDto: class + protected SimpleGetRepository(IScopeAccessor scopeAccessor, AppCaches cache, + ILogger> logger) + : base(scopeAccessor, cache, logger) { - protected SimpleGetRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger> logger) - : base(scopeAccessor, cache, logger) - { } + } - protected abstract TEntity ConvertToEntity(TDto dto); - protected abstract object GetBaseWhereClauseArguments(TId? id); - protected abstract string GetWhereInClauseForGetAll(); + protected abstract TEntity ConvertToEntity(TDto dto); + protected abstract object GetBaseWhereClauseArguments(TId? id); + protected abstract string GetWhereInClauseForGetAll(); - protected virtual IEnumerable PerformFetch(Sql sql) - { - return Database.Fetch(sql); - } + protected virtual IEnumerable PerformFetch(Sql sql) => Database.Fetch(sql); - protected override TEntity? PerformGet(TId? id) + protected override TEntity? PerformGet(TId? id) + { + Sql sql = GetBaseQuery(false); + sql.Where(GetBaseWhereClause(), GetBaseWhereClauseArguments(id)); + + TDto? dto = PerformFetch(sql).FirstOrDefault(); + if (dto == null) { - var sql = GetBaseQuery(false); - sql.Where(GetBaseWhereClause(), GetBaseWhereClauseArguments(id)); + return null; + } - var dto = PerformFetch(sql).FirstOrDefault(); - if (dto == null) - return null; + TEntity entity = ConvertToEntity(dto); - var entity = ConvertToEntity(dto); + if (entity is EntityBase dirtyEntity) + { + // reset dirty initial properties (U4-1946) + dirtyEntity.ResetDirtyProperties(false); + } - if (entity is EntityBase dirtyEntity) - { - // reset dirty initial properties (U4-1946) - dirtyEntity.ResetDirtyProperties(false); - } + return entity; + } - return entity; - } + protected override IEnumerable PerformGetAll(params TId[]? ids) + { + Sql sql = Sql().From(); - protected override IEnumerable PerformGetAll(params TId[]? ids) + if (ids?.Any() ?? false) { - var sql = Sql().From(); - - if (ids?.Any() ?? false) + sql.Where(GetWhereInClauseForGetAll(), new { - sql.Where(GetWhereInClauseForGetAll(), new { /*ids =*/ ids }); - } - - return Database.Fetch(sql).Select(ConvertToEntity); + /*ids =*/ + ids + }); } - protected sealed override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - return Database.Fetch(sql).Select(ConvertToEntity); - } + return Database.Fetch(sql).Select(ConvertToEntity); + } - #region Not implemented and not required + protected sealed override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + return Database.Fetch(sql).Select(ConvertToEntity); + } - protected sealed override IEnumerable GetDeleteClauses() - { - throw new InvalidOperationException("This method won't be implemented."); - } + #region Not implemented and not required - protected sealed override void PersistNewItem(TEntity entity) - { - throw new InvalidOperationException("This method won't be implemented."); - } + protected sealed override IEnumerable GetDeleteClauses() => + throw new InvalidOperationException("This method won't be implemented."); - protected sealed override void PersistUpdatedItem(TEntity entity) - { - throw new InvalidOperationException("This method won't be implemented."); - } + protected sealed override void PersistNewItem(TEntity entity) => + throw new InvalidOperationException("This method won't be implemented."); - #endregion - } + protected sealed override void PersistUpdatedItem(TEntity entity) => + throw new InvalidOperationException("This method won't be implemented."); + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/StylesheetRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/StylesheetRepository.cs index d22e54e76c4b..ca14a71503e5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/StylesheetRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/StylesheetRepository.cs @@ -1,115 +1,117 @@ -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents the Stylesheet Repository +/// +internal class StylesheetRepository : FileRepository, IStylesheetRepository { - /// - /// Represents the Stylesheet Repository - /// - internal class StylesheetRepository : FileRepository, IStylesheetRepository + public StylesheetRepository(FileSystems fileSystems) + : base(fileSystems.StylesheetsFileSystem) { - public StylesheetRepository(FileSystems fileSystems) - : base(fileSystems.StylesheetsFileSystem) - { - } + } - #region Overrides of FileRepository + #region Overrides of FileRepository - public override IStylesheet? Get(string? id) + public override IStylesheet? Get(string? id) + { + if (id is null || FileSystem is null) { - if (id is null || FileSystem is null) - { - return null; - } - // get the relative path within the filesystem - // (though... id should be relative already) - var path = FileSystem.GetRelativePath(id); + return null; + } - path = path.EnsureEndsWith(".css"); + // get the relative path within the filesystem + // (though... id should be relative already) + var path = FileSystem.GetRelativePath(id); - // if the css directory is changed, references to the old path can still exist (ie in RTE config) - // these old references will throw an error, which breaks the RTE - // try-catch here makes the request fail silently, and allows RTE to load correctly - try - { - if (FileSystem.FileExists(path) == false) - return null; - } catch + path = path.EnsureEndsWith(".css"); + + // if the css directory is changed, references to the old path can still exist (ie in RTE config) + // these old references will throw an error, which breaks the RTE + // try-catch here makes the request fail silently, and allows RTE to load correctly + try + { + if (FileSystem.FileExists(path) == false) { return null; } + } + catch + { + return null; + } - // content will be lazy-loaded when required - var created = FileSystem.GetCreated(path).UtcDateTime; - var updated = FileSystem.GetLastModified(path).UtcDateTime; - //var content = GetFileContent(path); + // content will be lazy-loaded when required + DateTime created = FileSystem.GetCreated(path).UtcDateTime; + DateTime updated = FileSystem.GetLastModified(path).UtcDateTime; + //var content = GetFileContent(path); - var stylesheet = new Stylesheet(path, file => GetFileContent(file.OriginalPath)) - { - //Content = content, - Key = path.EncodeAsGuid(), - CreateDate = created, - UpdateDate = updated, - Id = path.GetHashCode(), - VirtualPath = FileSystem.GetUrl(path) - }; - - // reset dirty initial properties (U4-1946) - stylesheet.ResetDirtyProperties(false); + var stylesheet = new Stylesheet(path, file => GetFileContent(file.OriginalPath)) + { + //Content = content, + Key = path.EncodeAsGuid(), + CreateDate = created, + UpdateDate = updated, + Id = path.GetHashCode(), + VirtualPath = FileSystem.GetUrl(path) + }; + + // reset dirty initial properties (U4-1946) + stylesheet.ResetDirtyProperties(false); + + return stylesheet; + } - return stylesheet; + public override void Save(IStylesheet entity) + { + // TODO: Casting :/ Review GetFileContent and it's usages, need to look into it later + var stylesheet = (Stylesheet)entity; - } + base.Save(stylesheet); - public override void Save(IStylesheet entity) + // ensure that from now on, content is lazy-loaded + if (stylesheet.GetFileContent == null) { - // TODO: Casting :/ Review GetFileContent and it's usages, need to look into it later - var stylesheet = (Stylesheet)entity; - - base.Save(stylesheet); - - // ensure that from now on, content is lazy-loaded - if (stylesheet.GetFileContent == null) - stylesheet.GetFileContent = file => GetFileContent(file.OriginalPath); + stylesheet.GetFileContent = file => GetFileContent(file.OriginalPath); } + } - public override IEnumerable GetMany(params string[]? ids) - { - //ensure they are de-duplicated, easy win if people don't do this as this can cause many excess queries - ids = ids? - .Select(x => x.EnsureEndsWith(".css")) - .Distinct() - .ToArray(); + public override IEnumerable GetMany(params string[]? ids) + { + //ensure they are de-duplicated, easy win if people don't do this as this can cause many excess queries + ids = ids? + .Select(x => x.EnsureEndsWith(".css")) + .Distinct() + .ToArray(); - if (ids?.Any() ?? false) + if (ids?.Any() ?? false) + { + foreach (var id in ids) { - foreach (var id in ids) + IStylesheet? stylesheet = Get(id); + if (stylesheet is not null) { - IStylesheet? stylesheet = Get(id); - if (stylesheet is not null) - { - yield return stylesheet; - } + yield return stylesheet; } } - else + } + else + { + IEnumerable files = FindAllFiles("", "*.css"); + foreach (var file in files) { - var files = FindAllFiles("", "*.css"); - foreach (var file in files) + IStylesheet? stylesheet = Get(file); + if (stylesheet is not null) { - IStylesheet? stylesheet = Get(file); - if (stylesheet is not null) - { - yield return stylesheet; - } + yield return stylesheet; } } } - - #endregion } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs index ca171a9b01c0..1a12770d5f5a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text; using Microsoft.Extensions.Logging; using NPoco; @@ -16,131 +13,131 @@ using Umbraco.Extensions; using static Umbraco.Cms.Core.Persistence.SqlExtensionsStatics; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class TagRepository : EntityRepositoryBase, ITagRepository { - internal class TagRepository : EntityRepositoryBase, ITagRepository + public TagRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) { - public TagRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { - } + } - #region Manage Tag Entities + #region Manage Tag Entities - /// - protected override ITag? PerformGet(int id) - { - Sql sql = Sql().Select().From().Where(x => x.Id == id); - TagDto? dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); - return dto == null ? null : TagFactory.BuildEntity(dto); - } + /// + protected override ITag? PerformGet(int id) + { + Sql sql = Sql().Select().From().Where(x => x.Id == id); + TagDto? dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); + return dto == null ? null : TagFactory.BuildEntity(dto); + } - /// - protected override IEnumerable PerformGetAll(params int[]? ids) - { - IEnumerable dtos = ids?.Length == 0 - ? Database.Fetch(Sql().Select().From()) - : Database.FetchByGroups(ids!, Constants.Sql.MaxParameterCount, - batch => Sql().Select().From().WhereIn(x => x.Id, batch)); + /// + protected override IEnumerable PerformGetAll(params int[]? ids) + { + IEnumerable dtos = ids?.Length == 0 + ? Database.Fetch(Sql().Select().From()) + : Database.FetchByGroups(ids!, Constants.Sql.MaxParameterCount, + batch => Sql().Select().From().WhereIn(x => x.Id, batch)); - return dtos.Select(TagFactory.BuildEntity).ToList(); - } + return dtos.Select(TagFactory.BuildEntity).ToList(); + } - /// - protected override IEnumerable PerformGetByQuery(IQuery query) - { - Sql sql = Sql().Select().From(); - var translator = new SqlTranslator(sql, query); - sql = translator.Translate(); + /// + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sql = Sql().Select().From(); + var translator = new SqlTranslator(sql, query); + sql = translator.Translate(); - return Database.Fetch(sql).Select(TagFactory.BuildEntity).ToList(); - } + return Database.Fetch(sql).Select(TagFactory.BuildEntity).ToList(); + } - /// - protected override Sql GetBaseQuery(bool isCount) => - isCount ? Sql().SelectCount().From() : GetBaseQuery(); + /// + protected override Sql GetBaseQuery(bool isCount) => + isCount ? Sql().SelectCount().From() : GetBaseQuery(); - private Sql GetBaseQuery() => Sql().Select().From(); + private Sql GetBaseQuery() => Sql().Select().From(); - /// - protected override string GetBaseWhereClause() => "id = @id"; + /// + protected override string GetBaseWhereClause() => "id = @id"; - /// - protected override IEnumerable GetDeleteClauses() + /// + protected override IEnumerable GetDeleteClauses() + { + var list = new List { - var list = new List - { - "DELETE FROM cmsTagRelationship WHERE tagId = @id", "DELETE FROM cmsTags WHERE id = @id" - }; - return list; - } + "DELETE FROM cmsTagRelationship WHERE tagId = @id", "DELETE FROM cmsTags WHERE id = @id" + }; + return list; + } - /// - protected override void PersistNewItem(ITag entity) - { - entity.AddingEntity(); + /// + protected override void PersistNewItem(ITag entity) + { + entity.AddingEntity(); - TagDto dto = TagFactory.BuildDto(entity); - var id = Convert.ToInt32(Database.Insert(dto)); - entity.Id = id; + TagDto dto = TagFactory.BuildDto(entity); + var id = Convert.ToInt32(Database.Insert(dto)); + entity.Id = id; - entity.ResetDirtyProperties(); - } + entity.ResetDirtyProperties(); + } - /// - protected override void PersistUpdatedItem(ITag entity) - { - entity.UpdatingEntity(); + /// + protected override void PersistUpdatedItem(ITag entity) + { + entity.UpdatingEntity(); - TagDto dto = TagFactory.BuildDto(entity); - Database.Update(dto); + TagDto dto = TagFactory.BuildDto(entity); + Database.Update(dto); - entity.ResetDirtyProperties(); - } + entity.ResetDirtyProperties(); + } - #endregion + #endregion - #region Assign and Remove Tags + #region Assign and Remove Tags - /// - // only invoked from ContentRepositoryBase with all cultures + replaceTags being true - public void Assign(int contentId, int propertyTypeId, IEnumerable tags, bool replaceTags = true) - { - // to no-duplicates array - ITag[] tagsA = tags.Distinct(new TagComparer()).ToArray(); + /// + // only invoked from ContentRepositoryBase with all cultures + replaceTags being true + public void Assign(int contentId, int propertyTypeId, IEnumerable tags, bool replaceTags = true) + { + // to no-duplicates array + ITag[] tagsA = tags.Distinct(new TagComparer()).ToArray(); - // replacing = clear all - if (replaceTags) - { - Sql sql0 = Sql().Delete() - .Where(x => x.NodeId == contentId && x.PropertyTypeId == propertyTypeId); - Database.Execute(sql0); - } + // replacing = clear all + if (replaceTags) + { + Sql sql0 = Sql().Delete() + .Where(x => x.NodeId == contentId && x.PropertyTypeId == propertyTypeId); + Database.Execute(sql0); + } - // no tags? nothing else to do - if (tagsA.Length == 0) - { - return; - } + // no tags? nothing else to do + if (tagsA.Length == 0) + { + return; + } - // tags - // using some clever logic (?) to insert tags that don't exist in 1 query - // must coalesce languageId because equality of NULLs does not exist + // tags + // using some clever logic (?) to insert tags that don't exist in 1 query + // must coalesce languageId because equality of NULLs does not exist - var tagSetSql = GetTagSet(tagsA); - var group = SqlSyntax.GetQuotedColumnName("group"); + var tagSetSql = GetTagSet(tagsA); + var group = SqlSyntax.GetQuotedColumnName("group"); - // insert tags - var sql1 = $@"INSERT INTO cmsTags (tag, {group}, languageId) + // insert tags + var sql1 = $@"INSERT INTO cmsTags (tag, {group}, languageId) SELECT tagSet.tag, tagSet.{group}, tagSet.languageId FROM {tagSetSql} LEFT OUTER JOIN cmsTags ON (tagSet.tag = cmsTags.tag AND tagSet.{group} = cmsTags.{group} AND COALESCE(tagSet.languageId, -1) = COALESCE(cmsTags.languageId, -1)) WHERE cmsTags.id IS NULL"; - Database.Execute(sql1); + Database.Execute(sql1); - // insert relations - var sql2 = $@"INSERT INTO cmsTagRelationship (nodeId, propertyTypeId, tagId) + // insert relations + var sql2 = $@"INSERT INTO cmsTagRelationship (nodeId, propertyTypeId, tagId) SELECT {contentId}, {propertyTypeId}, tagSet2.Id FROM ( SELECT t.Id @@ -150,417 +147,416 @@ INNER JOIN cmsTags as t ON (tagSet.tag = t.tag AND tagSet.{group} = t.{group} AN LEFT OUTER JOIN cmsTagRelationship r ON (tagSet2.id = r.tagId AND r.nodeId = {contentId} AND r.propertyTypeID = {propertyTypeId}) WHERE r.tagId IS NULL"; - Database.Execute(sql2); - } + Database.Execute(sql2); + } - /// - // only invoked from tests - public void Remove(int contentId, int propertyTypeId, IEnumerable tags) - { - var tagSetSql = GetTagSet(tags); - var group = SqlSyntax.GetQuotedColumnName("group"); + /// + // only invoked from tests + public void Remove(int contentId, int propertyTypeId, IEnumerable tags) + { + var tagSetSql = GetTagSet(tags); + var group = SqlSyntax.GetQuotedColumnName("group"); - var deleteSql = - $@"DELETE FROM cmsTagRelationship WHERE nodeId = {contentId} AND propertyTypeId = {propertyTypeId} AND tagId IN ( + var deleteSql = + $@"DELETE FROM cmsTagRelationship WHERE nodeId = {contentId} AND propertyTypeId = {propertyTypeId} AND tagId IN ( SELECT id FROM cmsTags INNER JOIN {tagSetSql} ON ( tagSet.tag = cmsTags.tag AND tagSet.{group} = cmsTags.{group} AND COALESCE(tagSet.languageId, -1) = COALESCE(cmsTags.languageId, -1) ) )"; - Database.Execute(deleteSql); - } + Database.Execute(deleteSql); + } - /// - public void RemoveAll(int contentId, int propertyTypeId) => - Database.Execute( - "DELETE FROM cmsTagRelationship WHERE nodeId = @nodeId AND propertyTypeId = @propertyTypeId", - new { nodeId = contentId, propertyTypeId }); - - /// - public void RemoveAll(int contentId) => - Database.Execute("DELETE FROM cmsTagRelationship WHERE nodeId = @nodeId", - new { nodeId = contentId }); - - // this is a clever way to produce an SQL statement like this: - // - // ( - // SELECT 'Spacesdd' AS Tag, 'default' AS [group] - // UNION - // SELECT 'Cool' AS tag, 'default' AS [group] - // ) AS tagSet - // - // which we can then use to reduce queries - // - private string GetTagSet(IEnumerable tags) - { - var sql = new StringBuilder(); - var group = SqlSyntax.GetQuotedColumnName("group"); - var first = true; + /// + public void RemoveAll(int contentId, int propertyTypeId) => + Database.Execute( + "DELETE FROM cmsTagRelationship WHERE nodeId = @nodeId AND propertyTypeId = @propertyTypeId", + new {nodeId = contentId, propertyTypeId}); + + /// + public void RemoveAll(int contentId) => + Database.Execute("DELETE FROM cmsTagRelationship WHERE nodeId = @nodeId", + new {nodeId = contentId}); + + // this is a clever way to produce an SQL statement like this: + // + // ( + // SELECT 'Spacesdd' AS Tag, 'default' AS [group] + // UNION + // SELECT 'Cool' AS tag, 'default' AS [group] + // ) AS tagSet + // + // which we can then use to reduce queries + // + private string GetTagSet(IEnumerable tags) + { + var sql = new StringBuilder(); + var group = SqlSyntax.GetQuotedColumnName("group"); + var first = true; - sql.Append("("); + sql.Append("("); - foreach (ITag tag in tags) + foreach (ITag tag in tags) + { + if (first) { - if (first) - { - first = false; - } - else - { - sql.Append(" UNION "); - } - - // HACK: SQLite (or rather SQL server setup was a hack) - if (SqlContext.DatabaseType.IsSqlServer()) - { - sql.Append("SELECT N'"); - } - else - { - sql.Append("SELECT '"); - } - - sql.Append(SqlSyntax.EscapeString(tag.Text)); - sql.Append("' AS tag, '"); - sql.Append(SqlSyntax.EscapeString(tag.Group)); - sql.Append("' AS "); - sql.Append(group); - sql.Append(" , "); - if (tag.LanguageId.HasValue) - { - sql.Append(tag.LanguageId); - } - else - { - sql.Append("NULL"); - } + first = false; + } + else + { + sql.Append(" UNION "); + } - sql.Append(" AS languageId"); + // HACK: SQLite (or rather SQL server setup was a hack) + if (SqlContext.DatabaseType.IsSqlServer()) + { + sql.Append("SELECT N'"); + } + else + { + sql.Append("SELECT '"); } - sql.Append(") AS tagSet"); + sql.Append(SqlSyntax.EscapeString(tag.Text)); + sql.Append("' AS tag, '"); + sql.Append(SqlSyntax.EscapeString(tag.Group)); + sql.Append("' AS "); + sql.Append(group); + sql.Append(" , "); + if (tag.LanguageId.HasValue) + { + sql.Append(tag.LanguageId); + } + else + { + sql.Append("NULL"); + } - return sql.ToString(); + sql.Append(" AS languageId"); } - // used to run Distinct() on tags - private class TagComparer : IEqualityComparer - { - public bool Equals(ITag? x, ITag? y) => - ReferenceEquals(x, y) // takes care of both being null - || (x != null && y != null && x.Text == y.Text && x.Group == y.Group && x.LanguageId == y.LanguageId); + sql.Append(") AS tagSet"); + + return sql.ToString(); + } + + // used to run Distinct() on tags + private class TagComparer : IEqualityComparer + { + public bool Equals(ITag? x, ITag? y) => + ReferenceEquals(x, y) // takes care of both being null + || (x != null && y != null && x.Text == y.Text && x.Group == y.Group && x.LanguageId == y.LanguageId); - public int GetHashCode(ITag obj) + public int GetHashCode(ITag obj) + { + unchecked { - unchecked - { - var h = obj.Text?.GetHashCode() ?? 1; - h = (h * 397) ^ obj.Group?.GetHashCode() ?? 0; - h = (h * 397) ^ (obj.LanguageId?.GetHashCode() ?? 0); - return h; - } + var h = obj.Text?.GetHashCode() ?? 1; + h = (h * 397) ^ obj.Group?.GetHashCode() ?? 0; + h = (h * 397) ^ (obj.LanguageId?.GetHashCode() ?? 0); + return h; } } + } - #endregion + #endregion - #region Queries + #region Queries - // TODO: consider caching implications - // add lookups for parentId or path (ie get content in tag group, that are descendants of x) + // TODO: consider caching implications + // add lookups for parentId or path (ie get content in tag group, that are descendants of x) - // ReSharper disable once ClassNeverInstantiated.Local - // ReSharper disable UnusedAutoPropertyAccessor.Local - private class TaggedEntityDto - { - public int NodeId { get; set; } - public string? PropertyTypeAlias { get; set; } - public int PropertyTypeId { get; set; } - public int TagId { get; set; } - public string TagText { get; set; } = null!; - public string TagGroup { get; set; } = null!; - public int? TagLanguage { get; set; } - } - // ReSharper restore UnusedAutoPropertyAccessor.Local + // ReSharper disable once ClassNeverInstantiated.Local + // ReSharper disable UnusedAutoPropertyAccessor.Local + private class TaggedEntityDto + { + public int NodeId { get; set; } + public string? PropertyTypeAlias { get; set; } + public int PropertyTypeId { get; set; } + public int TagId { get; set; } + public string TagText { get; } = null!; + public string TagGroup { get; } = null!; + public int? TagLanguage { get; set; } + } + // ReSharper restore UnusedAutoPropertyAccessor.Local - /// - public TaggedEntity? GetTaggedEntityByKey(Guid key) - { - Sql sql = GetTaggedEntitiesSql(TaggableObjectTypes.All, "*"); + /// + public TaggedEntity? GetTaggedEntityByKey(Guid key) + { + Sql sql = GetTaggedEntitiesSql(TaggableObjectTypes.All, "*"); - sql = sql - .Where(dto => dto.UniqueId == key); + sql = sql + .Where(dto => dto.UniqueId == key); - return Map(Database.Fetch(sql)).FirstOrDefault(); - } + return Map(Database.Fetch(sql)).FirstOrDefault(); + } - /// - public TaggedEntity? GetTaggedEntityById(int id) - { - Sql sql = GetTaggedEntitiesSql(TaggableObjectTypes.All, "*"); + /// + public TaggedEntity? GetTaggedEntityById(int id) + { + Sql sql = GetTaggedEntitiesSql(TaggableObjectTypes.All, "*"); - sql = sql - .Where(dto => dto.NodeId == id); + sql = sql + .Where(dto => dto.NodeId == id); - return Map(Database.Fetch(sql)).FirstOrDefault(); - } + return Map(Database.Fetch(sql)).FirstOrDefault(); + } - /// - public IEnumerable GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string group, - string? culture = null) - { - Sql sql = GetTaggedEntitiesSql(objectType, culture); + /// + public IEnumerable GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string group, + string? culture = null) + { + Sql sql = GetTaggedEntitiesSql(objectType, culture); - sql = sql - .Where(x => x.Group == group); + sql = sql + .Where(x => x.Group == group); - return Map(Database.Fetch(sql)); - } + return Map(Database.Fetch(sql)); + } - /// - public IEnumerable GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, - string? group = null, string? culture = null) - { - Sql sql = GetTaggedEntitiesSql(objectType, culture); + /// + public IEnumerable GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, + string? group = null, string? culture = null) + { + Sql sql = GetTaggedEntitiesSql(objectType, culture); + sql = sql + .Where(dto => dto.Text == tag); + + if (group.IsNullOrWhiteSpace() == false) + { sql = sql - .Where(dto => dto.Text == tag); + .Where(dto => dto.Group == group); + } - if (group.IsNullOrWhiteSpace() == false) - { - sql = sql - .Where(dto => dto.Group == group); - } + return Map(Database.Fetch(sql)); + } - return Map(Database.Fetch(sql)); + private Sql GetTaggedEntitiesSql(TaggableObjectTypes objectType, string? culture) + { + Sql sql = Sql() + .Select(x => Alias(x.NodeId, "NodeId")) + .AndSelect(x => Alias(x.Alias, "PropertyTypeAlias"), + x => Alias(x.Id, "PropertyTypeId")) + .AndSelect(x => Alias(x.Id, "TagId"), x => Alias(x.Text, "TagText"), + x => Alias(x.Group, "TagGroup"), x => Alias(x.LanguageId, "TagLanguage")) + .From() + .InnerJoin().On((tag, rel) => tag.Id == rel.TagId) + .InnerJoin() + .On((rel, content) => rel.NodeId == content.NodeId) + .InnerJoin() + .On((rel, prop) => rel.PropertyTypeId == prop.Id) + .InnerJoin().On((content, node) => content.NodeId == node.NodeId); + + if (culture == null) + { + sql = sql + .Where(dto => dto.LanguageId == null); } - - private Sql GetTaggedEntitiesSql(TaggableObjectTypes objectType, string? culture) + else if (culture != "*") { - Sql sql = Sql() - .Select(x => Alias(x.NodeId, "NodeId")) - .AndSelect(x => Alias(x.Alias, "PropertyTypeAlias"), - x => Alias(x.Id, "PropertyTypeId")) - .AndSelect(x => Alias(x.Id, "TagId"), x => Alias(x.Text, "TagText"), - x => Alias(x.Group, "TagGroup"), x => Alias(x.LanguageId, "TagLanguage")) - .From() - .InnerJoin().On((tag, rel) => tag.Id == rel.TagId) - .InnerJoin() - .On((rel, content) => rel.NodeId == content.NodeId) - .InnerJoin() - .On((rel, prop) => rel.PropertyTypeId == prop.Id) - .InnerJoin().On((content, node) => content.NodeId == node.NodeId); - - if (culture == null) - { - sql = sql - .Where(dto => dto.LanguageId == null); - } - else if (culture != "*") - { - sql = sql - .InnerJoin().On((tag, lang) => tag.LanguageId == lang.Id) - .Where(x => x.IsoCode == culture); - } - - if (objectType != TaggableObjectTypes.All) - { - Guid nodeObjectType = GetNodeObjectType(objectType); - sql = sql.Where(dto => dto.NodeObjectType == nodeObjectType); - } + sql = sql + .InnerJoin().On((tag, lang) => tag.LanguageId == lang.Id) + .Where(x => x.IsoCode == culture); + } - return sql; + if (objectType != TaggableObjectTypes.All) + { + Guid nodeObjectType = GetNodeObjectType(objectType); + sql = sql.Where(dto => dto.NodeObjectType == nodeObjectType); } - private static IEnumerable Map(IEnumerable dtos) => - dtos.GroupBy(x => x.NodeId).Select(dtosForNode => + return sql; + } + + private static IEnumerable Map(IEnumerable dtos) => + dtos.GroupBy(x => x.NodeId).Select(dtosForNode => + { + var taggedProperties = dtosForNode.GroupBy(x => x.PropertyTypeId).Select(dtosForProperty => { - var taggedProperties = dtosForNode.GroupBy(x => x.PropertyTypeId).Select(dtosForProperty => + string? propertyTypeAlias = null; + var tags = dtosForProperty.Select(dto => { - string? propertyTypeAlias = null; - var tags = dtosForProperty.Select(dto => - { - propertyTypeAlias = dto.PropertyTypeAlias; - return new Tag(dto.TagId, dto.TagGroup, dto.TagText, dto.TagLanguage); - }).ToList(); - return new TaggedProperty(dtosForProperty.Key, propertyTypeAlias, tags); + propertyTypeAlias = dto.PropertyTypeAlias; + return new Tag(dto.TagId, dto.TagGroup, dto.TagText, dto.TagLanguage); }).ToList(); - - return new TaggedEntity(dtosForNode.Key, taggedProperties); + return new TaggedProperty(dtosForProperty.Key, propertyTypeAlias, tags); }).ToList(); - /// - public IEnumerable GetTagsForEntityType(TaggableObjectTypes objectType, string? group = null, - string? culture = null) - { - Sql sql = GetTagsSql(culture, true); - - AddTagsSqlWhere(sql, culture); + return new TaggedEntity(dtosForNode.Key, taggedProperties); + }).ToList(); - if (objectType != TaggableObjectTypes.All) - { - Guid nodeObjectType = GetNodeObjectType(objectType); - sql = sql - .Where(dto => dto.NodeObjectType == nodeObjectType); - } + /// + public IEnumerable GetTagsForEntityType(TaggableObjectTypes objectType, string? group = null, + string? culture = null) + { + Sql sql = GetTagsSql(culture, true); - if (group.IsNullOrWhiteSpace() == false) - { - sql = sql - .Where(dto => dto.Group == group); - } + AddTagsSqlWhere(sql, culture); + if (objectType != TaggableObjectTypes.All) + { + Guid nodeObjectType = GetNodeObjectType(objectType); sql = sql - .GroupBy(x => x.Id, x => x.Text, x => x.Group, x => x.LanguageId); - - return ExecuteTagsQuery(sql); + .Where(dto => dto.NodeObjectType == nodeObjectType); } - /// - public IEnumerable GetTagsForEntity(int contentId, string? group = null, string? culture = null) + if (group.IsNullOrWhiteSpace() == false) { - Sql sql = GetTagsSql(culture); + sql = sql + .Where(dto => dto.Group == group); + } - AddTagsSqlWhere(sql, culture); + sql = sql + .GroupBy(x => x.Id, x => x.Text, x => x.Group, x => x.LanguageId); - sql = sql - .Where(dto => dto.NodeId == contentId); + return ExecuteTagsQuery(sql); + } - if (group.IsNullOrWhiteSpace() == false) - { - sql = sql - .Where(dto => dto.Group == group); - } + /// + public IEnumerable GetTagsForEntity(int contentId, string? group = null, string? culture = null) + { + Sql sql = GetTagsSql(culture); - return ExecuteTagsQuery(sql); - } + AddTagsSqlWhere(sql, culture); + + sql = sql + .Where(dto => dto.NodeId == contentId); - /// - public IEnumerable GetTagsForEntity(Guid contentId, string? group = null, string? culture = null) + if (group.IsNullOrWhiteSpace() == false) { - Sql sql = GetTagsSql(culture); + sql = sql + .Where(dto => dto.Group == group); + } - AddTagsSqlWhere(sql, culture); + return ExecuteTagsQuery(sql); + } - sql = sql - .Where(dto => dto.UniqueId == contentId); + /// + public IEnumerable GetTagsForEntity(Guid contentId, string? group = null, string? culture = null) + { + Sql sql = GetTagsSql(culture); - if (group.IsNullOrWhiteSpace() == false) - { - sql = sql - .Where(dto => dto.Group == group); - } + AddTagsSqlWhere(sql, culture); - return ExecuteTagsQuery(sql); - } + sql = sql + .Where(dto => dto.UniqueId == contentId); - /// - public IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string? group = null, - string? culture = null) + if (group.IsNullOrWhiteSpace() == false) { - Sql sql = GetTagsSql(culture); - sql = sql - .InnerJoin() - .On((prop, rel) => prop.Id == rel.PropertyTypeId) - .Where(x => x.NodeId == contentId) - .Where(x => x.Alias == propertyTypeAlias); + .Where(dto => dto.Group == group); + } - AddTagsSqlWhere(sql, culture); + return ExecuteTagsQuery(sql); + } - if (group.IsNullOrWhiteSpace() == false) - { - sql = sql - .Where(dto => dto.Group == group); - } + /// + public IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string? group = null, + string? culture = null) + { + Sql sql = GetTagsSql(culture); - return ExecuteTagsQuery(sql); - } + sql = sql + .InnerJoin() + .On((prop, rel) => prop.Id == rel.PropertyTypeId) + .Where(x => x.NodeId == contentId) + .Where(x => x.Alias == propertyTypeAlias); - /// - public IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string? group = null, - string? culture = null) - { - Sql sql = GetTagsSql(culture); + AddTagsSqlWhere(sql, culture); + if (group.IsNullOrWhiteSpace() == false) + { sql = sql - .InnerJoin() - .On((prop, rel) => prop.Id == rel.PropertyTypeId) - .Where(dto => dto.UniqueId == contentId) - .Where(dto => dto.Alias == propertyTypeAlias); + .Where(dto => dto.Group == group); + } - AddTagsSqlWhere(sql, culture); + return ExecuteTagsQuery(sql); + } - if (group.IsNullOrWhiteSpace() == false) - { - sql = sql - .Where(dto => dto.Group == group); - } + /// + public IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string? group = null, + string? culture = null) + { + Sql sql = GetTagsSql(culture); - return ExecuteTagsQuery(sql); - } + sql = sql + .InnerJoin() + .On((prop, rel) => prop.Id == rel.PropertyTypeId) + .Where(dto => dto.UniqueId == contentId) + .Where(dto => dto.Alias == propertyTypeAlias); + + AddTagsSqlWhere(sql, culture); - private Sql GetTagsSql(string? culture, bool withGrouping = false) + if (group.IsNullOrWhiteSpace() == false) { - Sql sql = Sql() - .Select(); + sql = sql + .Where(dto => dto.Group == group); + } - if (withGrouping) - { - sql = sql - .AndSelectCount("NodeCount"); - } + return ExecuteTagsQuery(sql); + } + + private Sql GetTagsSql(string? culture, bool withGrouping = false) + { + Sql sql = Sql() + .Select(); + if (withGrouping) + { sql = sql - .From() - .InnerJoin().On((rel, tag) => tag.Id == rel.TagId) - .InnerJoin() - .On((content, rel) => content.NodeId == rel.NodeId) - .InnerJoin().On((node, content) => node.NodeId == content.NodeId); + .AndSelectCount("NodeCount"); + } - if (culture != null && culture != "*") - { - sql = sql - .InnerJoin().On((tag, lang) => tag.LanguageId == lang.Id); - } + sql = sql + .From() + .InnerJoin().On((rel, tag) => tag.Id == rel.TagId) + .InnerJoin() + .On((content, rel) => content.NodeId == rel.NodeId) + .InnerJoin().On((node, content) => node.NodeId == content.NodeId); - return sql; + if (culture != null && culture != "*") + { + sql = sql + .InnerJoin().On((tag, lang) => tag.LanguageId == lang.Id); } - private Sql AddTagsSqlWhere(Sql sql, string? culture) - { - if (culture == null) - { - sql = sql - .Where(dto => dto.LanguageId == null); - } - else if (culture != "*") - { - sql = sql - .Where(x => x.IsoCode == culture); - } + return sql; + } - return sql; + private Sql AddTagsSqlWhere(Sql sql, string? culture) + { + if (culture == null) + { + sql = sql + .Where(dto => dto.LanguageId == null); + } + else if (culture != "*") + { + sql = sql + .Where(x => x.IsoCode == culture); } - private IEnumerable ExecuteTagsQuery(Sql sql) => - Database.Fetch(sql).Select(TagFactory.BuildEntity); + return sql; + } + + private IEnumerable ExecuteTagsQuery(Sql sql) => + Database.Fetch(sql).Select(TagFactory.BuildEntity); - private Guid GetNodeObjectType(TaggableObjectTypes type) + private Guid GetNodeObjectType(TaggableObjectTypes type) + { + switch (type) { - switch (type) - { - case TaggableObjectTypes.Content: - return Constants.ObjectTypes.Document; - case TaggableObjectTypes.Media: - return Constants.ObjectTypes.Media; - case TaggableObjectTypes.Member: - return Constants.ObjectTypes.Member; - default: - throw new ArgumentOutOfRangeException(nameof(type)); - } + case TaggableObjectTypes.Content: + return Constants.ObjectTypes.Document; + case TaggableObjectTypes.Media: + return Constants.ObjectTypes.Media; + case TaggableObjectTypes.Member: + return Constants.ObjectTypes.Member; + default: + throw new ArgumentOutOfRangeException(nameof(type)); } - - #endregion } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs index e214e2ef20a2..ceb3c5c41642 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Text; using Microsoft.Extensions.Logging; using NPoco; @@ -19,613 +15,620 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents the Template Repository +/// +internal class TemplateRepository : EntityRepositoryBase, ITemplateRepository { - /// - /// Represents the Template Repository - /// - internal class TemplateRepository : EntityRepositoryBase, ITemplateRepository + private readonly IIOHelper _ioHelper; + private readonly IShortStringHelper _shortStringHelper; + private readonly IViewHelper _viewHelper; + private readonly IFileSystem? _viewsFileSystem; + + public TemplateRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, + FileSystems fileSystems, IIOHelper ioHelper, IShortStringHelper shortStringHelper, IViewHelper viewHelper) + : base(scopeAccessor, cache, logger) { - private readonly IIOHelper _ioHelper; - private readonly IShortStringHelper _shortStringHelper; - private readonly IFileSystem? _viewsFileSystem; - private readonly IViewHelper _viewHelper; + _ioHelper = ioHelper; + _shortStringHelper = shortStringHelper; + _viewsFileSystem = fileSystems.MvcViewsFileSystem; + _viewHelper = viewHelper; + } - public TemplateRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, FileSystems fileSystems, IIOHelper ioHelper, IShortStringHelper shortStringHelper, IViewHelper viewHelper) - : base(scopeAccessor, cache, logger) + public Stream GetFileContentStream(string filepath) + { + IFileSystem? fileSystem = GetFileSystem(filepath); + if (fileSystem?.FileExists(filepath) == false) { - _ioHelper = ioHelper; - _shortStringHelper = shortStringHelper; - _viewsFileSystem = fileSystems.MvcViewsFileSystem; - _viewHelper = viewHelper; + return Stream.Null; } - protected override IRepositoryCachePolicy CreateCachePolicy() => - new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ false); + try + { + return fileSystem!.OpenFile(filepath); + } + catch + { + return Stream.Null; // deal with race conds + } + } - #region Overrides of RepositoryBase + public void SetFileContent(string filepath, Stream content) => + GetFileSystem(filepath)?.AddFile(filepath, content, true); - protected override ITemplate? PerformGet(int id) => - //use the underlying GetAll which will force cache all templates - base.GetMany()?.FirstOrDefault(x => x.Id == id); + public long GetFileSize(string filename) + { + IFileSystem? fileSystem = GetFileSystem(filename); + if (fileSystem?.FileExists(filename) == false) + { + return -1; + } - protected override IEnumerable PerformGetAll(params int[]? ids) + try + { + return fileSystem!.GetSize(filename); + } + catch { - Sql sql = GetBaseQuery(false); + return -1; // deal with race conds + } + } - if (ids?.Any() ?? false) - { - sql.Where("umbracoNode.id in (@ids)", new { ids }); - } - else + protected override IRepositoryCachePolicy CreateCachePolicy() => + new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, + GetEntityId, /*expires:*/ false); + + private IEnumerable GetAxisDefinitions(params TemplateDto[] templates) + { + //look up the simple template definitions that have a master template assigned, this is used + // later to populate the template item's properties + Sql childIdsSql = SqlContext.Sql() + .Select("nodeId,alias,parentID") + .From() + .InnerJoin() + .On(dto => dto.NodeId, dto => dto.NodeId) + //lookup axis's + .Where( + "umbracoNode." + SqlContext.SqlSyntax.GetQuotedColumnName("id") + + " IN (@parentIds) OR umbracoNode.parentID IN (@childIds)", + new + { + parentIds = templates.Select(x => x.NodeDto.ParentId), + childIds = templates.Select(x => x.NodeId) + }); + + IEnumerable childIds = Database.Fetch(childIdsSql) + .Select(x => new EntitySlim {Id = x.NodeId, ParentId = x.ParentId, Name = x.Alias}); + + return childIds; + } + + /// + /// Maps from a dto to an ITemplate + /// + /// + /// + /// This is a collection of template definitions ... either all templates, or the collection of child templates and + /// it's parent template + /// + /// + private ITemplate MapFromDto(TemplateDto dto, IUmbracoEntity[] axisDefinitions) + { + Template template = TemplateFactory.BuildEntity(_shortStringHelper, dto, axisDefinitions, + file => GetFileContent((Template)file, false)); + + if (dto.NodeDto.ParentId > 0) + { + IUmbracoEntity? masterTemplate = axisDefinitions.FirstOrDefault(x => x.Id == dto.NodeDto.ParentId); + if (masterTemplate != null) { - sql.Where(x => x.NodeObjectType == NodeObjectTypeId); + template.MasterTemplateAlias = masterTemplate.Name; + template.MasterTemplateId = new Lazy(() => dto.NodeDto.ParentId); } + } + + // get the infos (update date and virtual path) that will change only if + // path changes - but do not get content, will get loaded only when required + GetFileContent(template, true); + + // reset dirty initial properties (U4-1946) + template.ResetDirtyProperties(false); - List dtos = Database.Fetch(sql); + return template; + } - if (dtos.Count == 0) + private void SetVirtualPath(ITemplate template) + { + var path = template.OriginalPath; + if (string.IsNullOrWhiteSpace(path)) + { + // we need to discover the path + path = string.Concat(template.Alias, ".cshtml"); + if (_viewsFileSystem?.FileExists(path) ?? false) { - return Enumerable.Empty(); + template.VirtualPath = _viewsFileSystem.GetUrl(path); + return; } - //look up the simple template definitions that have a master template assigned, this is used - // later to populate the template item's properties - IUmbracoEntity[] childIds = (ids?.Any() ?? false - ? GetAxisDefinitions(dtos.ToArray()) - : dtos.Select(x => new EntitySlim - { - Id = x.NodeId, - ParentId = x.NodeDto.ParentId, - Name = x.Alias - })).ToArray(); - - return dtos.Select(d => MapFromDto(d, childIds)); + path = string.Concat(template.Alias, ".vbhtml"); + if (_viewsFileSystem?.FileExists(path) ?? false) + { + template.VirtualPath = _viewsFileSystem.GetUrl(path); + return; + } } - - protected override IEnumerable PerformGetByQuery(IQuery query) + else { - Sql sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - Sql sql = translator.Translate(); + // we know the path already + template.VirtualPath = _viewsFileSystem?.GetUrl(path); + } - List dtos = Database.Fetch(sql); + template.VirtualPath = string.Empty; // file not found... + } - if (dtos.Count == 0) + private string? GetFileContent(ITemplate template, bool init) + { + var path = template.OriginalPath; + if (string.IsNullOrWhiteSpace(path)) + { + // we need to discover the path + path = string.Concat(template.Alias, ".cshtml"); + if (_viewsFileSystem?.FileExists(path) ?? false) { - return Enumerable.Empty(); + return GetFileContent(template, _viewsFileSystem, path, init); } - //look up the simple template definitions that have a master template assigned, this is used - // later to populate the template item's properties - IUmbracoEntity[] childIds = GetAxisDefinitions(dtos.ToArray()).ToArray(); - - return dtos.Select(d => MapFromDto(d, childIds)); + path = string.Concat(template.Alias, ".vbhtml"); + if (_viewsFileSystem?.FileExists(path) ?? false) + { + return GetFileContent(template, _viewsFileSystem, path, init); + } + } + else + { + // we know the path already + return GetFileContent(template, _viewsFileSystem, path, init); } - #endregion + template.VirtualPath = string.Empty; // file not found... - #region Overrides of EntityRepositoryBase + return string.Empty; + } - protected override Sql GetBaseQuery(bool isCount) + private string? GetFileContent(ITemplate template, IFileSystem? fs, string filename, bool init) + { + // do not update .UpdateDate as that would make it dirty (side-effect) + // unless initializing, because we have to do it once + if (init && fs is not null) { - Sql sql = SqlContext.Sql(); + template.UpdateDate = fs.GetLastModified(filename).UtcDateTime; + } + + // TODO: see if this could enable us to update UpdateDate without messing with change tracking + // and then we'd want to do it for scripts, stylesheets and partial views too (ie files) + // var xtemplate = template as Template; + // xtemplate.DisableChangeTracking(); + // template.UpdateDate = fs.GetLastModified(filename).UtcDateTime; + // xtemplate.EnableChangeTracking(); - sql = isCount - ? sql.SelectCount() - : sql.Select(r => r.Select(x => x.NodeDto)); + template.VirtualPath = fs?.GetUrl(filename); - sql - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId); + return init ? null : GetFileContent(fs, filename); + } - return sql; + private string? GetFileContent(IFileSystem? fs, string filename) + { + if (fs is null) + { + return null; } - protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Node}.id = @id"; + using Stream stream = fs.OpenFile(filename); + using var reader = new StreamReader(stream, Encoding.UTF8, true); + return reader.ReadToEnd(); + } - protected override IEnumerable GetDeleteClauses() + private IFileSystem? GetFileSystem(string filepath) + { + var ext = Path.GetExtension(filepath); + IFileSystem? fs; + switch (ext) { - var list = new List - { - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.User2NodeNotify + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.UserGroup2Node + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.UserGroup2NodePermission + " WHERE nodeId = @id", - "UPDATE " + Cms.Core.Constants.DatabaseSchema.Tables.DocumentVersion + " SET templateId = NULL WHERE templateId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.DocumentType + " WHERE templateNodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Template + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Node + " WHERE id = @id" - }; - return list; + case ".cshtml": + case ".vbhtml": + fs = _viewsFileSystem; + break; + default: + throw new Exception("Unsupported extension " + ext + "."); } - protected Guid NodeObjectTypeId => Cms.Core.Constants.ObjectTypes.Template; + return fs; + } - protected override void PersistNewItem(ITemplate entity) - { - EnsureValidAlias(entity); + /// + /// Ensures that there are not duplicate aliases and if so, changes it to be a numbered version and also verifies the + /// length + /// + /// + private void EnsureValidAlias(ITemplate template) + { + //ensure unique alias + template.Alias = template.Alias.ToCleanString(_shortStringHelper, CleanStringType.UnderscoreAlias); - //Save to db - var template = (Template)entity; - template.AddingEntity(); + if (template.Alias.Length > 100) + { + template.Alias = template.Alias.Substring(0, 95); + } - TemplateDto dto = TemplateFactory.BuildDto(template, NodeObjectTypeId, template.Id); + if (AliasAlreadExists(template)) + { + template.Alias = EnsureUniqueAlias(template, 1); + } + } - //Create the (base) node data - umbracoNode - NodeDto nodeDto = dto.NodeDto; - nodeDto.Path = "-1," + dto.NodeDto.NodeId; - int o = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); + private bool AliasAlreadExists(ITemplate template) + { + Sql sql = GetBaseQuery(true) + .Where(x => x.Alias.InvariantEquals(template.Alias) && x.NodeId != template.Id); + var count = Database.ExecuteScalar(sql); + return count > 0; + } - //Update with new correct path - ITemplate? parent = Get(template.MasterTemplateId!.Value); - if (parent != null) - { - nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); - } - else - { - nodeDto.Path = "-1," + dto.NodeDto.NodeId; - } - Database.Update(nodeDto); + private string EnsureUniqueAlias(ITemplate template, int attempts) + { + // TODO: This is ported from the old data layer... pretty crap way of doing this but it works for now. + if (AliasAlreadExists(template)) + { + return template.Alias + attempts; + } - //Insert template dto - dto.NodeId = nodeDto.NodeId; - Database.Insert(dto); + attempts++; + return EnsureUniqueAlias(template, attempts); + } - //Update entity with correct values - template.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set - template.Path = nodeDto.Path; + #region Overrides of RepositoryBase - //now do the file work - SaveFile(template); + protected override ITemplate? PerformGet(int id) => + //use the underlying GetAll which will force cache all templates + GetMany()?.FirstOrDefault(x => x.Id == id); - template.ResetDirtyProperties(); + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = GetBaseQuery(false); - // ensure that from now on, content is lazy-loaded - if (template.GetFileContent == null) - { - template.GetFileContent = file => GetFileContent((Template) file, false); - } + if (ids?.Any() ?? false) + { + sql.Where("umbracoNode.id in (@ids)", new {ids}); + } + else + { + sql.Where(x => x.NodeObjectType == NodeObjectTypeId); } - protected override void PersistUpdatedItem(ITemplate entity) + List dtos = Database.Fetch(sql); + + if (dtos.Count == 0) { - EnsureValidAlias(entity); + return Enumerable.Empty(); + } - //store the changed alias if there is one for use with updating files later - string? originalAlias = entity.Alias; - if (entity.IsPropertyDirty("Alias")) - { - //we need to check what it currently is before saving and remove that file - ITemplate? current = Get(entity.Id); - originalAlias = current?.Alias; - } + //look up the simple template definitions that have a master template assigned, this is used + // later to populate the template item's properties + IUmbracoEntity[] childIds = (ids?.Any() ?? false + ? GetAxisDefinitions(dtos.ToArray()) + : dtos.Select(x => new EntitySlim {Id = x.NodeId, ParentId = x.NodeDto.ParentId, Name = x.Alias})) + .ToArray(); - var template = (Template)entity; + return dtos.Select(d => MapFromDto(d, childIds)); + } - if (entity.IsPropertyDirty("MasterTemplateId")) - { - ITemplate? parent = Get(template.MasterTemplateId!.Value); - if (parent != null) - { - entity.Path = string.Concat(parent.Path, ",", entity.Id); - } - else - { - //this means that the master template has been removed, so we need to reset the template's - //path to be at the root - entity.Path = string.Concat("-1,", entity.Id); - } - } + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); - //Get TemplateDto from db to get the Primary key of the entity - TemplateDto templateDto = Database.SingleOrDefault("WHERE nodeId = @Id", new { entity.Id }); + List dtos = Database.Fetch(sql); - //Save updated entity to db - template.UpdateDate = DateTime.Now; - TemplateDto dto = TemplateFactory.BuildDto(template, NodeObjectTypeId, templateDto.PrimaryKey); - Database.Update(dto.NodeDto); - Database.Update(dto); + if (dtos.Count == 0) + { + return Enumerable.Empty(); + } - //re-update if this is a master template, since it could have changed! - IEnumerable axisDefs = GetAxisDefinitions(dto); - template.IsMasterTemplate = axisDefs.Any(x => x.ParentId == dto.NodeId); + //look up the simple template definitions that have a master template assigned, this is used + // later to populate the template item's properties + IUmbracoEntity[] childIds = GetAxisDefinitions(dtos.ToArray()).ToArray(); - //now do the file work - SaveFile((Template) entity, originalAlias); + return dtos.Select(d => MapFromDto(d, childIds)); + } - entity.ResetDirtyProperties(); + #endregion - // ensure that from now on, content is lazy-loaded - if (template.GetFileContent == null) - { - template.GetFileContent = file => GetFileContent((Template) file, false); - } - } + #region Overrides of EntityRepositoryBase - private void SaveFile(Template template, string? originalAlias = null) - { - string? content; + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = SqlContext.Sql(); - if (template is TemplateOnDisk templateOnDisk && templateOnDisk.IsOnDisk) - { - // if "template on disk" load content from disk - content = _viewHelper.GetFileContents(template); - } - else - { - // else, create or write template.Content to disk - content = originalAlias == null - ? _viewHelper.CreateView(template, true) - : _viewHelper.UpdateViewFile(template, originalAlias); - } + sql = isCount + ? sql.SelectCount() + : sql.Select(r => r.Select(x => x.NodeDto)); - // once content has been set, "template on disk" are not "on disk" anymore - template.Content = content; - SetVirtualPath(template); - } + sql + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId); - protected override void PersistDeletedItem(ITemplate entity) - { - string[] deletes = GetDeleteClauses().ToArray(); + return sql; + } - var descendants = GetDescendants(entity.Id).ToList(); + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Node}.id = @id"; - //change the order so it goes bottom up! (deepest level first) - descendants.Reverse(); + protected override IEnumerable GetDeleteClauses() + { + var list = new List + { + "DELETE FROM " + Constants.DatabaseSchema.Tables.User2NodeNotify + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.UserGroup2Node + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.UserGroup2NodePermission + " WHERE nodeId = @id", + "UPDATE " + Constants.DatabaseSchema.Tables.DocumentVersion + + " SET templateId = NULL WHERE templateId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.DocumentType + " WHERE templateNodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Template + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Node + " WHERE id = @id" + }; + return list; + } - //delete the hierarchy - foreach (ITemplate descendant in descendants) - { - foreach (string delete in deletes) - { - Database.Execute(delete, new { id = GetEntityId(descendant) }); - } - } + protected Guid NodeObjectTypeId => Constants.ObjectTypes.Template; - //now we can delete this one - foreach (string delete in deletes) - { - Database.Execute(delete, new { id = GetEntityId(entity) }); - } + protected override void PersistNewItem(ITemplate entity) + { + EnsureValidAlias(entity); - string viewName = string.Concat(entity.Alias, ".cshtml"); - _viewsFileSystem?.DeleteFile(viewName); + //Save to db + var template = (Template)entity; + template.AddingEntity(); - entity.DeleteDate = DateTime.Now; - } + TemplateDto dto = TemplateFactory.BuildDto(template, NodeObjectTypeId, template.Id); - #endregion + //Create the (base) node data - umbracoNode + NodeDto nodeDto = dto.NodeDto; + nodeDto.Path = "-1," + dto.NodeDto.NodeId; + var o = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); - private IEnumerable GetAxisDefinitions(params TemplateDto[] templates) + //Update with new correct path + ITemplate? parent = Get(template.MasterTemplateId!.Value); + if (parent != null) { - //look up the simple template definitions that have a master template assigned, this is used - // later to populate the template item's properties - Sql childIdsSql = SqlContext.Sql() - .Select("nodeId,alias,parentID") - .From() - .InnerJoin() - .On(dto => dto.NodeId, dto => dto.NodeId) - //lookup axis's - .Where("umbracoNode." + SqlContext.SqlSyntax.GetQuotedColumnName("id") + " IN (@parentIds) OR umbracoNode.parentID IN (@childIds)", - new {parentIds = templates.Select(x => x.NodeDto.ParentId), childIds = templates.Select(x => x.NodeId)}); + nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); + } + else + { + nodeDto.Path = "-1," + dto.NodeDto.NodeId; + } - var childIds = Database.Fetch(childIdsSql) - .Select(x => new EntitySlim - { - Id = x.NodeId, - ParentId = x.ParentId, - Name = x.Alias - }); + Database.Update(nodeDto); - return childIds; - } + //Insert template dto + dto.NodeId = nodeDto.NodeId; + Database.Insert(dto); - /// - /// Maps from a dto to an ITemplate - /// - /// - /// - /// This is a collection of template definitions ... either all templates, or the collection of child templates and it's parent template - /// - /// - private ITemplate MapFromDto(TemplateDto dto, IUmbracoEntity[] axisDefinitions) - { + //Update entity with correct values + template.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set + template.Path = nodeDto.Path; - Template template = TemplateFactory.BuildEntity(_shortStringHelper, dto, axisDefinitions, file => GetFileContent((Template) file, false)); + //now do the file work + SaveFile(template); - if (dto.NodeDto.ParentId > 0) - { - IUmbracoEntity? masterTemplate = axisDefinitions.FirstOrDefault(x => x.Id == dto.NodeDto.ParentId); - if (masterTemplate != null) - { - template.MasterTemplateAlias = masterTemplate.Name; - template.MasterTemplateId = new Lazy(() => dto.NodeDto.ParentId); - } - } + template.ResetDirtyProperties(); - // get the infos (update date and virtual path) that will change only if - // path changes - but do not get content, will get loaded only when required - GetFileContent(template, true); + // ensure that from now on, content is lazy-loaded + if (template.GetFileContent == null) + { + template.GetFileContent = file => GetFileContent((Template)file, false); + } + } - // reset dirty initial properties (U4-1946) - template.ResetDirtyProperties(false); + protected override void PersistUpdatedItem(ITemplate entity) + { + EnsureValidAlias(entity); - return template; + //store the changed alias if there is one for use with updating files later + var originalAlias = entity.Alias; + if (entity.IsPropertyDirty("Alias")) + { + //we need to check what it currently is before saving and remove that file + ITemplate? current = Get(entity.Id); + originalAlias = current?.Alias; } - private void SetVirtualPath(ITemplate template) + var template = (Template)entity; + + if (entity.IsPropertyDirty("MasterTemplateId")) { - string path = template.OriginalPath; - if (string.IsNullOrWhiteSpace(path)) + ITemplate? parent = Get(template.MasterTemplateId!.Value); + if (parent != null) { - // we need to discover the path - path = string.Concat(template.Alias, ".cshtml"); - if (_viewsFileSystem?.FileExists(path) ?? false) - { - template.VirtualPath = _viewsFileSystem.GetUrl(path); - return; - } - path = string.Concat(template.Alias, ".vbhtml"); - if (_viewsFileSystem?.FileExists(path) ?? false) - { - template.VirtualPath = _viewsFileSystem.GetUrl(path); - return; - } + entity.Path = string.Concat(parent.Path, ",", entity.Id); } else { - // we know the path already - template.VirtualPath = _viewsFileSystem?.GetUrl(path); + //this means that the master template has been removed, so we need to reset the template's + //path to be at the root + entity.Path = string.Concat("-1,", entity.Id); } - - template.VirtualPath = string.Empty; // file not found... } - private string? GetFileContent(ITemplate template, bool init) - { - string path = template.OriginalPath; - if (string.IsNullOrWhiteSpace(path)) - { - // we need to discover the path - path = string.Concat(template.Alias, ".cshtml"); - if (_viewsFileSystem?.FileExists(path) ?? false) - { - return GetFileContent(template, _viewsFileSystem, path, init); - } + //Get TemplateDto from db to get the Primary key of the entity + TemplateDto templateDto = Database.SingleOrDefault("WHERE nodeId = @Id", new {entity.Id}); - path = string.Concat(template.Alias, ".vbhtml"); - if (_viewsFileSystem?.FileExists(path) ?? false) - { - return GetFileContent(template, _viewsFileSystem, path, init); - } - } - else - { - // we know the path already - return GetFileContent(template, _viewsFileSystem, path, init); - } + //Save updated entity to db + template.UpdateDate = DateTime.Now; + TemplateDto dto = TemplateFactory.BuildDto(template, NodeObjectTypeId, templateDto.PrimaryKey); + Database.Update(dto.NodeDto); + Database.Update(dto); - template.VirtualPath = string.Empty; // file not found... + //re-update if this is a master template, since it could have changed! + IEnumerable axisDefs = GetAxisDefinitions(dto); + template.IsMasterTemplate = axisDefs.Any(x => x.ParentId == dto.NodeId); - return string.Empty; + //now do the file work + SaveFile((Template)entity, originalAlias); + + entity.ResetDirtyProperties(); + + // ensure that from now on, content is lazy-loaded + if (template.GetFileContent == null) + { + template.GetFileContent = file => GetFileContent((Template)file, false); } + } - private string? GetFileContent(ITemplate template, IFileSystem? fs, string filename, bool init) + private void SaveFile(Template template, string? originalAlias = null) + { + string? content; + + if (template is TemplateOnDisk templateOnDisk && templateOnDisk.IsOnDisk) { - // do not update .UpdateDate as that would make it dirty (side-effect) - // unless initializing, because we have to do it once - if (init && fs is not null) - { - template.UpdateDate = fs.GetLastModified(filename).UtcDateTime; - } + // if "template on disk" load content from disk + content = _viewHelper.GetFileContents(template); + } + else + { + // else, create or write template.Content to disk + content = originalAlias == null + ? _viewHelper.CreateView(template, true) + : _viewHelper.UpdateViewFile(template, originalAlias); + } + + // once content has been set, "template on disk" are not "on disk" anymore + template.Content = content; + SetVirtualPath(template); + } - // TODO: see if this could enable us to update UpdateDate without messing with change tracking - // and then we'd want to do it for scripts, stylesheets and partial views too (ie files) - // var xtemplate = template as Template; - // xtemplate.DisableChangeTracking(); - // template.UpdateDate = fs.GetLastModified(filename).UtcDateTime; - // xtemplate.EnableChangeTracking(); + protected override void PersistDeletedItem(ITemplate entity) + { + var deletes = GetDeleteClauses().ToArray(); - template.VirtualPath = fs?.GetUrl(filename); + var descendants = GetDescendants(entity.Id).ToList(); - return init ? null : GetFileContent(fs, filename); - } + //change the order so it goes bottom up! (deepest level first) + descendants.Reverse(); - private string? GetFileContent(IFileSystem? fs, string filename) + //delete the hierarchy + foreach (ITemplate descendant in descendants) { - if (fs is null) + foreach (var delete in deletes) { - return null; + Database.Execute(delete, new {id = GetEntityId(descendant)}); } - - using Stream stream = fs.OpenFile(filename); - using var reader = new StreamReader(stream, Encoding.UTF8, true); - return reader.ReadToEnd(); } - public Stream GetFileContentStream(string filepath) + //now we can delete this one + foreach (var delete in deletes) { - IFileSystem? fileSystem = GetFileSystem(filepath); - if (fileSystem?.FileExists(filepath) == false) - { - return Stream.Null; - } - - try - { - return fileSystem!.OpenFile(filepath); - } - catch - { - return Stream.Null; // deal with race conds - } + Database.Execute(delete, new {id = GetEntityId(entity)}); } - public void SetFileContent(string filepath, Stream content) => GetFileSystem(filepath)?.AddFile(filepath, content, true); + var viewName = string.Concat(entity.Alias, ".cshtml"); + _viewsFileSystem?.DeleteFile(viewName); - public long GetFileSize(string filename) - { - IFileSystem? fileSystem = GetFileSystem(filename); - if (fileSystem?.FileExists(filename) == false) - { - return -1; - } + entity.DeleteDate = DateTime.Now; + } - try - { - return fileSystem!.GetSize(filename); - } - catch - { - return -1; // deal with race conds - } - } + #endregion - private IFileSystem? GetFileSystem(string filepath) + #region Implementation of ITemplateRepository + + public ITemplate? Get(string? alias) => GetAll(alias)?.FirstOrDefault(); + + public IEnumerable? GetAll(params string?[] aliases) + { + //We must call the base (normal) GetAll method + // which is cached. This is a specialized method and unfortunately with the params[] it + // overlaps with the normal GetAll method. + if (aliases.Any() == false) { - string ext = Path.GetExtension(filepath); - IFileSystem? fs; - switch (ext) - { - case ".cshtml": - case ".vbhtml": - fs = _viewsFileSystem; - break; - default: - throw new Exception("Unsupported extension " + ext + "."); - } - return fs; + return GetMany(); } - #region Implementation of ITemplateRepository + //return from base.GetAll, this is all cached + return GetMany()?.Where(x => aliases.WhereNotNull().InvariantContains(x.Alias)); + } - public ITemplate? Get(string? alias) => GetAll(alias)?.FirstOrDefault(); + public IEnumerable? GetChildren(int masterTemplateId) + { + //return from base.GetAll, this is all cached + ITemplate[]? all = GetMany()?.ToArray(); - public IEnumerable? GetAll(params string?[] aliases) + if (masterTemplateId <= 0) { - //We must call the base (normal) GetAll method - // which is cached. This is a specialized method and unfortunately with the params[] it - // overlaps with the normal GetAll method. - if (aliases.Any() == false) - { - return base.GetMany(); - } - - //return from base.GetAll, this is all cached - return base.GetMany()?.Where(x => aliases.WhereNotNull().InvariantContains(x.Alias)); + return all?.Where(x => x.MasterTemplateAlias.IsNullOrWhiteSpace()); } - public IEnumerable? GetChildren(int masterTemplateId) + ITemplate? parent = all?.FirstOrDefault(x => x.Id == masterTemplateId); + if (parent == null) { - //return from base.GetAll, this is all cached - ITemplate[]? all = base.GetMany()?.ToArray(); + return Enumerable.Empty(); + } - if (masterTemplateId <= 0) - { - return all?.Where(x => x.MasterTemplateAlias.IsNullOrWhiteSpace()); - } + IEnumerable? children = all?.Where(x => x.MasterTemplateAlias.InvariantEquals(parent.Alias)); + return children; + } + public IEnumerable GetDescendants(int masterTemplateId) + { + //return from base.GetAll, this is all cached + ITemplate[]? all = GetMany()?.ToArray(); + var descendants = new List(); + if (masterTemplateId > 0) + { ITemplate? parent = all?.FirstOrDefault(x => x.Id == masterTemplateId); if (parent == null) { return Enumerable.Empty(); } - IEnumerable? children = all?.Where(x => x.MasterTemplateAlias.InvariantEquals(parent.Alias)); - return children; + //recursively add all children with a level + AddChildren(all, descendants, parent.Alias); } - - public IEnumerable GetDescendants(int masterTemplateId) + else { - //return from base.GetAll, this is all cached - ITemplate[]? all = base.GetMany()?.ToArray(); - var descendants = new List(); - if (masterTemplateId > 0) - { - ITemplate? parent = all?.FirstOrDefault(x => x.Id == masterTemplateId); - if (parent == null) - { - return Enumerable.Empty(); - } - - //recursively add all children with a level - AddChildren(all, descendants, parent.Alias); - } - else + if (all is not null) { - if (all is not null) + descendants.AddRange(all.Where(x => x.MasterTemplateAlias.IsNullOrWhiteSpace())); + foreach (ITemplate parent in descendants) { - descendants.AddRange(all.Where(x => x.MasterTemplateAlias.IsNullOrWhiteSpace())); - foreach (ITemplate parent in descendants) - { - //recursively add all children with a level - AddChildren(all, descendants, parent.Alias); - } + //recursively add all children with a level + AddChildren(all, descendants, parent.Alias); } } - - //return the list - it will be naturally ordered by level - return descendants; } - private void AddChildren(ITemplate[]? all, List descendants, string masterAlias) - { - ITemplate[]? c = all?.Where(x => x.MasterTemplateAlias.InvariantEquals(masterAlias)).ToArray(); - if (c is null || c.Any() == false) - { - return; - } - descendants.AddRange(c); - - //recurse through all children - foreach (ITemplate child in c) - { - AddChildren(all, descendants, child.Alias); - } - } - - #endregion + //return the list - it will be naturally ordered by level + return descendants; + } - /// - /// Ensures that there are not duplicate aliases and if so, changes it to be a numbered version and also verifies the length - /// - /// - private void EnsureValidAlias(ITemplate template) + private void AddChildren(ITemplate[]? all, List descendants, string masterAlias) + { + ITemplate[]? c = all?.Where(x => x.MasterTemplateAlias.InvariantEquals(masterAlias)).ToArray(); + if (c is null || c.Any() == false) { - //ensure unique alias - template.Alias = template.Alias.ToCleanString(_shortStringHelper, CleanStringType.UnderscoreAlias); - - if (template.Alias.Length > 100) - { - template.Alias = template.Alias.Substring(0, 95); - } - - if (AliasAlreadExists(template)) - { - template.Alias = EnsureUniqueAlias(template, 1); - } + return; } - private bool AliasAlreadExists(ITemplate template) - { - Sql sql = GetBaseQuery(true).Where(x => x.Alias.InvariantEquals(template.Alias) && x.NodeId != template.Id); - int count = Database.ExecuteScalar(sql); - return count > 0; - } + descendants.AddRange(c); - private string EnsureUniqueAlias(ITemplate template, int attempts) + //recurse through all children + foreach (ITemplate child in c) { - // TODO: This is ported from the old data layer... pretty crap way of doing this but it works for now. - if (AliasAlreadExists(template)) - { - return template.Alias + attempts; - } - - attempts++; - return EnsureUniqueAlias(template, attempts); + AddChildren(all, descendants, child.Alias); } } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs index 18098623cf92..6b26d78f2ce8 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs @@ -1,181 +1,207 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Linq.Expressions; using NPoco; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class TrackedReferencesRepository : ITrackedReferencesRepository { - internal class TrackedReferencesRepository : ITrackedReferencesRepository - { - private readonly IScopeAccessor _scopeAccessor; + private readonly IScopeAccessor _scopeAccessor; - public TrackedReferencesRepository(IScopeAccessor scopeAccessor) - { - _scopeAccessor = scopeAccessor; - } + public TrackedReferencesRepository(IScopeAccessor scopeAccessor) => _scopeAccessor = scopeAccessor; - /// - /// Gets a page of items used in any kind of relation from selected integer ids. - /// - public IEnumerable GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords) + /// + /// Gets a page of items used in any kind of relation from selected integer ids. + /// + public IEnumerable GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, + bool filterMustBeIsDependency, out long totalRecords) + { + Sql? sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql().SelectDistinct( + "[pn].[id] as nodeId", + "[pn].[uniqueId] as nodeKey", + "[pn].[text] as nodeName", + "[pn].[nodeObjectType] as nodeObjectType", + "[ct].[icon] as contentTypeIcon", + "[ct].[alias] as contentTypeAlias", + "[ctn].[text] as contentTypeName", + "[umbracoRelationType].[alias] as relationTypeAlias", + "[umbracoRelationType].[name] as relationTypeName", + "[umbracoRelationType].[isDependency] as relationTypeIsDependency", + "[umbracoRelationType].[dual] as relationTypeIsBidirectional") + .From("r") + .InnerJoin("umbracoRelationType") + .On((left, right) => left.RelationType == right.Id, "r", + "umbracoRelationType") + .InnerJoin("cn").On( + (r, cn, rt) => (!rt.Dual && r.ParentId == cn.NodeId) || + (rt.Dual && (r.ChildId == cn.NodeId || r.ParentId == cn.NodeId)), "r", "cn", + "umbracoRelationType") + .InnerJoin("pn").On( + (r, pn, cn) => (pn.NodeId == r.ChildId && cn.NodeId == r.ParentId) || + (pn.NodeId == r.ParentId && cn.NodeId == r.ChildId), "r", "pn", "cn") + .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, "pn", "c") + .LeftJoin("ct") + .On((left, right) => left.ContentTypeId == right.NodeId, "c", "ct") + .LeftJoin("ctn") + .On((left, right) => left.NodeId == right.NodeId, "ct", "ctn"); + + if (ids.Any()) { - var sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql().SelectDistinct( - "[pn].[id] as nodeId", - "[pn].[uniqueId] as nodeKey", - "[pn].[text] as nodeName", - "[pn].[nodeObjectType] as nodeObjectType", - "[ct].[icon] as contentTypeIcon", - "[ct].[alias] as contentTypeAlias", - "[ctn].[text] as contentTypeName", - "[umbracoRelationType].[alias] as relationTypeAlias", - "[umbracoRelationType].[name] as relationTypeName", - "[umbracoRelationType].[isDependency] as relationTypeIsDependency", - "[umbracoRelationType].[dual] as relationTypeIsBidirectional") - .From("r") - .InnerJoin("umbracoRelationType").On((left, right) => left.RelationType == right.Id, aliasLeft: "r", aliasRight: "umbracoRelationType") - .InnerJoin("cn").On((r, cn, rt) => (!rt.Dual && r.ParentId == cn.NodeId) || (rt.Dual && (r.ChildId == cn.NodeId || r.ParentId == cn.NodeId)), aliasLeft: "r", aliasRight: "cn", aliasOther: "umbracoRelationType") - .InnerJoin("pn").On((r, pn, cn) => (pn.NodeId == r.ChildId && cn.NodeId == r.ParentId) || (pn.NodeId == r.ParentId && cn.NodeId == r.ChildId), aliasLeft: "r", aliasRight: "pn", aliasOther: "cn") - .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, aliasLeft: "pn", aliasRight: "c") - .LeftJoin("ct").On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft: "c", aliasRight: "ct") - .LeftJoin("ctn").On((left, right) => left.NodeId == right.NodeId, aliasLeft: "ct", aliasRight: "ctn"); - - if (ids.Any()) - { - sql = sql?.Where(x => ids.Contains(x.NodeId), "pn"); - } - - if (filterMustBeIsDependency) - { - sql = sql?.Where(rt => rt.IsDependency, "umbracoRelationType"); - } - - // Ordering is required for paging - sql = sql?.OrderBy(x => x.Alias); - - var pagedResult = _scopeAccessor.AmbientScope?.Database.Page(pageIndex + 1, pageSize, sql); - totalRecords = Convert.ToInt32(pagedResult?.TotalItems); - - return pagedResult?.Items.Select(MapDtoToEntity) ?? Enumerable.Empty(); + sql = sql?.Where(x => ids.Contains(x.NodeId), "pn"); } - /// - /// Gets a page of the descending items that have any references, given a parent id. - /// - public IEnumerable GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords) + if (filterMustBeIsDependency) { - var syntax = _scopeAccessor.AmbientScope?.Database.SqlContext.SqlSyntax; - - // Gets the path of the parent with ",%" added - var subsubQuery = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() - .Select(syntax?.GetConcat("[node].[path]", "',%'")) - .From("node") - .Where(x => x.NodeId == parentId, "node"); - - // Gets the descendants of the parent node - Sql? subQuery = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() - .Select(x => x.NodeId) - .From() - .WhereLike(x => x.Path, subsubQuery); - - // Get all relations where parent is in the sub query - var sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql().SelectDistinct( - "[pn].[id] as nodeId", - "[pn].[uniqueId] as nodeKey", - "[pn].[text] as nodeName", - "[pn].[nodeObjectType] as nodeObjectType", - "[ct].[icon] as contentTypeIcon", - "[ct].[alias] as contentTypeAlias", - "[ctn].[text] as contentTypeName", - "[umbracoRelationType].[alias] as relationTypeAlias", - "[umbracoRelationType].[name] as relationTypeName", - "[umbracoRelationType].[isDependency] as relationTypeIsDependency", - "[umbracoRelationType].[dual] as relationTypeIsBidirectional") - .From("r") - .InnerJoin("umbracoRelationType").On((left, right) => left.RelationType == right.Id, aliasLeft: "r", aliasRight: "umbracoRelationType") - .InnerJoin("cn").On((r, cn, rt) => (!rt.Dual && r.ParentId == cn.NodeId) || (rt.Dual && (r.ChildId == cn.NodeId || r.ParentId == cn.NodeId)), aliasLeft: "r", aliasRight: "cn", aliasOther: "umbracoRelationType") - .InnerJoin("pn").On((r, pn, cn) => (pn.NodeId == r.ChildId && cn.NodeId == r.ParentId) || (pn.NodeId == r.ParentId && cn.NodeId == r.ChildId), aliasLeft: "r", aliasRight: "pn", aliasOther: "cn") - .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, aliasLeft: "pn", aliasRight: "c") - .LeftJoin("ct").On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft: "c", aliasRight: "ct") - .LeftJoin("ctn").On((left, right) => left.NodeId == right.NodeId, aliasLeft: "ct", aliasRight: "ctn") - .WhereIn((System.Linq.Expressions.Expression>)(x => x.NodeId), subQuery, "pn"); - - if (filterMustBeIsDependency) - { - sql = sql?.Where(rt => rt.IsDependency, "umbracoRelationType"); - } - - // Ordering is required for paging - sql = sql?.OrderBy(x => x.Alias); - - var pagedResult = _scopeAccessor.AmbientScope?.Database.Page(pageIndex + 1, pageSize, sql); - totalRecords = Convert.ToInt32(pagedResult?.TotalItems); - - return pagedResult?.Items.Select(MapDtoToEntity) ?? Enumerable.Empty(); + sql = sql?.Where(rt => rt.IsDependency, "umbracoRelationType"); } - /// - /// Gets a page of items which are in relation with the current item. - /// Basically, shows the items which depend on the current item. - /// - public IEnumerable GetPagedRelationsForItem(int id, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords) + // Ordering is required for paging + sql = sql?.OrderBy(x => x.Alias); + + Page? pagedResult = + _scopeAccessor.AmbientScope?.Database.Page(pageIndex + 1, pageSize, sql); + totalRecords = Convert.ToInt32(pagedResult?.TotalItems); + + return pagedResult?.Items.Select(MapDtoToEntity) ?? Enumerable.Empty(); + } + + /// + /// Gets a page of the descending items that have any references, given a parent id. + /// + public IEnumerable GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, + bool filterMustBeIsDependency, out long totalRecords) + { + ISqlSyntaxProvider? syntax = _scopeAccessor.AmbientScope?.Database.SqlContext.SqlSyntax; + + // Gets the path of the parent with ",%" added + Sql? subsubQuery = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() + .Select(syntax?.GetConcat("[node].[path]", "',%'")) + .From("node") + .Where(x => x.NodeId == parentId, "node"); + + // Gets the descendants of the parent node + Sql? subQuery = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() + .Select(x => x.NodeId) + .From() + .WhereLike(x => x.Path, subsubQuery); + + // Get all relations where parent is in the sub query + Sql? sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql().SelectDistinct( + "[pn].[id] as nodeId", + "[pn].[uniqueId] as nodeKey", + "[pn].[text] as nodeName", + "[pn].[nodeObjectType] as nodeObjectType", + "[ct].[icon] as contentTypeIcon", + "[ct].[alias] as contentTypeAlias", + "[ctn].[text] as contentTypeName", + "[umbracoRelationType].[alias] as relationTypeAlias", + "[umbracoRelationType].[name] as relationTypeName", + "[umbracoRelationType].[isDependency] as relationTypeIsDependency", + "[umbracoRelationType].[dual] as relationTypeIsBidirectional") + .From("r") + .InnerJoin("umbracoRelationType") + .On((left, right) => left.RelationType == right.Id, "r", + "umbracoRelationType") + .InnerJoin("cn").On( + (r, cn, rt) => (!rt.Dual && r.ParentId == cn.NodeId) || + (rt.Dual && (r.ChildId == cn.NodeId || r.ParentId == cn.NodeId)), "r", "cn", + "umbracoRelationType") + .InnerJoin("pn").On( + (r, pn, cn) => (pn.NodeId == r.ChildId && cn.NodeId == r.ParentId) || + (pn.NodeId == r.ParentId && cn.NodeId == r.ChildId), "r", "pn", "cn") + .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, "pn", "c") + .LeftJoin("ct") + .On((left, right) => left.ContentTypeId == right.NodeId, "c", "ct") + .LeftJoin("ctn") + .On((left, right) => left.NodeId == right.NodeId, "ct", "ctn") + .WhereIn((Expression>)(x => x.NodeId), subQuery, "pn"); + + if (filterMustBeIsDependency) { - var sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql().SelectDistinct( - "[cn].[id] as nodeId", - "[cn].[uniqueId] as nodeKey", - "[cn].[text] as nodeName", - "[cn].[nodeObjectType] as nodeObjectType", - "[ct].[icon] as contentTypeIcon", - "[ct].[alias] as contentTypeAlias", - "[ctn].[text] as contentTypeName", - "[umbracoRelationType].[alias] as relationTypeAlias", - "[umbracoRelationType].[name] as relationTypeName", - "[umbracoRelationType].[isDependency] as relationTypeIsDependency", - "[umbracoRelationType].[dual] as relationTypeIsBidirectional") - .From("r") - .InnerJoin("umbracoRelationType").On((left, right) => left.RelationType == right.Id, aliasLeft: "r", aliasRight: "umbracoRelationType") - .InnerJoin("cn").On((r, cn, rt) => (!rt.Dual && r.ParentId == cn.NodeId) || (rt.Dual && (r.ChildId == cn.NodeId || r.ParentId == cn.NodeId)), aliasLeft: "r", aliasRight: "cn", aliasOther: "umbracoRelationType") - .InnerJoin("pn").On((r, pn, cn) => (pn.NodeId == r.ChildId && cn.NodeId == r.ParentId) || (pn.NodeId == r.ParentId && cn.NodeId == r.ChildId), aliasLeft: "r", aliasRight: "pn", aliasOther: "cn") - .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, aliasLeft: "cn", aliasRight: "c") - .LeftJoin("ct").On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft: "c", aliasRight: "ct") - .LeftJoin("ctn").On((left, right) => left.NodeId == right.NodeId, aliasLeft: "ct", aliasRight: "ctn") - .Where(x => x.NodeId == id, "pn") - .Where(x => x.ChildId == id || x.ParentId == id, "r"); // This last Where is purely to help SqlServer make a smarter query plan. More info https://github.com/umbraco/Umbraco-CMS/issues/12190 - - if (filterMustBeIsDependency) - { - sql = sql?.Where(rt => rt.IsDependency, "umbracoRelationType"); - } - - // Ordering is required for paging - sql = sql?.OrderBy(x => x.Alias); - - var pagedResult = _scopeAccessor.AmbientScope?.Database.Page(pageIndex + 1, pageSize, sql); - totalRecords = Convert.ToInt32(pagedResult?.TotalItems); - - return pagedResult?.Items.Select(MapDtoToEntity) ?? Enumerable.Empty(); + sql = sql?.Where(rt => rt.IsDependency, "umbracoRelationType"); } - private RelationItem MapDtoToEntity(RelationItemDto dto) + // Ordering is required for paging + sql = sql?.OrderBy(x => x.Alias); + + Page? pagedResult = + _scopeAccessor.AmbientScope?.Database.Page(pageIndex + 1, pageSize, sql); + totalRecords = Convert.ToInt32(pagedResult?.TotalItems); + + return pagedResult?.Items.Select(MapDtoToEntity) ?? Enumerable.Empty(); + } + + /// + /// Gets a page of items which are in relation with the current item. + /// Basically, shows the items which depend on the current item. + /// + public IEnumerable GetPagedRelationsForItem(int id, long pageIndex, int pageSize, + bool filterMustBeIsDependency, out long totalRecords) + { + Sql? sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql().SelectDistinct( + "[cn].[id] as nodeId", + "[cn].[uniqueId] as nodeKey", + "[cn].[text] as nodeName", + "[cn].[nodeObjectType] as nodeObjectType", + "[ct].[icon] as contentTypeIcon", + "[ct].[alias] as contentTypeAlias", + "[ctn].[text] as contentTypeName", + "[umbracoRelationType].[alias] as relationTypeAlias", + "[umbracoRelationType].[name] as relationTypeName", + "[umbracoRelationType].[isDependency] as relationTypeIsDependency", + "[umbracoRelationType].[dual] as relationTypeIsBidirectional") + .From("r") + .InnerJoin("umbracoRelationType") + .On((left, right) => left.RelationType == right.Id, "r", + "umbracoRelationType") + .InnerJoin("cn").On( + (r, cn, rt) => (!rt.Dual && r.ParentId == cn.NodeId) || + (rt.Dual && (r.ChildId == cn.NodeId || r.ParentId == cn.NodeId)), "r", "cn", + "umbracoRelationType") + .InnerJoin("pn").On( + (r, pn, cn) => (pn.NodeId == r.ChildId && cn.NodeId == r.ParentId) || + (pn.NodeId == r.ParentId && cn.NodeId == r.ChildId), "r", "pn", "cn") + .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, "cn", "c") + .LeftJoin("ct") + .On((left, right) => left.ContentTypeId == right.NodeId, "c", "ct") + .LeftJoin("ctn") + .On((left, right) => left.NodeId == right.NodeId, "ct", "ctn") + .Where(x => x.NodeId == id, "pn") + .Where(x => x.ChildId == id || x.ParentId == id, + "r"); // This last Where is purely to help SqlServer make a smarter query plan. More info https://github.com/umbraco/Umbraco-CMS/issues/12190 + + if (filterMustBeIsDependency) { - return new RelationItem() - { - NodeId = dto.ChildNodeId, - NodeKey = dto.ChildNodeKey, - NodeType = ObjectTypes.GetUdiType(dto.ChildNodeObjectType), - NodeName = dto.ChildNodeName, - RelationTypeName = dto.RelationTypeName, - RelationTypeIsBidirectional = dto.RelationTypeIsBidirectional, - RelationTypeIsDependency = dto.RelationTypeIsDependency, - ContentTypeAlias = dto.ChildContentTypeAlias, - ContentTypeIcon = dto.ChildContentTypeIcon, - ContentTypeName = dto.ChildContentTypeName, - }; + sql = sql?.Where(rt => rt.IsDependency, "umbracoRelationType"); } + + // Ordering is required for paging + sql = sql?.OrderBy(x => x.Alias); + + Page? pagedResult = + _scopeAccessor.AmbientScope?.Database.Page(pageIndex + 1, pageSize, sql); + totalRecords = Convert.ToInt32(pagedResult?.TotalItems); + + return pagedResult?.Items.Select(MapDtoToEntity) ?? Enumerable.Empty(); } + + private RelationItem MapDtoToEntity(RelationItemDto dto) => + new RelationItem + { + NodeId = dto.ChildNodeId, + NodeKey = dto.ChildNodeKey, + NodeType = ObjectTypes.GetUdiType(dto.ChildNodeObjectType), + NodeName = dto.ChildNodeName, + RelationTypeName = dto.RelationTypeName, + RelationTypeIsBidirectional = dto.RelationTypeIsBidirectional, + RelationTypeIsDependency = dto.RelationTypeIsDependency, + ContentTypeAlias = dto.ChildContentTypeAlias, + ContentTypeIcon = dto.ChildContentTypeIcon, + ContentTypeName = dto.ChildContentTypeName + }; } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TupleExtensions.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TupleExtensions.cs index a5a3fe4f211f..45a6c7b6fb1c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TupleExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TupleExtensions.cs @@ -1,19 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +internal static class TupleExtensions { - static class TupleExtensions - { - public static IEnumerable Map(this Tuple, List> t, Func relator) - { - return t.Item1.Zip(t.Item2, relator); - } + public static IEnumerable Map(this Tuple, List> t, + Func relator) => t.Item1.Zip(t.Item2, relator); -// public static IEnumerable Map(this Tuple, List, List> t, Func relator) + // public static IEnumerable Map(this Tuple, List, List> t, Func relator) // { // return t.Item1.Zip(t.Item2, t.Item3, relator); // } - } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TwoFactorLoginRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TwoFactorLoginRepository.cs index b74063df9b81..abd6b941c3af 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TwoFactorLoginRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TwoFactorLoginRepository.cs @@ -1,9 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; @@ -13,128 +10,128 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class TwoFactorLoginRepository : EntityRepositoryBase, ITwoFactorLoginRepository { - internal class TwoFactorLoginRepository : EntityRepositoryBase, ITwoFactorLoginRepository + public TwoFactorLoginRepository(IScopeAccessor scopeAccessor, AppCaches cache, + ILogger logger) + : base(scopeAccessor, cache, logger) + { + } + + public async Task DeleteUserLoginsAsync(Guid userOrMemberKey) => + await DeleteUserLoginsAsync(userOrMemberKey, null); + + public async Task DeleteUserLoginsAsync(Guid userOrMemberKey, string? providerName) { - public TwoFactorLoginRepository(IScopeAccessor scopeAccessor, AppCaches cache, - ILogger logger) - : base(scopeAccessor, cache, logger) + Sql sql = Sql() + .Delete() + .From() + .Where(x => x.UserOrMemberKey == userOrMemberKey); + + if (providerName is not null) { + sql = sql.Where(x => x.ProviderName == providerName); } + var deletedRows = await Database.ExecuteAsync(sql); - protected override Sql GetBaseQuery(bool isCount) - { - var sql = SqlContext.Sql(); + return deletedRows > 0; + } - sql = isCount - ? sql.SelectCount() - : sql.Select(); + public async Task> GetByUserOrMemberKeyAsync(Guid userOrMemberKey) + { + Sql sql = Sql() + .Select() + .From() + .Where(x => x.UserOrMemberKey == userOrMemberKey); + List? dtos = await Database.FetchAsync(sql); + return dtos.WhereNotNull().Select(Map).WhereNotNull(); + } - sql.From(); - return sql; - } + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = SqlContext.Sql(); - protected override string GetBaseWhereClause() => - Core.Constants.DatabaseSchema.Tables.TwoFactorLogin + ".id = @id"; + sql = isCount + ? sql.SelectCount() + : sql.Select(); - protected override IEnumerable GetDeleteClauses() => Enumerable.Empty(); + sql.From(); - protected override ITwoFactorLogin? PerformGet(int id) - { - var sql = GetBaseQuery(false).Where(x => x.Id == id); - var dto = Database.Fetch(sql).FirstOrDefault(); - return dto == null ? null : Map(dto); - } + return sql; + } - protected override IEnumerable PerformGetAll(params int[]? ids) - { - var sql = GetBaseQuery(false).WhereIn(x => x.Id, ids); - var dtos = Database.Fetch(sql); - return dtos.WhereNotNull().Select(Map).WhereNotNull(); - } + protected override string GetBaseWhereClause() => + Constants.DatabaseSchema.Tables.TwoFactorLogin + ".id = @id"; - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - return Database.Fetch(sql).Select(Map).WhereNotNull(); - } + protected override IEnumerable GetDeleteClauses() => Enumerable.Empty(); - protected override void PersistNewItem(ITwoFactorLogin entity) - { - var dto = Map(entity); - Database.Insert(dto); - } + protected override ITwoFactorLogin? PerformGet(int id) + { + Sql sql = GetBaseQuery(false).Where(x => x.Id == id); + TwoFactorLoginDto? dto = Database.Fetch(sql).FirstOrDefault(); + return dto == null ? null : Map(dto); + } - protected override void PersistUpdatedItem(ITwoFactorLogin entity) - { - var dto = Map(entity); - if (dto is not null) - { - Database.Update(dto); - } - } + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = GetBaseQuery(false).WhereIn(x => x.Id, ids); + List? dtos = Database.Fetch(sql); + return dtos.WhereNotNull().Select(Map).WhereNotNull(); + } - private static TwoFactorLoginDto? Map(ITwoFactorLogin entity) - { - if (entity == null) return null; - - return new TwoFactorLoginDto - { - Id = entity.Id, - UserOrMemberKey = entity.UserOrMemberKey, - ProviderName = entity.ProviderName, - Secret = entity.Secret, - }; - } + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + return Database.Fetch(sql).Select(Map).WhereNotNull(); + } - private static ITwoFactorLogin? Map(TwoFactorLoginDto dto) + protected override void PersistNewItem(ITwoFactorLogin entity) + { + TwoFactorLoginDto? dto = Map(entity); + Database.Insert(dto); + } + + protected override void PersistUpdatedItem(ITwoFactorLogin entity) + { + TwoFactorLoginDto? dto = Map(entity); + if (dto is not null) { - if (dto == null) return null; - - return new TwoFactorLogin - { - Id = dto.Id, - UserOrMemberKey = dto.UserOrMemberKey, - ProviderName = dto.ProviderName, - Secret = dto.Secret, - }; + Database.Update(dto); } + } - public async Task DeleteUserLoginsAsync(Guid userOrMemberKey) + private static TwoFactorLoginDto? Map(ITwoFactorLogin entity) + { + if (entity == null) { - return await DeleteUserLoginsAsync(userOrMemberKey, null); + return null; } - public async Task DeleteUserLoginsAsync(Guid userOrMemberKey, string? providerName) + return new TwoFactorLoginDto { - var sql = Sql() - .Delete() - .From() - .Where(x => x.UserOrMemberKey == userOrMemberKey); - - if (providerName is not null) - { - sql = sql.Where(x => x.ProviderName == providerName); - } - - var deletedRows = await Database.ExecuteAsync(sql); + Id = entity.Id, + UserOrMemberKey = entity.UserOrMemberKey, + ProviderName = entity.ProviderName, + Secret = entity.Secret + }; + } - return deletedRows > 0; + private static ITwoFactorLogin? Map(TwoFactorLoginDto dto) + { + if (dto == null) + { + return null; } - public async Task> GetByUserOrMemberKeyAsync(Guid userOrMemberKey) + return new TwoFactorLogin { - var sql = Sql() - .Select() - .From() - .Where(x => x.UserOrMemberKey == userOrMemberKey); - var dtos = await Database.FetchAsync(sql); - return dtos.WhereNotNull().Select(Map).WhereNotNull(); - } + Id = dto.Id, UserOrMemberKey = dto.UserOrMemberKey, ProviderName = dto.ProviderName, Secret = dto.Secret + }; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs index 7a3ca69db1c8..b2a8afdc5a01 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -17,455 +14,449 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents the UserGroupRepository for doing CRUD operations for +/// +public class UserGroupRepository : EntityRepositoryBase, IUserGroupRepository { - /// - /// Represents the UserGroupRepository for doing CRUD operations for - /// - public class UserGroupRepository : EntityRepositoryBase, IUserGroupRepository + private readonly PermissionRepository _permissionRepository; + private readonly IShortStringHelper _shortStringHelper; + private readonly UserGroupWithUsersRepository _userGroupWithUsersRepository; + + public UserGroupRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, + ILoggerFactory loggerFactory, IShortStringHelper shortStringHelper) + : base(scopeAccessor, appCaches, logger) { - private readonly IShortStringHelper _shortStringHelper; - private readonly UserGroupWithUsersRepository _userGroupWithUsersRepository; - private readonly PermissionRepository _permissionRepository; + _shortStringHelper = shortStringHelper; + _userGroupWithUsersRepository = new UserGroupWithUsersRepository(this, scopeAccessor, appCaches, + loggerFactory.CreateLogger()); + _permissionRepository = new PermissionRepository(scopeAccessor, appCaches, + loggerFactory.CreateLogger>()); + } - public UserGroupRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, ILoggerFactory loggerFactory, IShortStringHelper shortStringHelper) - : base(scopeAccessor, appCaches, logger) + public IUserGroup? Get(string alias) + { + try { - _shortStringHelper = shortStringHelper; - _userGroupWithUsersRepository = new UserGroupWithUsersRepository(this, scopeAccessor, appCaches, loggerFactory.CreateLogger()); - _permissionRepository = new PermissionRepository(scopeAccessor, appCaches, loggerFactory.CreateLogger>()); + //need to do a simple query to get the id - put this cache + var id = IsolatedCache.GetCacheItem(GetByAliasCacheKey(alias), () => + { + var groupId = + Database.ExecuteScalar("SELECT id FROM umbracoUserGroup WHERE userGroupAlias=@alias", + new {alias}); + if (groupId.HasValue == false) + { + throw new InvalidOperationException("No group found with alias " + alias); + } + + return groupId.Value; + }); + + //return from the normal method which will cache + return Get(id); } + catch (InvalidOperationException) + { + //if this is caught it's because we threw this in the caching method + return null; + } + } + + public IEnumerable GetGroupsAssignedToSection(string sectionAlias) + { + //Here we're building up a query that looks like this, a sub query is required because the resulting structure + // needs to still contain all of the section rows per user group. + + //SELECT * + //FROM [umbracoUserGroup] + //LEFT JOIN [umbracoUserGroup2App] + //ON [umbracoUserGroup].[id] = [umbracoUserGroup2App].[user] + //WHERE umbracoUserGroup.id IN (SELECT umbracoUserGroup.id + // FROM [umbracoUserGroup] + // LEFT JOIN [umbracoUserGroup2App] + // ON [umbracoUserGroup].[id] = [umbracoUserGroup2App].[user] + // WHERE umbracoUserGroup2App.app = 'content') + + Sql sql = GetBaseQuery(QueryType.Many); + Sql innerSql = GetBaseQuery(QueryType.Ids); + innerSql.Where("umbracoUserGroup2App.app = " + SqlSyntax.GetQuotedValue(sectionAlias)); + sql.Where($"umbracoUserGroup.id IN ({innerSql.SQL})"); + AppendGroupBy(sql); + + return Database.Fetch(sql).Select(x => UserGroupFactory.BuildEntity(_shortStringHelper, x)); + } + + public void AddOrUpdateGroupWithUsers(IUserGroup userGroup, int[]? userIds) => + _userGroupWithUsersRepository.Save(new UserGroupWithUsers(userGroup, userIds)); + + /// + /// Gets explicitly defined permissions for the group for specified entities + /// + /// + /// Array of entity Ids, if empty will return permissions for the group for all entities + public EntityPermissionCollection GetPermissions(int[] groupIds, params int[] entityIds) => + _permissionRepository.GetPermissionsForEntities(groupIds, entityIds); - public static string GetByAliasCacheKey(string alias) + /// + /// Gets explicit and default permissions (if requested) permissions for the group for specified entities + /// + /// + /// + /// If true will include the group's default permissions if no permissions are + /// explicitly assigned + /// + /// Array of entity Ids, if empty will return permissions for the group for all entities + public EntityPermissionCollection GetPermissions(IReadOnlyUserGroup[]? groups, bool fallbackToDefaultPermissions, + params int[] nodeIds) + { + if (groups == null) { - return CacheKeys.UserGroupGetByAliasCacheKeyPrefix + alias; + throw new ArgumentNullException(nameof(groups)); } - public IUserGroup? Get(string alias) + var groupIds = groups.Select(x => x.Id).ToArray(); + EntityPermissionCollection explicitPermissions = GetPermissions(groupIds, nodeIds); + var result = new EntityPermissionCollection(explicitPermissions); + + // If requested, and no permissions are assigned to a particular node, then we will fill in those permissions with the group's defaults + if (fallbackToDefaultPermissions) { - try - { - //need to do a simple query to get the id - put this cache - var id = IsolatedCache.GetCacheItem(GetByAliasCacheKey(alias), () => - { - var groupId = Database.ExecuteScalar("SELECT id FROM umbracoUserGroup WHERE userGroupAlias=@alias", new { alias }); - if (groupId.HasValue == false) throw new InvalidOperationException("No group found with alias " + alias); - return groupId.Value; - }); + //if no node ids are passed in, then we need to determine the node ids for the explicit permissions set + nodeIds = nodeIds.Length == 0 + ? explicitPermissions.Select(x => x.EntityId).Distinct().ToArray() + : nodeIds; - //return from the normal method which will cache - return Get(id); + //if there are still no nodeids we can just exit + if (nodeIds.Length == 0) + { + return result; } - catch (InvalidOperationException) + + foreach (IReadOnlyUserGroup group in groups) { - //if this is caught it's because we threw this in the caching method - return null; + foreach (var nodeId in nodeIds) + { + // TODO: We could/should change the EntityPermissionsCollection into a KeyedCollection and they key could be + // a struct of the nodeid + groupid so then we don't actually allocate this class just to check if it's not + // going to be included in the result! + + var defaultPermission = new EntityPermission(group.Id, nodeId, + group.Permissions?.ToArray() ?? Array.Empty(), true); + //Since this is a hashset, this will not add anything that already exists by group/node combination + result.Add(defaultPermission); + } } } - public IEnumerable GetGroupsAssignedToSection(string sectionAlias) - { - //Here we're building up a query that looks like this, a sub query is required because the resulting structure - // needs to still contain all of the section rows per user group. - - //SELECT * - //FROM [umbracoUserGroup] - //LEFT JOIN [umbracoUserGroup2App] - //ON [umbracoUserGroup].[id] = [umbracoUserGroup2App].[user] - //WHERE umbracoUserGroup.id IN (SELECT umbracoUserGroup.id - // FROM [umbracoUserGroup] - // LEFT JOIN [umbracoUserGroup2App] - // ON [umbracoUserGroup].[id] = [umbracoUserGroup2App].[user] - // WHERE umbracoUserGroup2App.app = 'content') - - var sql = GetBaseQuery(QueryType.Many); - var innerSql = GetBaseQuery(QueryType.Ids); - innerSql.Where("umbracoUserGroup2App.app = " + SqlSyntax.GetQuotedValue(sectionAlias)); - sql.Where($"umbracoUserGroup.id IN ({innerSql.SQL})"); - AppendGroupBy(sql); - - return Database.Fetch(sql).Select(x => UserGroupFactory.BuildEntity(_shortStringHelper, x)); - } + return result; + } - public void AddOrUpdateGroupWithUsers(IUserGroup userGroup, int[]? userIds) - { - _userGroupWithUsersRepository.Save(new UserGroupWithUsers(userGroup, userIds)); - } + /// + /// Replaces the same permission set for a single group to any number of entities + /// + /// Id of group + /// + /// Permissions as enumerable list of If nothing is specified all permissions + /// are removed. + /// + /// Specify the nodes to replace permissions for. + public void ReplaceGroupPermissions(int groupId, IEnumerable? permissions, params int[] entityIds) => + _permissionRepository.ReplacePermissions(groupId, permissions, entityIds); + /// + /// Assigns the same permission set for a single group to any number of entities + /// + /// Id of group + /// Permissions as enumerable list of + /// Specify the nodes to replace permissions for + public void AssignGroupPermission(int groupId, char permission, params int[] entityIds) => + _permissionRepository.AssignPermission(groupId, permission, entityIds); - /// - /// Gets explicitly defined permissions for the group for specified entities - /// - /// - /// Array of entity Ids, if empty will return permissions for the group for all entities - public EntityPermissionCollection GetPermissions(int[] groupIds, params int[] entityIds) + + public static string GetByAliasCacheKey(string alias) => CacheKeys.UserGroupGetByAliasCacheKeyPrefix + alias; + + /// + /// used to persist a user group with associated users at once + /// + private class UserGroupWithUsers : EntityBase + { + public UserGroupWithUsers(IUserGroup userGroup, int[]? userIds) { - return _permissionRepository.GetPermissionsForEntities(groupIds, entityIds); + UserGroup = userGroup; + UserIds = userIds; } - /// - /// Gets explicit and default permissions (if requested) permissions for the group for specified entities - /// - /// - /// If true will include the group's default permissions if no permissions are explicitly assigned - /// Array of entity Ids, if empty will return permissions for the group for all entities - public EntityPermissionCollection GetPermissions(IReadOnlyUserGroup[]? groups, bool fallbackToDefaultPermissions, params int[] nodeIds) - { - if (groups == null) throw new ArgumentNullException(nameof(groups)); + public override bool HasIdentity => UserGroup.HasIdentity; - var groupIds = groups.Select(x => x.Id).ToArray(); - var explicitPermissions = GetPermissions(groupIds, nodeIds); - var result = new EntityPermissionCollection(explicitPermissions); + public IUserGroup UserGroup { get; } + public int[]? UserIds { get; } + } - // If requested, and no permissions are assigned to a particular node, then we will fill in those permissions with the group's defaults - if (fallbackToDefaultPermissions) + /// + /// used to persist a user group with associated users at once + /// + private class UserGroupWithUsersRepository : EntityRepositoryBase + { + private readonly UserGroupRepository _userGroupRepo; + + public UserGroupWithUsersRepository(UserGroupRepository userGroupRepo, IScopeAccessor scopeAccessor, + AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) => + _userGroupRepo = userGroupRepo; + + protected override void PersistNewItem(UserGroupWithUsers entity) + { + //save the user group + _userGroupRepo.PersistNewItem(entity.UserGroup); + + if (entity.UserIds == null) { - //if no node ids are passed in, then we need to determine the node ids for the explicit permissions set - nodeIds = nodeIds.Length == 0 - ? explicitPermissions.Select(x => x.EntityId).Distinct().ToArray() - : nodeIds; + return; + } - //if there are still no nodeids we can just exit - if (nodeIds.Length == 0) - return result; + //now the user association + RefreshUsersInGroup(entity.UserGroup.Id, entity.UserIds); + } - foreach (var group in groups) - { - foreach (var nodeId in nodeIds) - { - // TODO: We could/should change the EntityPermissionsCollection into a KeyedCollection and they key could be - // a struct of the nodeid + groupid so then we don't actually allocate this class just to check if it's not - // going to be included in the result! - - var defaultPermission = new EntityPermission(group.Id, nodeId, group.Permissions?.ToArray() ?? Array.Empty(), isDefaultPermissions: true); - //Since this is a hashset, this will not add anything that already exists by group/node combination - result.Add(defaultPermission); - } - } + protected override void PersistUpdatedItem(UserGroupWithUsers entity) + { + //save the user group + _userGroupRepo.PersistUpdatedItem(entity.UserGroup); + + if (entity.UserIds == null) + { + return; } - return result; + //now the user association + RefreshUsersInGroup(entity.UserGroup.Id, entity.UserIds); } /// - /// Replaces the same permission set for a single group to any number of entities + /// Adds a set of users to a group, first removing any that exist /// /// Id of group - /// Permissions as enumerable list of If nothing is specified all permissions are removed. - /// Specify the nodes to replace permissions for. - public void ReplaceGroupPermissions(int groupId, IEnumerable? permissions, params int[] entityIds) + /// Ids of users + private void RefreshUsersInGroup(int groupId, int[] userIds) { - _permissionRepository.ReplacePermissions(groupId, permissions, entityIds); + RemoveAllUsersFromGroup(groupId); + AddUsersToGroup(groupId, userIds); } /// - /// Assigns the same permission set for a single group to any number of entities + /// Removes all users from a group /// /// Id of group - /// Permissions as enumerable list of - /// Specify the nodes to replace permissions for - public void AssignGroupPermission(int groupId, char permission, params int[] entityIds) - { - _permissionRepository.AssignPermission(groupId, permission, entityIds); - } - - #region Overrides of RepositoryBase + private void RemoveAllUsersFromGroup(int groupId) => + Database.Delete("WHERE userGroupId = @groupId", new {groupId}); - protected override IUserGroup? PerformGet(int id) + /// + /// Adds a set of users to a group + /// + /// Id of group + /// Ids of users + private void AddUsersToGroup(int groupId, int[] userIds) { - var sql = GetBaseQuery(QueryType.Single); - sql.Where(GetBaseWhereClause(), new { id = id }); - - AppendGroupBy(sql); - sql.OrderBy(x => x.Id); // required for references - - var dto = Database.FetchOneToMany(x => x.UserGroup2AppDtos, sql).FirstOrDefault(); - - if (dto == null) - return null; - - var userGroup = UserGroupFactory.BuildEntity(_shortStringHelper, dto); - return userGroup; + foreach (var userId in userIds) + { + var dto = new User2UserGroupDto {UserGroupId = groupId, UserId = userId}; + Database.Insert(dto); + } } - protected override IEnumerable PerformGetAll(params int[]? ids) - { - var sql = GetBaseQuery(QueryType.Many); + #region Not implemented (don't need to for the purposes of this repo) - if (ids?.Any() ?? false) - sql.WhereIn(x => x.Id, ids); - else - sql.Where(x => x.Id >= 0); + protected override UserGroupWithUsers PerformGet(int id) => + throw new InvalidOperationException("This method won't be implemented."); - AppendGroupBy(sql); - sql.OrderBy(x => x.Id); // required for references + protected override IEnumerable PerformGetAll(params int[]? ids) => + throw new InvalidOperationException("This method won't be implemented."); - var dtos = Database.FetchOneToMany(x => x.UserGroup2AppDtos, sql); - return dtos.Select(x=>UserGroupFactory.BuildEntity(_shortStringHelper, x)); - } + protected override IEnumerable PerformGetByQuery(IQuery query) => + throw new InvalidOperationException("This method won't be implemented."); - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(QueryType.Many); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); + protected override Sql GetBaseQuery(bool isCount) => + throw new InvalidOperationException("This method won't be implemented."); - AppendGroupBy(sql); - sql.OrderBy(x => x.Id); // required for references + protected override string GetBaseWhereClause() => + throw new InvalidOperationException("This method won't be implemented."); - var dtos = Database.FetchOneToMany(x => x.UserGroup2AppDtos, sql); - return dtos.Select(x => UserGroupFactory.BuildEntity(_shortStringHelper, x)); - } + protected override IEnumerable GetDeleteClauses() => + throw new InvalidOperationException("This method won't be implemented."); #endregion + } - #region Overrides of EntityRepositoryBase - - protected Sql GetBaseQuery(QueryType type) - { - var sql = Sql(); - var addFrom = false; + #region Overrides of RepositoryBase - switch (type) - { - case QueryType.Count: - sql - .SelectCount() - .From(); - break; - case QueryType.Ids: - sql - .Select(x => x.Id); - addFrom = true; - break; - case QueryType.Single: - case QueryType.Many: - sql - .Select(r => - r.Select(x => x.UserGroup2AppDtos), - s => s.Append($", COUNT({sql.Columns(x => x.UserId)}) AS {SqlSyntax.GetQuotedColumnName("UserCount")}")); - addFrom = true; - break; - default: - throw new NotSupportedException(type.ToString()); - } + protected override IUserGroup? PerformGet(int id) + { + Sql sql = GetBaseQuery(QueryType.Single); + sql.Where(GetBaseWhereClause(), new {id}); - if (addFrom) - sql - .From() - .LeftJoin() - .On(left => left.Id, right => right.UserGroupId) - .LeftJoin() - .On(left => left.UserGroupId, right => right.Id); + AppendGroupBy(sql); + sql.OrderBy(x => x.Id); // required for references - return sql; - } + UserGroupDto? dto = Database.FetchOneToMany(x => x.UserGroup2AppDtos, sql).FirstOrDefault(); - protected override Sql GetBaseQuery(bool isCount) + if (dto == null) { - return GetBaseQuery(isCount ? QueryType.Count : QueryType.Many); + return null; } - private static void AppendGroupBy(Sql sql) - { - sql - .GroupBy(x => x.CreateDate, x => x.Icon, x => x.Id, x => x.StartContentId, x => x.StartMediaId, - x => x.UpdateDate, x => x.Alias, x => x.DefaultPermissions, x => x.Name) - .AndBy(x => x.AppAlias, x => x.UserGroupId); - } + IUserGroup userGroup = UserGroupFactory.BuildEntity(_shortStringHelper, dto); + return userGroup; + } - protected override string GetBaseWhereClause() - { - return $"{Constants.DatabaseSchema.Tables.UserGroup}.id = @id"; - } + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = GetBaseQuery(QueryType.Many); - protected override IEnumerable GetDeleteClauses() + if (ids?.Any() ?? false) { - var list = new List - { - "DELETE FROM umbracoUser2UserGroup WHERE userGroupId = @id", - "DELETE FROM umbracoUserGroup2App WHERE userGroupId = @id", - "DELETE FROM umbracoUserGroup2Node WHERE userGroupId = @id", - "DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @id", - "DELETE FROM umbracoUserGroup WHERE id = @id" - }; - return list; + sql.WhereIn(x => x.Id, ids); } - - protected override void PersistNewItem(IUserGroup entity) + else { - entity.AddingEntity(); - - var userGroupDto = UserGroupFactory.BuildDto(entity); - - var id = Convert.ToInt32(Database.Insert(userGroupDto)); - entity.Id = id; - - PersistAllowedSections(entity); - - entity.ResetDirtyProperties(); + sql.Where(x => x.Id >= 0); } - protected override void PersistUpdatedItem(IUserGroup entity) - { - entity.UpdatingEntity(); - - var userGroupDto = UserGroupFactory.BuildDto(entity); + AppendGroupBy(sql); + sql.OrderBy(x => x.Id); // required for references - Database.Update(userGroupDto); + List? dtos = Database.FetchOneToMany(x => x.UserGroup2AppDtos, sql); + return dtos.Select(x => UserGroupFactory.BuildEntity(_shortStringHelper, x)); + } - PersistAllowedSections(entity); + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(QueryType.Many); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); - entity.ResetDirtyProperties(); - } + AppendGroupBy(sql); + sql.OrderBy(x => x.Id); // required for references - private void PersistAllowedSections(IUserGroup entity) - { - var userGroup = entity; + List? dtos = Database.FetchOneToMany(x => x.UserGroup2AppDtos, sql); + return dtos.Select(x => UserGroupFactory.BuildEntity(_shortStringHelper, x)); + } - // First delete all - Database.Delete("WHERE UserGroupId = @UserGroupId", new { UserGroupId = userGroup.Id }); + #endregion - // Then re-add any associated with the group - foreach (var app in userGroup.AllowedSections) - { - var dto = new UserGroup2AppDto - { - UserGroupId = userGroup.Id, - AppAlias = app - }; - Database.Insert(dto); - } - } + #region Overrides of EntityRepositoryBase - #endregion + protected Sql GetBaseQuery(QueryType type) + { + Sql sql = Sql(); + var addFrom = false; - /// - /// used to persist a user group with associated users at once - /// - private class UserGroupWithUsers : EntityBase + switch (type) { - public UserGroupWithUsers(IUserGroup userGroup, int[]? userIds) - { - UserGroup = userGroup; - UserIds = userIds; - } - - public override bool HasIdentity => UserGroup.HasIdentity; - - public IUserGroup UserGroup { get; } - public int[]? UserIds { get; } + case QueryType.Count: + sql + .SelectCount() + .From(); + break; + case QueryType.Ids: + sql + .Select(x => x.Id); + addFrom = true; + break; + case QueryType.Single: + case QueryType.Many: + sql + .Select(r => + r.Select(x => x.UserGroup2AppDtos), + s => s.Append( + $", COUNT({sql.Columns(x => x.UserId)}) AS {SqlSyntax.GetQuotedColumnName("UserCount")}")); + addFrom = true; + break; + default: + throw new NotSupportedException(type.ToString()); } - /// - /// used to persist a user group with associated users at once - /// - private class UserGroupWithUsersRepository : EntityRepositoryBase + if (addFrom) { - private readonly UserGroupRepository _userGroupRepo; + sql + .From() + .LeftJoin() + .On(left => left.Id, right => right.UserGroupId) + .LeftJoin() + .On(left => left.UserGroupId, right => right.Id); + } - public UserGroupWithUsersRepository(UserGroupRepository userGroupRepo, IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { - _userGroupRepo = userGroupRepo; - } + return sql; + } - #region Not implemented (don't need to for the purposes of this repo) + protected override Sql GetBaseQuery(bool isCount) => + GetBaseQuery(isCount ? QueryType.Count : QueryType.Many); - protected override UserGroupWithUsers PerformGet(int id) - { - throw new InvalidOperationException("This method won't be implemented."); - } + private static void AppendGroupBy(Sql sql) => + sql + .GroupBy(x => x.CreateDate, x => x.Icon, x => x.Id, x => x.StartContentId, + x => x.StartMediaId, + x => x.UpdateDate, x => x.Alias, x => x.DefaultPermissions, x => x.Name) + .AndBy(x => x.AppAlias, x => x.UserGroupId); - protected override IEnumerable PerformGetAll(params int[]? ids) - { - throw new InvalidOperationException("This method won't be implemented."); - } + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.UserGroup}.id = @id"; - protected override IEnumerable PerformGetByQuery(IQuery query) - { - throw new InvalidOperationException("This method won't be implemented."); - } + protected override IEnumerable GetDeleteClauses() + { + var list = new List + { + "DELETE FROM umbracoUser2UserGroup WHERE userGroupId = @id", + "DELETE FROM umbracoUserGroup2App WHERE userGroupId = @id", + "DELETE FROM umbracoUserGroup2Node WHERE userGroupId = @id", + "DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @id", + "DELETE FROM umbracoUserGroup WHERE id = @id" + }; + return list; + } - protected override Sql GetBaseQuery(bool isCount) - { - throw new InvalidOperationException("This method won't be implemented."); - } + protected override void PersistNewItem(IUserGroup entity) + { + entity.AddingEntity(); - protected override string GetBaseWhereClause() - { - throw new InvalidOperationException("This method won't be implemented."); - } + UserGroupDto userGroupDto = UserGroupFactory.BuildDto(entity); - protected override IEnumerable GetDeleteClauses() - { - throw new InvalidOperationException("This method won't be implemented."); - } + var id = Convert.ToInt32(Database.Insert(userGroupDto)); + entity.Id = id; - #endregion + PersistAllowedSections(entity); - protected override void PersistNewItem(UserGroupWithUsers entity) - { - //save the user group - _userGroupRepo.PersistNewItem(entity.UserGroup); + entity.ResetDirtyProperties(); + } - if (entity.UserIds == null) - return; + protected override void PersistUpdatedItem(IUserGroup entity) + { + entity.UpdatingEntity(); - //now the user association - RefreshUsersInGroup(entity.UserGroup.Id, entity.UserIds); - } + UserGroupDto userGroupDto = UserGroupFactory.BuildDto(entity); - protected override void PersistUpdatedItem(UserGroupWithUsers entity) - { - //save the user group - _userGroupRepo.PersistUpdatedItem(entity.UserGroup); + Database.Update(userGroupDto); - if (entity.UserIds == null) - return; + PersistAllowedSections(entity); - //now the user association - RefreshUsersInGroup(entity.UserGroup.Id, entity.UserIds); - } + entity.ResetDirtyProperties(); + } - /// - /// Adds a set of users to a group, first removing any that exist - /// - /// Id of group - /// Ids of users - private void RefreshUsersInGroup(int groupId, int[] userIds) - { - RemoveAllUsersFromGroup(groupId); - AddUsersToGroup(groupId, userIds); - } + private void PersistAllowedSections(IUserGroup entity) + { + IUserGroup userGroup = entity; - /// - /// Removes all users from a group - /// - /// Id of group - private void RemoveAllUsersFromGroup(int groupId) - { - Database.Delete("WHERE userGroupId = @groupId", new { groupId }); - } + // First delete all + Database.Delete("WHERE UserGroupId = @UserGroupId", new {UserGroupId = userGroup.Id}); - /// - /// Adds a set of users to a group - /// - /// Id of group - /// Ids of users - private void AddUsersToGroup(int groupId, int[] userIds) - { - foreach (var userId in userIds) - { - var dto = new User2UserGroupDto - { - UserGroupId = groupId, - UserId = userId, - }; - Database.Insert(dto); - } - } + // Then re-add any associated with the group + foreach (var app in userGroup.AllowedSections) + { + var dto = new UserGroup2AppDto {UserGroupId = userGroup.Id, AppAlias = app}; + Database.Insert(dto); } } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs index 106d1c906f57..9c40921ae31a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs @@ -1,7 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; +using System.Reflection; using System.Text; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -22,162 +20,171 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents the UserRepository for doing CRUD operations for +/// +internal class UserRepository : EntityRepositoryBase, IUserRepository { + private readonly GlobalSettings _globalSettings; + private readonly IJsonSerializer _jsonSerializer; + private readonly IMapperCollection _mapperCollection; + private readonly UserPasswordConfigurationSettings _passwordConfiguration; + private readonly IRuntimeState _runtimeState; + private bool _passwordConfigInitialized; + private string? _passwordConfigJson; + /// - /// Represents the UserRepository for doing CRUD operations for + /// Initializes a new instance of the class. /// - internal class UserRepository : EntityRepositoryBase, IUserRepository + /// The scope accessor. + /// The application caches. + /// The logger. + /// + /// A dictionary specifying the configuration for user passwords. If this is null then no + /// password configuration will be persisted or read. + /// + /// The global settings. + /// The password configuration. + /// The JSON serializer. + /// State of the runtime. + /// + /// mapperCollection + /// or + /// globalSettings + /// or + /// passwordConfiguration + /// + public UserRepository( + IScopeAccessor scopeAccessor, + AppCaches appCaches, + ILogger logger, + IMapperCollection mapperCollection, + IOptions globalSettings, + IOptions passwordConfiguration, + IJsonSerializer jsonSerializer, + IRuntimeState runtimeState) + : base(scopeAccessor, appCaches, logger) { - private readonly IMapperCollection _mapperCollection; - private readonly GlobalSettings _globalSettings; - private readonly UserPasswordConfigurationSettings _passwordConfiguration; - private readonly IJsonSerializer _jsonSerializer; - private readonly IRuntimeState _runtimeState; - private string? _passwordConfigJson; - private bool _passwordConfigInitialized; - - /// - /// Initializes a new instance of the class. - /// - /// The scope accessor. - /// The application caches. - /// The logger. - /// A dictionary specifying the configuration for user passwords. If this is null then no password configuration will be persisted or read. - /// The global settings. - /// The password configuration. - /// The JSON serializer. - /// State of the runtime. - /// mapperCollection - /// or - /// globalSettings - /// or - /// passwordConfiguration - public UserRepository( - IScopeAccessor scopeAccessor, - AppCaches appCaches, - ILogger logger, - IMapperCollection mapperCollection, - IOptions globalSettings, - IOptions passwordConfiguration, - IJsonSerializer jsonSerializer, - IRuntimeState runtimeState) - : base(scopeAccessor, appCaches, logger) - { - _mapperCollection = mapperCollection ?? throw new ArgumentNullException(nameof(mapperCollection)); - _globalSettings = globalSettings.Value ?? throw new ArgumentNullException(nameof(globalSettings)); - _passwordConfiguration = passwordConfiguration.Value ?? throw new ArgumentNullException(nameof(passwordConfiguration)); - _jsonSerializer = jsonSerializer; - _runtimeState = runtimeState; - } - - /// - /// Returns a serialized dictionary of the password configuration that is stored against the user in the database - /// - private string? DefaultPasswordConfigJson - { - get - { - if (_passwordConfigInitialized) - { - return _passwordConfigJson; - } - - var passwordConfig = new PersistedPasswordSettings - { - HashAlgorithm = _passwordConfiguration.HashAlgorithmType - }; + _mapperCollection = mapperCollection ?? throw new ArgumentNullException(nameof(mapperCollection)); + _globalSettings = globalSettings.Value ?? throw new ArgumentNullException(nameof(globalSettings)); + _passwordConfiguration = + passwordConfiguration.Value ?? throw new ArgumentNullException(nameof(passwordConfiguration)); + _jsonSerializer = jsonSerializer; + _runtimeState = runtimeState; + } - _passwordConfigJson = passwordConfig == null ? null : _jsonSerializer.Serialize(passwordConfig); - _passwordConfigInitialized = true; + /// + /// Returns a serialized dictionary of the password configuration that is stored against the user in the database + /// + private string? DefaultPasswordConfigJson + { + get + { + if (_passwordConfigInitialized) + { return _passwordConfigJson; } + + var passwordConfig = new PersistedPasswordSettings + { + HashAlgorithm = _passwordConfiguration.HashAlgorithmType + }; + + _passwordConfigJson = passwordConfig == null ? null : _jsonSerializer.Serialize(passwordConfig); + _passwordConfigInitialized = true; + return _passwordConfigJson; } + } - #region Overrides of RepositoryBase + private IEnumerable ConvertFromDtos(IEnumerable dtos) => + dtos.Select(x => UserFactory.BuildEntity(_globalSettings, x)); - protected override IUser? PerformGet(int id) - { - // This will never resolve to a user, yet this is asked - // for all of the time (especially in cases of members). - // Don't issue a SQL call for this, we know it will not exist. - if (_runtimeState.Level == RuntimeLevel.Upgrade) + #region Overrides of RepositoryBase + + protected override IUser? PerformGet(int id) + { + // This will never resolve to a user, yet this is asked + // for all of the time (especially in cases of members). + // Don't issue a SQL call for this, we know it will not exist. + if (_runtimeState.Level == RuntimeLevel.Upgrade) + { + // when upgrading people might come from version 7 where user 0 was the default, + // only in upgrade mode do we want to fetch the user of Id 0 + if (id < -1) { - // when upgrading people might come from version 7 where user 0 was the default, - // only in upgrade mode do we want to fetch the user of Id 0 - if (id < -1) - { - return null; - } + return null; } - else + } + else + { + if (id == default || id < -1) { - if (id == default || id < -1) - { - return null; - } + return null; } - - var sql = SqlContext.Sql() - .Select() - .From() - .Where(x => x.Id == id); - - var dtos = Database.Fetch(sql); - if (dtos.Count == 0) return null; - - PerformGetReferencedDtos(dtos); - return UserFactory.BuildEntity(_globalSettings, dtos[0]); } - /// - /// Returns a user by username - /// - /// - /// - /// Can be used for slightly faster user lookups if the result doesn't require security data (i.e. groups, apps & start nodes). - /// This is really only used for a shim in order to upgrade to 7.6. - /// - /// - /// A non cached instance - /// - public IUser? GetByUsername(string username, bool includeSecurityData) - { - return GetWith(sql => sql.Where(x => x.Login == username), includeSecurityData); - } + Sql sql = SqlContext.Sql() + .Select() + .From() + .Where(x => x.Id == id); - /// - /// Returns a user by id - /// - /// - /// - /// This is really only used for a shim in order to upgrade to 7.6 but could be used - /// for slightly faster user lookups if the result doesn't require security data (i.e. groups, apps & start nodes) - /// - /// - /// A non cached instance - /// - public IUser? Get(int? id, bool includeSecurityData) + List? dtos = Database.Fetch(sql); + if (dtos.Count == 0) { - return GetWith(sql => sql.Where(x => x.Id == id), includeSecurityData); + return null; } - public IProfile? GetProfile(string username) - { - var dto = GetDtoWith(sql => sql.Where(x => x.Login == username), false); - return dto == null ? null : new UserProfile(dto.Id, dto.UserName); - } + PerformGetReferencedDtos(dtos); + return UserFactory.BuildEntity(_globalSettings, dtos[0]); + } - public IProfile? GetProfile(int id) - { - var dto = GetDtoWith(sql => sql.Where(x => x.Id == id), false); - return dto == null ? null : new UserProfile(dto.Id, dto.UserName); - } + /// + /// Returns a user by username + /// + /// + /// + /// Can be used for slightly faster user lookups if the result doesn't require security data (i.e. groups, apps & start + /// nodes). + /// This is really only used for a shim in order to upgrade to 7.6. + /// + /// + /// A non cached instance + /// + public IUser? GetByUsername(string username, bool includeSecurityData) => + GetWith(sql => sql.Where(x => x.Login == username), includeSecurityData); - public IDictionary GetUserStates() - { - // These keys in this query map to the `Umbraco.Core.Models.Membership.UserState` enum - var sql = @"SELECT -1 AS [Key], COUNT(id) AS [Value] FROM umbracoUser + /// + /// Returns a user by id + /// + /// + /// + /// This is really only used for a shim in order to upgrade to 7.6 but could be used + /// for slightly faster user lookups if the result doesn't require security data (i.e. groups, apps & start nodes) + /// + /// + /// A non cached instance + /// + public IUser? Get(int? id, bool includeSecurityData) => + GetWith(sql => sql.Where(x => x.Id == id), includeSecurityData); + + public IProfile? GetProfile(string username) + { + UserDto? dto = GetDtoWith(sql => sql.Where(x => x.Login == username), false); + return dto == null ? null : new UserProfile(dto.Id, dto.UserName); + } + + public IProfile? GetProfile(int id) + { + UserDto? dto = GetDtoWith(sql => sql.Where(x => x.Id == id), false); + return dto == null ? null : new UserProfile(dto.Id, dto.UserName); + } + + public IDictionary GetUserStates() + { + // These keys in this query map to the `Umbraco.Core.Models.Membership.UserState` enum + var sql = @"SELECT -1 AS [Key], COUNT(id) AS [Value] FROM umbracoUser UNION SELECT 0 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0 AND userNoConsole = 0 AND lastLoginDate IS NOT NULL UNION @@ -189,752 +196,797 @@ public IDictionary GetUserStates() UNION SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0 AND userNoConsole = 0 AND lastLoginDate IS NULL"; - var result = Database.Dictionary(sql); + Dictionary? result = Database.Dictionary(sql); - return result.ToDictionary(x => (UserState)x.Key, x => x.Value); - } + return result.ToDictionary(x => (UserState)x.Key, x => x.Value); + } - public Guid CreateLoginSession(int? userId, string requestingIpAddress, bool cleanStaleSessions = true) + public Guid CreateLoginSession(int? userId, string requestingIpAddress, bool cleanStaleSessions = true) + { + DateTime now = DateTime.UtcNow; + var dto = new UserLoginDto { - var now = DateTime.UtcNow; - var dto = new UserLoginDto - { - UserId = userId, - IpAddress = requestingIpAddress, - LoggedInUtc = now, - LastValidatedUtc = now, - LoggedOutUtc = null, - SessionId = Guid.NewGuid() - }; - Database.Insert(dto); + UserId = userId, + IpAddress = requestingIpAddress, + LoggedInUtc = now, + LastValidatedUtc = now, + LoggedOutUtc = null, + SessionId = Guid.NewGuid() + }; + Database.Insert(dto); - if (cleanStaleSessions) - { - ClearLoginSessions(TimeSpan.FromDays(15)); - } - - return dto.SessionId; - } - - public bool ValidateLoginSession(int userId, Guid sessionId) + if (cleanStaleSessions) { - // with RepeatableRead transaction mode, read-then-update operations can - // cause deadlocks, and the ForUpdate() hint is required to tell the database - // to acquire an exclusive lock when reading - - // that query is going to run a *lot*, make it a template - var t = SqlContext.Templates.Get("Umbraco.Core.UserRepository.ValidateLoginSession", s => s - .Select() - .From() - .Where(x => x.SessionId == SqlTemplate.Arg("sessionId")) - .ForUpdate() - .SelectTop(1)); // Stick at end, SQL server syntax provider will insert at start of query after "select ", but sqlite will append limit to end. + ClearLoginSessions(TimeSpan.FromDays(15)); + } - var sql = t.Sql(sessionId); + return dto.SessionId; + } - var found = Database.FirstOrDefault(sql); - if (found == null || found.UserId != userId || found.LoggedOutUtc.HasValue) - return false; + public bool ValidateLoginSession(int userId, Guid sessionId) + { + // with RepeatableRead transaction mode, read-then-update operations can + // cause deadlocks, and the ForUpdate() hint is required to tell the database + // to acquire an exclusive lock when reading - //now detect if there's been a timeout - if (DateTime.UtcNow - found.LastValidatedUtc > _globalSettings.TimeOut) - { - //timeout detected, update the record - ClearLoginSession(sessionId); - return false; - } + // that query is going to run a *lot*, make it a template + SqlTemplate t = SqlContext.Templates.Get("Umbraco.Core.UserRepository.ValidateLoginSession", s => s + .Select() + .From() + .Where(x => x.SessionId == SqlTemplate.Arg("sessionId")) + .ForUpdate() + .SelectTop(1)); // Stick at end, SQL server syntax provider will insert at start of query after "select ", but sqlite will append limit to end. - //update the validate date - found.LastValidatedUtc = DateTime.UtcNow; - Database.Update(found); - return true; - } + Sql sql = t.Sql(sessionId); - public int ClearLoginSessions(int userId) + UserLoginDto? found = Database.FirstOrDefault(sql); + if (found == null || found.UserId != userId || found.LoggedOutUtc.HasValue) { - return Database.Delete(Sql().Where(x => x.UserId == userId)); + return false; } - public int ClearLoginSessions(TimeSpan timespan) + //now detect if there's been a timeout + if (DateTime.UtcNow - found.LastValidatedUtc > _globalSettings.TimeOut) { - var fromDate = DateTime.UtcNow - timespan; - return Database.Delete(Sql().Where(x => x.LastValidatedUtc < fromDate)); + //timeout detected, update the record + ClearLoginSession(sessionId); + return false; } - public void ClearLoginSession(Guid sessionId) - { - // TODO: why is that one updating and not deleting? - Database.Execute(Sql() - .Update(u => u.Set(x => x.LoggedOutUtc, DateTime.UtcNow)) - .Where(x => x.SessionId == sessionId)); - } + //update the validate date + found.LastValidatedUtc = DateTime.UtcNow; + Database.Update(found); + return true; + } - protected override IEnumerable PerformGetAll(params int[]? ids) - { - var dtos = ids?.Length == 0 - ? GetDtosWith(null, true) - : GetDtosWith(sql => sql.WhereIn(x => x.Id, ids), true); - var users = new IUser[dtos.Count]; - var i = 0; - foreach (var dto in dtos) - users[i++] = UserFactory.BuildEntity(_globalSettings, dto); - return users; - } + public int ClearLoginSessions(int userId) => + Database.Delete(Sql().Where(x => x.UserId == userId)); - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var dtos = GetDtosWith(sql => new SqlTranslator(sql, query).Translate(), true) - .DistinctBy(x => x.Id) - .ToList(); + public int ClearLoginSessions(TimeSpan timespan) + { + DateTime fromDate = DateTime.UtcNow - timespan; + return Database.Delete(Sql().Where(x => x.LastValidatedUtc < fromDate)); + } - var users = new IUser[dtos.Count]; - var i = 0; - foreach (var dto in dtos) - users[i++] = UserFactory.BuildEntity(_globalSettings, dto); - return users; - } + public void ClearLoginSession(Guid sessionId) => + // TODO: why is that one updating and not deleting? + Database.Execute(Sql() + .Update(u => u.Set(x => x.LoggedOutUtc, DateTime.UtcNow)) + .Where(x => x.SessionId == sessionId)); - private IUser? GetWith(Action> with, bool includeReferences) + protected override IEnumerable PerformGetAll(params int[]? ids) + { + List dtos = ids?.Length == 0 + ? GetDtosWith(null, true) + : GetDtosWith(sql => sql.WhereIn(x => x.Id, ids), true); + var users = new IUser[dtos.Count]; + var i = 0; + foreach (UserDto dto in dtos) { - var dto = GetDtoWith(with, includeReferences); - return dto == null ? null : UserFactory.BuildEntity(_globalSettings, dto); + users[i++] = UserFactory.BuildEntity(_globalSettings, dto); } - private UserDto? GetDtoWith(Action> with, bool includeReferences) + return users; + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + var dtos = GetDtosWith(sql => new SqlTranslator(sql, query).Translate(), true) + .DistinctBy(x => x.Id) + .ToList(); + + var users = new IUser[dtos.Count]; + var i = 0; + foreach (UserDto dto in dtos) { - var dtos = GetDtosWith(with, includeReferences); - return dtos.FirstOrDefault(); + users[i++] = UserFactory.BuildEntity(_globalSettings, dto); } - private List GetDtosWith(Action>? with, bool includeReferences) - { - var sql = SqlContext.Sql() - .Select() - .From(); + return users; + } + + private IUser? GetWith(Action> with, bool includeReferences) + { + UserDto? dto = GetDtoWith(with, includeReferences); + return dto == null ? null : UserFactory.BuildEntity(_globalSettings, dto); + } - with?.Invoke(sql); + private UserDto? GetDtoWith(Action> with, bool includeReferences) + { + List dtos = GetDtosWith(with, includeReferences); + return dtos.FirstOrDefault(); + } + + private List GetDtosWith(Action>? with, bool includeReferences) + { + Sql sql = SqlContext.Sql() + .Select() + .From(); - var dtos = Database.Fetch(sql); + with?.Invoke(sql); - if (includeReferences) - PerformGetReferencedDtos(dtos); + List? dtos = Database.Fetch(sql); - return dtos; + if (includeReferences) + { + PerformGetReferencedDtos(dtos); } - // NPoco cannot fetch 2+ references at a time - // plus it creates a combinatorial explosion - // better use extra queries - // unfortunately, SqlCe doesn't support multiple result sets - private void PerformGetReferencedDtos(List dtos) + return dtos; + } + + // NPoco cannot fetch 2+ references at a time + // plus it creates a combinatorial explosion + // better use extra queries + // unfortunately, SqlCe doesn't support multiple result sets + private void PerformGetReferencedDtos(List dtos) + { + if (dtos.Count == 0) { - if (dtos.Count == 0) return; + return; + } - var userIds = dtos.Count == 1 ? new List { dtos[0].Id } : dtos.Select(x => x.Id).ToList(); - var xUsers = dtos.Count == 1 ? null : dtos.ToDictionary(x => x.Id, x => x); + List userIds = dtos.Count == 1 ? new List {dtos[0].Id} : dtos.Select(x => x.Id).ToList(); + Dictionary? xUsers = dtos.Count == 1 ? null : dtos.ToDictionary(x => x.Id, x => x); - // get users2groups + // get users2groups - var sql = SqlContext.Sql() - .Select() - .From() - .WhereIn(x => x.UserId, userIds); + Sql sql = SqlContext.Sql() + .Select() + .From() + .WhereIn(x => x.UserId, userIds); - var users2groups = Database.Fetch(sql); - var groupIds = users2groups.Select(x => x.UserGroupId).ToList(); + List? users2groups = Database.Fetch(sql); + var groupIds = users2groups.Select(x => x.UserGroupId).ToList(); - // get groups + // get groups - sql = SqlContext.Sql() - .Select() - .From() - .WhereIn(x => x.Id, groupIds); + sql = SqlContext.Sql() + .Select() + .From() + .WhereIn(x => x.Id, groupIds); - var groups = Database.Fetch(sql) - .ToDictionary(x => x.Id, x => x); + var groups = Database.Fetch(sql) + .ToDictionary(x => x.Id, x => x); - // get groups2apps + // get groups2apps - sql = SqlContext.Sql() - .Select() - .From() - .WhereIn(x => x.UserGroupId, groupIds); + sql = SqlContext.Sql() + .Select() + .From() + .WhereIn(x => x.UserGroupId, groupIds); - var groups2apps = Database.Fetch(sql) - .GroupBy(x => x.UserGroupId) - .ToDictionary(x => x.Key, x => x); + var groups2apps = Database.Fetch(sql) + .GroupBy(x => x.UserGroupId) + .ToDictionary(x => x.Key, x => x); - // get start nodes + // get start nodes - sql = SqlContext.Sql() - .Select() - .From() - .WhereIn(x => x.UserId, userIds); + sql = SqlContext.Sql() + .Select() + .From() + .WhereIn(x => x.UserId, userIds); - var startNodes = Database.Fetch(sql); + List? startNodes = Database.Fetch(sql); - // map groups + // map groups - foreach (var user2group in users2groups) + foreach (User2UserGroupDto? user2group in users2groups) + { + if (groups.TryGetValue(user2group.UserGroupId, out UserGroupDto? group)) { - if (groups.TryGetValue(user2group.UserGroupId, out var group)) - { - var dto = xUsers == null ? dtos[0] : xUsers[user2group.UserId]; - dto.UserGroupDtos.Add(group); // user2group is distinct - } + UserDto dto = xUsers == null ? dtos[0] : xUsers[user2group.UserId]; + dto.UserGroupDtos.Add(group); // user2group is distinct } + } - // map start nodes + // map start nodes - foreach (var startNode in startNodes) - { - var dto = xUsers == null ? dtos[0] : xUsers[startNode.UserId]; - dto.UserStartNodeDtos.Add(startNode); // hashset = distinct - } + foreach (UserStartNodeDto? startNode in startNodes) + { + UserDto dto = xUsers == null ? dtos[0] : xUsers[startNode.UserId]; + dto.UserStartNodeDtos.Add(startNode); // hashset = distinct + } - // map apps + // map apps - foreach (var group in groups.Values) + foreach (UserGroupDto? group in groups.Values) + { + if (groups2apps.TryGetValue(group.Id, out IGrouping? list)) { - if (groups2apps.TryGetValue(group.Id, out var list)) - group.UserGroup2AppDtos = list.ToList(); // groups2apps is distinct + group.UserGroup2AppDtos = list.ToList(); // groups2apps is distinct } } + } - #endregion + #endregion - #region Overrides of EntityRepositoryBase + #region Overrides of EntityRepositoryBase - protected override Sql GetBaseQuery(bool isCount) + protected override Sql GetBaseQuery(bool isCount) + { + if (isCount) { - if (isCount) - return SqlContext.Sql() - .SelectCount() - .From(); - return SqlContext.Sql() - .Select() + .SelectCount() .From(); } - private static void AddGroupLeftJoin(Sql sql) - { - sql - .LeftJoin() - .On(left => left.UserId, right => right.Id) - .LeftJoin() - .On(left => left.Id, right => right.UserGroupId) - .LeftJoin() - .On(left => left.UserGroupId, right => right.Id) - .LeftJoin() - .On(left => left.UserId, right => right.Id); - } + return SqlContext.Sql() + .Select() + .From(); + } + + private static void AddGroupLeftJoin(Sql sql) => + sql + .LeftJoin() + .On(left => left.UserId, right => right.Id) + .LeftJoin() + .On(left => left.Id, right => right.UserGroupId) + .LeftJoin() + .On(left => left.UserGroupId, right => right.Id) + .LeftJoin() + .On(left => left.UserId, right => right.Id); + + private Sql GetBaseQuery(string columns) => + SqlContext.Sql() + .Select(columns) + .From(); + + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.User}.id = @id"; + + protected override IEnumerable GetDeleteClauses() + { + var list = new List + { + $"DELETE FROM {Constants.DatabaseSchema.Tables.UserLogin} WHERE userId = @id", + $"DELETE FROM {Constants.DatabaseSchema.Tables.User2UserGroup} WHERE userId = @id", + $"DELETE FROM {Constants.DatabaseSchema.Tables.User2NodeNotify} WHERE userId = @id", + $"DELETE FROM {Constants.DatabaseSchema.Tables.UserStartNode} WHERE userId = @id", + $"DELETE FROM {Constants.DatabaseSchema.Tables.User} WHERE id = @id", + $"DELETE FROM {Constants.DatabaseSchema.Tables.ExternalLogin} WHERE id = @id" + }; + return list; + } + + protected override void PersistNewItem(IUser entity) + { + entity.AddingEntity(); - private Sql GetBaseQuery(string columns) + // ensure security stamp if missing + if (entity.SecurityStamp.IsNullOrWhiteSpace()) { - return SqlContext.Sql() - .Select(columns) - .From(); + entity.SecurityStamp = Guid.NewGuid().ToString(); } - protected override string GetBaseWhereClause() + UserDto userDto = UserFactory.BuildDto(entity); + + // check if we have a user config else use the default + userDto.PasswordConfig = entity.PasswordConfiguration ?? DefaultPasswordConfigJson; + + var id = Convert.ToInt32(Database.Insert(userDto)); + entity.Id = id; + + if (entity.IsPropertyDirty("StartContentIds")) { - return $"{Constants.DatabaseSchema.Tables.User}.id = @id"; + AddingOrUpdateStartNodes(entity, Enumerable.Empty(), + UserStartNodeDto.StartNodeTypeValue.Content, entity.StartContentIds); } - protected override IEnumerable GetDeleteClauses() + if (entity.IsPropertyDirty("StartMediaIds")) { - var list = new List - { - $"DELETE FROM {Constants.DatabaseSchema.Tables.UserLogin} WHERE userId = @id", - $"DELETE FROM {Constants.DatabaseSchema.Tables.User2UserGroup} WHERE userId = @id", - $"DELETE FROM {Constants.DatabaseSchema.Tables.User2NodeNotify} WHERE userId = @id", - $"DELETE FROM {Constants.DatabaseSchema.Tables.UserStartNode} WHERE userId = @id", - $"DELETE FROM {Constants.DatabaseSchema.Tables.User} WHERE id = @id", - $"DELETE FROM {Constants.DatabaseSchema.Tables.ExternalLogin} WHERE id = @id" - }; - return list; + AddingOrUpdateStartNodes(entity, Enumerable.Empty(), + UserStartNodeDto.StartNodeTypeValue.Media, entity.StartMediaIds); } - protected override void PersistNewItem(IUser entity) + if (entity.IsPropertyDirty("Groups")) { - entity.AddingEntity(); + // lookup all assigned + List? assigned = entity.Groups == null || entity.Groups.Any() == false + ? new List() + : Database.Fetch("SELECT * FROM umbracoUserGroup WHERE userGroupAlias IN (@aliases)", + new {aliases = entity.Groups.Select(x => x.Alias)}); - // ensure security stamp if missing - if (entity.SecurityStamp.IsNullOrWhiteSpace()) + foreach (UserGroupDto? groupDto in assigned) { - entity.SecurityStamp = Guid.NewGuid().ToString(); + var dto = new User2UserGroupDto {UserGroupId = groupDto.Id, UserId = entity.Id}; + Database.Insert(dto); } + } - UserDto userDto = UserFactory.BuildDto(entity); - - // check if we have a user config else use the default - userDto.PasswordConfig = entity.PasswordConfiguration ?? DefaultPasswordConfigJson; + entity.ResetDirtyProperties(); + } - var id = Convert.ToInt32(Database.Insert(userDto)); - entity.Id = id; + protected override void PersistUpdatedItem(IUser entity) + { + // updates Modified date + entity.UpdatingEntity(); - if (entity.IsPropertyDirty("StartContentIds")) - { - AddingOrUpdateStartNodes(entity, Enumerable.Empty(), UserStartNodeDto.StartNodeTypeValue.Content, entity.StartContentIds); - } + // ensure security stamp if missing + if (entity.SecurityStamp.IsNullOrWhiteSpace()) + { + entity.SecurityStamp = Guid.NewGuid().ToString(); + } - if (entity.IsPropertyDirty("StartMediaIds")) - { - AddingOrUpdateStartNodes(entity, Enumerable.Empty(), UserStartNodeDto.StartNodeTypeValue.Media, entity.StartMediaIds); - } + UserDto userDto = UserFactory.BuildDto(entity); - if (entity.IsPropertyDirty("Groups")) - { - // lookup all assigned - var assigned = entity.Groups == null || entity.Groups.Any() == false - ? new List() - : Database.Fetch("SELECT * FROM umbracoUserGroup WHERE userGroupAlias IN (@aliases)", new { aliases = entity.Groups.Select(x => x.Alias) }); + // build list of columns to check for saving - we don't want to save the password if it hasn't changed! + // list the columns to save, NOTE: would be nice to not have hard coded strings here but no real good way around that + var colsToSave = new Dictionary + { + //TODO: Change these to constants + nameof + {"userDisabled", "IsApproved"}, + {"userNoConsole", "IsLockedOut"}, + {"startStructureID", "StartContentId"}, + {"startMediaID", "StartMediaId"}, + {"userName", "Name"}, + {"userLogin", "Username"}, + {"userEmail", "Email"}, + {"userLanguage", "Language"}, + {"securityStampToken", "SecurityStamp"}, + {"lastLockoutDate", "LastLockoutDate"}, + {"lastPasswordChangeDate", "LastPasswordChangeDate"}, + {"lastLoginDate", "LastLoginDate"}, + {"failedLoginAttempts", "FailedPasswordAttempts"}, + {"createDate", "CreateDate"}, + {"updateDate", "UpdateDate"}, + {"avatar", "Avatar"}, + {"emailConfirmedDate", "EmailConfirmedDate"}, + {"invitedDate", "InvitedDate"}, + {"tourData", "TourData"} + }; - foreach (var groupDto in assigned) - { - var dto = new User2UserGroupDto - { - UserGroupId = groupDto.Id, - UserId = entity.Id - }; - Database.Insert(dto); - } - } + // create list of properties that have changed + var changedCols = colsToSave + .Where(col => entity.IsPropertyDirty(col.Value)) + .Select(col => col.Key) + .ToList(); - entity.ResetDirtyProperties(); + if (entity.IsPropertyDirty("SecurityStamp")) + { + changedCols.Add("securityStampToken"); } - protected override void PersistUpdatedItem(IUser entity) + // DO NOT update the password if it has not changed or if it is null or empty + if (entity.IsPropertyDirty("RawPasswordValue") && entity.RawPasswordValue.IsNullOrWhiteSpace() == false) { - // updates Modified date - entity.UpdatingEntity(); + changedCols.Add("userPassword"); - // ensure security stamp if missing - if (entity.SecurityStamp.IsNullOrWhiteSpace()) + // If the security stamp hasn't already updated we need to force it + if (entity.IsPropertyDirty("SecurityStamp") == false) { - entity.SecurityStamp = Guid.NewGuid().ToString(); + userDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString(); + changedCols.Add("securityStampToken"); } - var userDto = UserFactory.BuildDto(entity); - - // build list of columns to check for saving - we don't want to save the password if it hasn't changed! - // list the columns to save, NOTE: would be nice to not have hard coded strings here but no real good way around that - var colsToSave = new Dictionary - { - //TODO: Change these to constants + nameof - {"userDisabled", "IsApproved"}, - {"userNoConsole", "IsLockedOut"}, - {"startStructureID", "StartContentId"}, - {"startMediaID", "StartMediaId"}, - {"userName", "Name"}, - {"userLogin", "Username"}, - {"userEmail", "Email"}, - {"userLanguage", "Language"}, - {"securityStampToken", "SecurityStamp"}, - {"lastLockoutDate", "LastLockoutDate"}, - {"lastPasswordChangeDate", "LastPasswordChangeDate"}, - {"lastLoginDate", "LastLoginDate"}, - {"failedLoginAttempts", "FailedPasswordAttempts"}, - {"createDate", "CreateDate"}, - {"updateDate", "UpdateDate"}, - {"avatar", "Avatar"}, - {"emailConfirmedDate", "EmailConfirmedDate"}, - {"invitedDate", "InvitedDate"}, - {"tourData", "TourData"} - }; + // check if we have a user config else use the default + userDto.PasswordConfig = entity.PasswordConfiguration ?? DefaultPasswordConfigJson; + changedCols.Add("passwordConfig"); + } - // create list of properties that have changed - var changedCols = colsToSave - .Where(col => entity.IsPropertyDirty(col.Value)) - .Select(col => col.Key) - .ToList(); + // If userlogin or the email has changed then need to reset security stamp + if (changedCols.Contains("userLogin") || changedCols.Contains("userEmail")) + { + userDto.EmailConfirmedDate = null; + changedCols.Add("emailConfirmedDate"); - if (entity.IsPropertyDirty("SecurityStamp")) + // If the security stamp hasn't already updated we need to force it + if (entity.IsPropertyDirty("SecurityStamp") == false) { + userDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString(); changedCols.Add("securityStampToken"); } + } - // DO NOT update the password if it has not changed or if it is null or empty - if (entity.IsPropertyDirty("RawPasswordValue") && entity.RawPasswordValue.IsNullOrWhiteSpace() == false) - { - changedCols.Add("userPassword"); - - // If the security stamp hasn't already updated we need to force it - if (entity.IsPropertyDirty("SecurityStamp") == false) - { - userDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString(); - changedCols.Add("securityStampToken"); - } - - // check if we have a user config else use the default - userDto.PasswordConfig = entity.PasswordConfiguration ?? DefaultPasswordConfigJson; - changedCols.Add("passwordConfig"); - } + //only update the changed cols + if (changedCols.Count > 0) + { + Database.Update(userDto, changedCols); + } - // If userlogin or the email has changed then need to reset security stamp - if (changedCols.Contains("userLogin") || changedCols.Contains("userEmail")) + if (entity.IsPropertyDirty("StartContentIds") || entity.IsPropertyDirty("StartMediaIds")) + { + List? assignedStartNodes = + Database.Fetch("SELECT * FROM umbracoUserStartNode WHERE userId = @userId", + new {userId = entity.Id}); + if (entity.IsPropertyDirty("StartContentIds")) { - userDto.EmailConfirmedDate = null; - changedCols.Add("emailConfirmedDate"); - - // If the security stamp hasn't already updated we need to force it - if (entity.IsPropertyDirty("SecurityStamp") == false) - { - userDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString(); - changedCols.Add("securityStampToken"); - } + AddingOrUpdateStartNodes(entity, assignedStartNodes, UserStartNodeDto.StartNodeTypeValue.Content, + entity.StartContentIds); } - //only update the changed cols - if (changedCols.Count > 0) + if (entity.IsPropertyDirty("StartMediaIds")) { - Database.Update(userDto, changedCols); + AddingOrUpdateStartNodes(entity, assignedStartNodes, UserStartNodeDto.StartNodeTypeValue.Media, + entity.StartMediaIds); } + } + + if (entity.IsPropertyDirty("Groups")) + { + //lookup all assigned + List? assigned = entity.Groups == null || entity.Groups.Any() == false + ? new List() + : Database.Fetch("SELECT * FROM umbracoUserGroup WHERE userGroupAlias IN (@aliases)", + new {aliases = entity.Groups.Select(x => x.Alias)}); - if (entity.IsPropertyDirty("StartContentIds") || entity.IsPropertyDirty("StartMediaIds")) + //first delete all + // TODO: We could do this a nicer way instead of "Nuke and Pave" + Database.Delete("WHERE UserId = @UserId", new {UserId = entity.Id}); + + foreach (UserGroupDto? groupDto in assigned) { - var assignedStartNodes = Database.Fetch("SELECT * FROM umbracoUserStartNode WHERE userId = @userId", new { userId = entity.Id }); - if (entity.IsPropertyDirty("StartContentIds")) - { - AddingOrUpdateStartNodes(entity, assignedStartNodes, UserStartNodeDto.StartNodeTypeValue.Content, entity.StartContentIds); - } - if (entity.IsPropertyDirty("StartMediaIds")) - { - AddingOrUpdateStartNodes(entity, assignedStartNodes, UserStartNodeDto.StartNodeTypeValue.Media, entity.StartMediaIds); - } + var dto = new User2UserGroupDto {UserGroupId = groupDto.Id, UserId = entity.Id}; + Database.Insert(dto); } + } - if (entity.IsPropertyDirty("Groups")) - { - //lookup all assigned - var assigned = entity.Groups == null || entity.Groups.Any() == false - ? new List() - : Database.Fetch("SELECT * FROM umbracoUserGroup WHERE userGroupAlias IN (@aliases)", new { aliases = entity.Groups.Select(x => x.Alias) }); + entity.ResetDirtyProperties(); + } - //first delete all - // TODO: We could do this a nicer way instead of "Nuke and Pave" - Database.Delete("WHERE UserId = @UserId", new { UserId = entity.Id }); + private void AddingOrUpdateStartNodes(IEntity entity, IEnumerable current, + UserStartNodeDto.StartNodeTypeValue startNodeType, int[]? entityStartIds) + { + if (entityStartIds is null) + { + return; + } - foreach (var groupDto in assigned) - { - var dto = new User2UserGroupDto - { - UserGroupId = groupDto.Id, - UserId = entity.Id - }; - Database.Insert(dto); - } - } + var assignedIds = current.Where(x => x.StartNodeType == (int)startNodeType).Select(x => x.StartNode).ToArray(); - entity.ResetDirtyProperties(); + //remove the ones not assigned to the entity + var toDelete = assignedIds.Except(entityStartIds).ToArray(); + if (toDelete.Length > 0) + { + Database.Delete("WHERE UserId = @UserId AND startNode IN (@startNodes)", + new {UserId = entity.Id, startNodes = toDelete}); } - private void AddingOrUpdateStartNodes(IEntity entity, IEnumerable current, UserStartNodeDto.StartNodeTypeValue startNodeType, int[]? entityStartIds) + //add the ones not currently in the db + var toAdd = entityStartIds.Except(assignedIds).ToArray(); + foreach (var i in toAdd) { - if (entityStartIds is null) - { - return; - } - var assignedIds = current.Where(x => x.StartNodeType == (int)startNodeType).Select(x => x.StartNode).ToArray(); - - //remove the ones not assigned to the entity - var toDelete = assignedIds.Except(entityStartIds).ToArray(); - if (toDelete.Length > 0) - Database.Delete("WHERE UserId = @UserId AND startNode IN (@startNodes)", new { UserId = entity.Id, startNodes = toDelete }); - //add the ones not currently in the db - var toAdd = entityStartIds.Except(assignedIds).ToArray(); - foreach (var i in toAdd) - { - var dto = new UserStartNodeDto - { - StartNode = i, - StartNodeType = (int)startNodeType, - UserId = entity.Id - }; - Database.Insert(dto); - } + var dto = new UserStartNodeDto {StartNode = i, StartNodeType = (int)startNodeType, UserId = entity.Id}; + Database.Insert(dto); } + } - #endregion + #endregion - #region Implementation of IUserRepository + #region Implementation of IUserRepository - public int GetCountByQuery(IQuery? query) - { - var sqlClause = GetBaseQuery("umbracoUser.id"); - var translator = new SqlTranslator(sqlClause, query); - var subquery = translator.Translate(); - //get the COUNT base query - var sql = GetBaseQuery(true) - .Append(new Sql("WHERE umbracoUser.id IN (" + subquery.SQL + ")", subquery.Arguments)); + public int GetCountByQuery(IQuery? query) + { + Sql sqlClause = GetBaseQuery("umbracoUser.id"); + var translator = new SqlTranslator(sqlClause, query); + Sql subquery = translator.Translate(); + //get the COUNT base query + Sql? sql = GetBaseQuery(true) + .Append(new Sql("WHERE umbracoUser.id IN (" + subquery.SQL + ")", subquery.Arguments)); + + return Database.ExecuteScalar(sql); + } - return Database.ExecuteScalar(sql); - } + public bool Exists(string username) => ExistsByUserName(username); - public bool Exists(string username) - { - return ExistsByUserName(username); - } + public bool ExistsByUserName(string username) + { + Sql sql = SqlContext.Sql() + .SelectCount() + .From() + .Where(x => x.UserName == username); - public bool ExistsByUserName(string username) - { - var sql = SqlContext.Sql() - .SelectCount() - .From() - .Where(x => x.UserName == username); + return Database.ExecuteScalar(sql) > 0; + } - return Database.ExecuteScalar(sql) > 0; - } + public bool ExistsByLogin(string login) + { + Sql sql = SqlContext.Sql() + .SelectCount() + .From() + .Where(x => x.Login == login); - public bool ExistsByLogin(string login) - { - var sql = SqlContext.Sql() - .SelectCount() - .From() - .Where(x => x.Login == login); + return Database.ExecuteScalar(sql) > 0; + } - return Database.ExecuteScalar(sql) > 0; - } + /// + /// Gets a list of objects associated with a given group + /// + /// Id of group + public IEnumerable GetAllInGroup(int groupId) => GetAllInOrNotInGroup(groupId, true); + + /// + /// Gets a list of objects not associated with a given group + /// + /// Id of group + public IEnumerable GetAllNotInGroup(int groupId) => GetAllInOrNotInGroup(groupId, false); + + private IEnumerable GetAllInOrNotInGroup(int groupId, bool include) + { + Sql sql = SqlContext.Sql() + .Select() + .From(); - /// - /// Gets a list of objects associated with a given group - /// - /// Id of group - public IEnumerable GetAllInGroup(int groupId) + Sql inSql = SqlContext.Sql() + .Select(x => x.UserId) + .From() + .Where(x => x.UserGroupId == groupId); + + if (include) { - return GetAllInOrNotInGroup(groupId, true); + sql.WhereIn(x => x.Id, inSql); } - - /// - /// Gets a list of objects not associated with a given group - /// - /// Id of group - public IEnumerable GetAllNotInGroup(int groupId) + else { - return GetAllInOrNotInGroup(groupId, false); + sql.WhereNotIn(x => x.Id, inSql); } - private IEnumerable GetAllInOrNotInGroup(int groupId, bool include) - { - var sql = SqlContext.Sql() - .Select() - .From(); - var inSql = SqlContext.Sql() - .Select(x => x.UserId) - .From() - .Where(x => x.UserGroupId == groupId); + List? dtos = Database.Fetch(sql); - if (include) - sql.WhereIn(x => x.Id, inSql); - else - sql.WhereNotIn(x => x.Id, inSql); + //adds missing bits like content and media start nodes + PerformGetReferencedDtos(dtos); + return ConvertFromDtos(dtos); + } - var dtos = Database.Fetch(sql); + /// + /// Gets paged user results + /// + /// + /// + /// + /// + /// + /// + /// + /// A filter to only include user that belong to these user groups + /// + /// + /// A filter to only include users that do not belong to these user groups + /// + /// Optional parameter to filter by specified user state + /// + /// + /// + /// The query supplied will ONLY work with data specifically on the umbracoUser table because we are using NPoco paging + /// (SQL paging) + /// + public IEnumerable GetPagedResultsByQuery(IQuery? query, long pageIndex, int pageSize, + out long totalRecords, + Expression> orderBy, Direction orderDirection = Direction.Ascending, + string[]? includeUserGroups = null, string[]? excludeUserGroups = null, UserState[]? userState = null, + IQuery? filter = null) + { + if (orderBy == null) + { + throw new ArgumentNullException(nameof(orderBy)); + } - //adds missing bits like content and media start nodes - PerformGetReferencedDtos(dtos); + Sql? filterSql = null; + Tuple[]? customFilterWheres = filter?.GetWhereClauses().ToArray(); + var hasCustomFilter = customFilterWheres != null && customFilterWheres.Length > 0; + if (hasCustomFilter + || (includeUserGroups != null && includeUserGroups.Length > 0) + || (excludeUserGroups != null && excludeUserGroups.Length > 0) + || (userState != null && userState.Length > 0 && userState.Contains(UserState.All) == false)) + { + filterSql = SqlContext.Sql(); + } - return ConvertFromDtos(dtos); - } - - /// - /// Gets paged user results - /// - /// - /// - /// - /// - /// - /// - /// - /// A filter to only include user that belong to these user groups - /// - /// - /// A filter to only include users that do not belong to these user groups - /// - /// Optional parameter to filter by specified user state - /// - /// - /// - /// The query supplied will ONLY work with data specifically on the umbracoUser table because we are using NPoco paging (SQL paging) - /// - public IEnumerable GetPagedResultsByQuery(IQuery? query, long pageIndex, int pageSize, out long totalRecords, - Expression> orderBy, Direction orderDirection = Direction.Ascending, - string[]? includeUserGroups = null, string[]? excludeUserGroups = null, UserState[]? userState = null, IQuery? filter = null) - { - if (orderBy == null) throw new ArgumentNullException(nameof(orderBy)); - - Sql? filterSql = null; - var customFilterWheres = filter?.GetWhereClauses().ToArray(); - var hasCustomFilter = customFilterWheres != null && customFilterWheres.Length > 0; - if (hasCustomFilter - || includeUserGroups != null && includeUserGroups.Length > 0 - || excludeUserGroups != null && excludeUserGroups.Length > 0 - || userState != null && userState.Length > 0 && userState.Contains(UserState.All) == false) - filterSql = SqlContext.Sql(); - - if (hasCustomFilter) + if (hasCustomFilter) + { + foreach (Tuple clause in customFilterWheres!) { - foreach (var clause in customFilterWheres!) - filterSql?.Append($"AND ({clause.Item1})", clause.Item2); + filterSql?.Append($"AND ({clause.Item1})", clause.Item2); } + } - if (includeUserGroups != null && includeUserGroups.Length > 0) - { - const string subQuery = @"AND (umbracoUser.id IN (SELECT DISTINCT umbracoUser.id + if (includeUserGroups != null && includeUserGroups.Length > 0) + { + const string subQuery = @"AND (umbracoUser.id IN (SELECT DISTINCT umbracoUser.id FROM umbracoUser INNER JOIN umbracoUser2UserGroup ON umbracoUser2UserGroup.userId = umbracoUser.id INNER JOIN umbracoUserGroup ON umbracoUserGroup.id = umbracoUser2UserGroup.userGroupId WHERE umbracoUserGroup.userGroupAlias IN (@userGroups)))"; - filterSql?.Append(subQuery, new { userGroups = includeUserGroups }); - } + filterSql?.Append(subQuery, new {userGroups = includeUserGroups}); + } - if (excludeUserGroups != null && excludeUserGroups.Length > 0) - { - const string subQuery = @"AND (umbracoUser.id NOT IN (SELECT DISTINCT umbracoUser.id + if (excludeUserGroups != null && excludeUserGroups.Length > 0) + { + const string subQuery = @"AND (umbracoUser.id NOT IN (SELECT DISTINCT umbracoUser.id FROM umbracoUser INNER JOIN umbracoUser2UserGroup ON umbracoUser2UserGroup.userId = umbracoUser.id INNER JOIN umbracoUserGroup ON umbracoUserGroup.id = umbracoUser2UserGroup.userGroupId WHERE umbracoUserGroup.userGroupAlias IN (@userGroups)))"; - filterSql?.Append(subQuery, new { userGroups = excludeUserGroups }); - } + filterSql?.Append(subQuery, new {userGroups = excludeUserGroups}); + } - if (userState != null && userState.Length > 0) + if (userState != null && userState.Length > 0) + { + //the "ALL" state doesn't require any filtering so we ignore that, if it exists in the list we don't do any filtering + if (userState.Contains(UserState.All) == false) { - //the "ALL" state doesn't require any filtering so we ignore that, if it exists in the list we don't do any filtering - if (userState.Contains(UserState.All) == false) + var sb = new StringBuilder("("); + var appended = false; + + if (userState.Contains(UserState.Active)) { - var sb = new StringBuilder("("); - var appended = false; + sb.Append("(userDisabled = 0 AND userNoConsole = 0 AND lastLoginDate IS NOT NULL)"); + appended = true; + } - if (userState.Contains(UserState.Active)) - { - sb.Append("(userDisabled = 0 AND userNoConsole = 0 AND lastLoginDate IS NOT NULL)"); - appended = true; - } - if (userState.Contains(UserState.Inactive)) + if (userState.Contains(UserState.Inactive)) + { + if (appended) { - if (appended) sb.Append(" OR "); - sb.Append("(userDisabled = 0 AND userNoConsole = 0 AND lastLoginDate IS NULL)"); - appended = true; + sb.Append(" OR "); } - if (userState.Contains(UserState.Disabled)) + + sb.Append("(userDisabled = 0 AND userNoConsole = 0 AND lastLoginDate IS NULL)"); + appended = true; + } + + if (userState.Contains(UserState.Disabled)) + { + if (appended) { - if (appended) sb.Append(" OR "); - sb.Append("(userDisabled = 1)"); - appended = true; + sb.Append(" OR "); } - if (userState.Contains(UserState.LockedOut)) + + sb.Append("(userDisabled = 1)"); + appended = true; + } + + if (userState.Contains(UserState.LockedOut)) + { + if (appended) { - if (appended) sb.Append(" OR "); - sb.Append("(userNoConsole = 1)"); - appended = true; + sb.Append(" OR "); } - if (userState.Contains(UserState.Invited)) + + sb.Append("(userNoConsole = 1)"); + appended = true; + } + + if (userState.Contains(UserState.Invited)) + { + if (appended) { - if (appended) sb.Append(" OR "); - sb.Append("(lastLoginDate IS NULL AND userDisabled = 1 AND invitedDate IS NOT NULL)"); - appended = true; + sb.Append(" OR "); } - sb.Append(")"); - filterSql?.Append("AND " + sb); + sb.Append("(lastLoginDate IS NULL AND userDisabled = 1 AND invitedDate IS NOT NULL)"); + appended = true; } + + sb.Append(")"); + filterSql?.Append("AND " + sb); } + } - // create base query - var sql = SqlContext.Sql() - .Select() - .From(); + // create base query + Sql sql = SqlContext.Sql() + .Select() + .From(); - // apply query - if (query != null) - sql = new SqlTranslator(sql, query).Translate(); + // apply query + if (query != null) + { + sql = new SqlTranslator(sql, query).Translate(); + } - // get sorted and filtered sql - var sqlNodeIdsWithSort = ApplySort(ApplyFilter(sql, filterSql, query != null), orderBy, orderDirection); + // get sorted and filtered sql + Sql sqlNodeIdsWithSort = + ApplySort(ApplyFilter(sql, filterSql, query != null), orderBy, orderDirection); - // get a page of results and total count - var pagedResult = Database.Page(pageIndex + 1, pageSize, sqlNodeIdsWithSort); - totalRecords = Convert.ToInt32(pagedResult.TotalItems); + // get a page of results and total count + Page? pagedResult = Database.Page(pageIndex + 1, pageSize, sqlNodeIdsWithSort); + totalRecords = Convert.ToInt32(pagedResult.TotalItems); - // map references - PerformGetReferencedDtos(pagedResult.Items); - return pagedResult.Items.Select(x => UserFactory.BuildEntity(_globalSettings, x)); - } + // map references + PerformGetReferencedDtos(pagedResult.Items); + return pagedResult.Items.Select(x => UserFactory.BuildEntity(_globalSettings, x)); + } - private Sql ApplyFilter(Sql sql, Sql? filterSql, bool hasWhereClause) + private Sql ApplyFilter(Sql sql, Sql? filterSql, bool hasWhereClause) + { + if (filterSql == null) { - if (filterSql == null) return sql; + return sql; + } - //ensure we don't append a WHERE if there is already one - var args = filterSql.Arguments; - var sqlFilter = hasWhereClause - ? filterSql.SQL - : " WHERE " + filterSql.SQL.TrimStart("AND "); + //ensure we don't append a WHERE if there is already one + var args = filterSql.Arguments; + var sqlFilter = hasWhereClause + ? filterSql.SQL + : " WHERE " + filterSql.SQL.TrimStart("AND "); - sql.Append(SqlContext.Sql(sqlFilter, args)); + sql.Append(SqlContext.Sql(sqlFilter, args)); - return sql; - } + return sql; + } - private Sql ApplySort(Sql sql, Expression> orderBy, Direction orderDirection) + private Sql ApplySort(Sql sql, Expression> orderBy, + Direction orderDirection) + { + if (orderBy == null) { - if (orderBy == null) return sql; - - var expressionMember = ExpressionHelper.GetMemberInfo(orderBy); - var mapper = _mapperCollection[typeof(IUser)]; - var mappedField = mapper.Map(expressionMember?.Name); + return sql; + } - if (mappedField.IsNullOrWhiteSpace()) - throw new ArgumentException("Could not find a mapping for the column specified in the orderBy clause"); + MemberInfo? expressionMember = ExpressionHelper.GetMemberInfo(orderBy); + BaseMapper mapper = _mapperCollection[typeof(IUser)]; + var mappedField = mapper.Map(expressionMember?.Name); - // beware! NPoco paging code parses the query to isolate the ORDER BY fragment, - // using a regex that wants "([\w\.\[\]\(\)\s""`,]+)" - meaning that anything - // else in orderBy is going to break NPoco / not be detected + if (mappedField.IsNullOrWhiteSpace()) + { + throw new ArgumentException("Could not find a mapping for the column specified in the orderBy clause"); + } - // beware! NPoco paging code (in PagingHelper) collapses everything [foo].[bar] - // to [bar] only, so we MUST use aliases, cannot use [table].[field] + // beware! NPoco paging code parses the query to isolate the ORDER BY fragment, + // using a regex that wants "([\w\.\[\]\(\)\s""`,]+)" - meaning that anything + // else in orderBy is going to break NPoco / not be detected - // beware! pre-2012 SqlServer is using a convoluted syntax for paging, which - // includes "SELECT ROW_NUMBER() OVER (ORDER BY ...) poco_rn FROM SELECT (...", - // so anything added here MUST also be part of the inner SELECT statement, ie - // the original statement, AND must be using the proper alias, as the inner SELECT - // will hide the original table.field names entirely + // beware! NPoco paging code (in PagingHelper) collapses everything [foo].[bar] + // to [bar] only, so we MUST use aliases, cannot use [table].[field] - var orderByField = sql.GetAliasedField(mappedField); + // beware! pre-2012 SqlServer is using a convoluted syntax for paging, which + // includes "SELECT ROW_NUMBER() OVER (ORDER BY ...) poco_rn FROM SELECT (...", + // so anything added here MUST also be part of the inner SELECT statement, ie + // the original statement, AND must be using the proper alias, as the inner SELECT + // will hide the original table.field names entirely - if (orderDirection == Direction.Ascending) - sql.OrderBy(orderByField); - else - sql.OrderByDescending(orderByField); + var orderByField = sql.GetAliasedField(mappedField); - return sql; + if (orderDirection == Direction.Ascending) + { + sql.OrderBy(orderByField); } - - public IEnumerable GetNextUsers(int id, int count) + else { - var idsQuery = SqlContext.Sql() - .Select(x => x.Id) - .From() - .Where(x => x.Id >= id) - .OrderBy(x => x.Id); - - // first page is index 1, not zero - var ids = Database.Page(1, count, idsQuery).Items.ToArray(); - - // now get the actual users and ensure they are ordered properly (same clause) - return ids.Length == 0 ? Enumerable.Empty() : GetMany(ids)?.OrderBy(x => x.Id) ?? Enumerable.Empty(); + sql.OrderByDescending(orderByField); } - #endregion + return sql; + } - private IEnumerable ConvertFromDtos(IEnumerable dtos) - { - return dtos.Select(x => UserFactory.BuildEntity(_globalSettings, x)); - } + public IEnumerable GetNextUsers(int id, int count) + { + Sql idsQuery = SqlContext.Sql() + .Select(x => x.Id) + .From() + .Where(x => x.Id >= id) + .OrderBy(x => x.Id); + + // first page is index 1, not zero + var ids = Database.Page(1, count, idsQuery).Items.ToArray(); + + // now get the actual users and ensure they are ordered properly (same clause) + return ids.Length == 0 + ? Enumerable.Empty() + : GetMany(ids)?.OrderBy(x => x.Id) ?? Enumerable.Empty(); } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/ScalarMapper.cs b/src/Umbraco.Infrastructure/Persistence/ScalarMapper.cs index 827aef193290..83c585dfd208 100644 --- a/src/Umbraco.Infrastructure/Persistence/ScalarMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/ScalarMapper.cs @@ -1,14 +1,12 @@ -using System; - namespace Umbraco.Cms.Infrastructure.Persistence; public abstract class ScalarMapper : IScalarMapper { + /// + object IScalarMapper.Map(object value) => Map(value)!; + /// - /// Performs a strongly typed mapping operation for a scalar value. + /// Performs a strongly typed mapping operation for a scalar value. /// protected abstract T Map(object value); - - /// - object IScalarMapper.Map(object value) => Map(value)!; } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlContext.cs b/src/Umbraco.Infrastructure/Persistence/SqlContext.cs index 6eb903c1f507..bf58fa50f6b2 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlContext.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlContext.cs @@ -1,60 +1,56 @@ -using System; -using System.Linq; using NPoco; -using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -using MapperCollection = Umbraco.Cms.Infrastructure.Persistence.Mappers.MapperCollection; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +/// +/// Implements . +/// +public class SqlContext : ISqlContext { /// - /// Implements . + /// Initializes a new instance of the class. /// - public class SqlContext : ISqlContext + /// The sql syntax provider. + /// The Poco data factory. + /// The database type. + /// The mappers. + public SqlContext(ISqlSyntaxProvider sqlSyntax, DatabaseType databaseType, IPocoDataFactory pocoDataFactory, + IMapperCollection? mappers = null) { - /// - /// Initializes a new instance of the class. - /// - /// The sql syntax provider. - /// The Poco data factory. - /// The database type. - /// The mappers. - public SqlContext(ISqlSyntaxProvider sqlSyntax, DatabaseType databaseType, IPocoDataFactory pocoDataFactory, IMapperCollection? mappers = null) - { - // for tests - Mappers = mappers; - - SqlSyntax = sqlSyntax ?? throw new ArgumentNullException(nameof(sqlSyntax)); - PocoDataFactory = pocoDataFactory ?? throw new ArgumentNullException(nameof(pocoDataFactory)); - DatabaseType = databaseType ?? throw new ArgumentNullException(nameof(databaseType)); - Templates = new SqlTemplates(this); - } - - /// - public ISqlSyntaxProvider SqlSyntax { get; } - - /// - public DatabaseType DatabaseType { get; } - - /// - public Sql Sql() => NPoco.Sql.BuilderFor((ISqlContext) this); - - /// - public Sql Sql(string sql, params object[] args) => Sql().Append(sql, args); - - /// - public IQuery Query() => new Query(this); - - /// - public SqlTemplates Templates { get; } - - /// - public IPocoDataFactory PocoDataFactory { get; } - - /// - public IMapperCollection? Mappers { get; } + // for tests + Mappers = mappers; + + SqlSyntax = sqlSyntax ?? throw new ArgumentNullException(nameof(sqlSyntax)); + PocoDataFactory = pocoDataFactory ?? throw new ArgumentNullException(nameof(pocoDataFactory)); + DatabaseType = databaseType ?? throw new ArgumentNullException(nameof(databaseType)); + Templates = new SqlTemplates(this); } + + /// + public ISqlSyntaxProvider SqlSyntax { get; } + + /// + public DatabaseType DatabaseType { get; } + + /// + public Sql Sql() => NPoco.Sql.BuilderFor((ISqlContext)this); + + /// + public Sql Sql(string sql, params object[] args) => Sql().Append(sql, args); + + /// + public IQuery Query() => new Query(this); + + /// + public SqlTemplates Templates { get; } + + /// + public IPocoDataFactory PocoDataFactory { get; } + + /// + public IMapperCollection? Mappers { get; } } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlContextExtensions.cs b/src/Umbraco.Infrastructure/Persistence/SqlContextExtensions.cs index b929b0e7ecf0..8dbc8e8b59ef 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlContextExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlContextExtensions.cs @@ -1,110 +1,113 @@ -using System; -using System.Linq.Expressions; +using System.Linq.Expressions; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Querying; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods to . +/// +public static class SqlContextExtensions { /// - /// Provides extension methods to . + /// Visit an expression. /// - public static class SqlContextExtensions + /// The type of the DTO. + /// An . + /// An expression to visit. + /// An optional table alias. + /// A SQL statement, and arguments, corresponding to the expression. + public static (string Sql, object[] Args) VisitDto(this ISqlContext sqlContext, + Expression> expression, string? alias = null) { - /// - /// Visit an expression. - /// - /// The type of the DTO. - /// An . - /// An expression to visit. - /// An optional table alias. - /// A SQL statement, and arguments, corresponding to the expression. - public static (string Sql, object[] Args) VisitDto(this ISqlContext sqlContext, Expression> expression, string? alias = null) - { - var visitor = new PocoToSqlExpressionVisitor(sqlContext, alias); - var visited = visitor.Visit(expression); - return (visited, visitor.GetSqlParameters()); - } + var visitor = new PocoToSqlExpressionVisitor(sqlContext, alias); + var visited = visitor.Visit(expression); + return (visited, visitor.GetSqlParameters()); + } - /// - /// Visit an expression. - /// - /// The type of the DTO. - /// The type returned by the expression. - /// An . - /// An expression to visit. - /// An optional table alias. - /// A SQL statement, and arguments, corresponding to the expression. - public static (string Sql, object[] Args) VisitDto(this ISqlContext sqlContext, Expression> expression, string? alias = null) - { - var visitor = new PocoToSqlExpressionVisitor(sqlContext, alias); - var visited = visitor.Visit(expression); - return (visited, visitor.GetSqlParameters()); - } + /// + /// Visit an expression. + /// + /// The type of the DTO. + /// The type returned by the expression. + /// An . + /// An expression to visit. + /// An optional table alias. + /// A SQL statement, and arguments, corresponding to the expression. + public static (string Sql, object[] Args) VisitDto(this ISqlContext sqlContext, + Expression> expression, string? alias = null) + { + var visitor = new PocoToSqlExpressionVisitor(sqlContext, alias); + var visited = visitor.Visit(expression); + return (visited, visitor.GetSqlParameters()); + } - /// - /// Visit an expression. - /// - /// The type of the first DTO. - /// The type of the second DTO. - /// An . - /// An expression to visit. - /// An optional table alias for the first DTO. - /// An optional table alias for the second DTO. - /// A SQL statement, and arguments, corresponding to the expression. - public static (string Sql, object[] Args) VisitDto(this ISqlContext sqlContext, Expression> expression, string? alias1 = null, string? alias2 = null) - { - var visitor = new PocoToSqlExpressionVisitor(sqlContext, alias1, alias2); - var visited = visitor.Visit(expression); - return (visited, visitor.GetSqlParameters()); - } + /// + /// Visit an expression. + /// + /// The type of the first DTO. + /// The type of the second DTO. + /// An . + /// An expression to visit. + /// An optional table alias for the first DTO. + /// An optional table alias for the second DTO. + /// A SQL statement, and arguments, corresponding to the expression. + public static (string Sql, object[] Args) VisitDto(this ISqlContext sqlContext, + Expression> expression, string? alias1 = null, string? alias2 = null) + { + var visitor = new PocoToSqlExpressionVisitor(sqlContext, alias1, alias2); + var visited = visitor.Visit(expression); + return (visited, visitor.GetSqlParameters()); + } - /// - /// Visit an expression. - /// - /// The type of the first DTO. - /// The type of the second DTO. - /// The type returned by the expression. - /// An . - /// An expression to visit. - /// An optional table alias for the first DTO. - /// An optional table alias for the second DTO. - /// A SQL statement, and arguments, corresponding to the expression. - public static (string Sql, object[] Args) VisitDto(this ISqlContext sqlContext, Expression> expression, string? alias1 = null, string? alias2 = null) - { - var visitor = new PocoToSqlExpressionVisitor(sqlContext, alias1, alias2); - var visited = visitor.Visit(expression); - return (visited, visitor.GetSqlParameters()); - } + /// + /// Visit an expression. + /// + /// The type of the first DTO. + /// The type of the second DTO. + /// The type returned by the expression. + /// An . + /// An expression to visit. + /// An optional table alias for the first DTO. + /// An optional table alias for the second DTO. + /// A SQL statement, and arguments, corresponding to the expression. + public static (string Sql, object[] Args) VisitDto(this ISqlContext sqlContext, + Expression> expression, string? alias1 = null, string? alias2 = null) + { + var visitor = new PocoToSqlExpressionVisitor(sqlContext, alias1, alias2); + var visited = visitor.Visit(expression); + return (visited, visitor.GetSqlParameters()); + } - /// - /// Visit a model expression. - /// - /// The type of the model. - /// An . - /// An expression to visit. - /// A SQL statement, and arguments, corresponding to the expression. - public static (string Sql, object[] Args) VisitModel(this ISqlContext sqlContext, Expression> expression) - { - var visitor = new ModelToSqlExpressionVisitor(sqlContext.SqlSyntax, sqlContext.Mappers); - var visited = visitor.Visit(expression); - return (visited, visitor.GetSqlParameters()); - } + /// + /// Visit a model expression. + /// + /// The type of the model. + /// An . + /// An expression to visit. + /// A SQL statement, and arguments, corresponding to the expression. + public static (string Sql, object[] Args) VisitModel(this ISqlContext sqlContext, + Expression> expression) + { + var visitor = new ModelToSqlExpressionVisitor(sqlContext.SqlSyntax, sqlContext.Mappers); + var visited = visitor.Visit(expression); + return (visited, visitor.GetSqlParameters()); + } - /// - /// Visit a model expression representing a field. - /// - /// The type of the model. - /// An . - /// An expression to visit, representing a field. - /// The name of the field. - public static string VisitModelField(this ISqlContext sqlContext, Expression> field) - { - var (sql, _) = sqlContext.VisitModel(field); + /// + /// Visit a model expression representing a field. + /// + /// The type of the model. + /// An . + /// An expression to visit, representing a field. + /// The name of the field. + public static string VisitModelField(this ISqlContext sqlContext, Expression> field) + { + var (sql, _) = sqlContext.VisitModel(field); - // going to return " = @0" - // take the first part only - var pos = sql.IndexOf(' '); - return sql.Substring(0, pos); - } + // going to return " = @0" + // take the first part only + var pos = sql.IndexOf(' '); + return sql.Substring(0, pos); } } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlServerDbProviderFactoryCreator.cs b/src/Umbraco.Infrastructure/Persistence/SqlServerDbProviderFactoryCreator.cs index e69de29bb2d1..8b137891791f 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlServerDbProviderFactoryCreator.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlServerDbProviderFactoryCreator.cs @@ -0,0 +1 @@ + diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ColumnInfo.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ColumnInfo.cs index fed6e221b569..59f6f93ee46a 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ColumnInfo.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ColumnInfo.cs @@ -1,40 +1,40 @@ -namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax +namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; + +public class ColumnInfo { - public class ColumnInfo + public ColumnInfo(string tableName, string columnName, int ordinal, string columnDefault, string isNullable, + string dataType) { - public ColumnInfo(string tableName, string columnName, int ordinal, string columnDefault, string isNullable, string dataType) - { - TableName = tableName; - ColumnName = columnName; - Ordinal = ordinal; - ColumnDefault = columnDefault; - IsNullable = isNullable.Equals("YES"); - DataType = dataType; - } - - public ColumnInfo(string tableName, string columnName, int ordinal, string isNullable, string dataType) - { - TableName = tableName; - ColumnName = columnName; - Ordinal = ordinal; - IsNullable = isNullable.Equals("YES"); - DataType = dataType; - } + TableName = tableName; + ColumnName = columnName; + Ordinal = ordinal; + ColumnDefault = columnDefault; + IsNullable = isNullable.Equals("YES"); + DataType = dataType; + } - public ColumnInfo(string tableName, string columnName, int ordinal, bool isNullable, string dataType) - { - TableName = tableName; - ColumnName = columnName; - Ordinal = ordinal; - IsNullable = isNullable; - DataType = dataType; - } + public ColumnInfo(string tableName, string columnName, int ordinal, string isNullable, string dataType) + { + TableName = tableName; + ColumnName = columnName; + Ordinal = ordinal; + IsNullable = isNullable.Equals("YES"); + DataType = dataType; + } - public string TableName { get; set; } - public string ColumnName { get; set; } - public int Ordinal { get; set; } - public string? ColumnDefault { get; set; } - public bool IsNullable { get; set; } - public string DataType { get; set; } + public ColumnInfo(string tableName, string columnName, int ordinal, bool isNullable, string dataType) + { + TableName = tableName; + ColumnName = columnName; + Ordinal = ordinal; + IsNullable = isNullable; + DataType = dataType; } + + public string TableName { get; set; } + public string ColumnName { get; set; } + public int Ordinal { get; set; } + public string? ColumnDefault { get; set; } + public bool IsNullable { get; set; } + public string DataType { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypes.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypes.cs index 18e4791d0b9b..edd180af46c3 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypes.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypes.cs @@ -1,18 +1,15 @@ -using System; -using System.Collections.Generic; using System.Data; -namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax +namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; + +public class DbTypes { - public class DbTypes + public DbTypes(IReadOnlyDictionary columnTypeMap, IReadOnlyDictionary columnDbTypeMap) { - public DbTypes(IReadOnlyDictionary columnTypeMap, IReadOnlyDictionary columnDbTypeMap) - { - ColumnTypeMap = columnTypeMap; - ColumnDbTypeMap = columnDbTypeMap; - } - - public IReadOnlyDictionary ColumnTypeMap { get; } - public IReadOnlyDictionary ColumnDbTypeMap { get; } + ColumnTypeMap = columnTypeMap; + ColumnDbTypeMap = columnDbTypeMap; } + + public IReadOnlyDictionary ColumnTypeMap { get; } + public IReadOnlyDictionary ColumnDbTypeMap { get; } } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypesFactory.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypesFactory.cs index bf1e0989f573..cf5bdb1b2e13 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypesFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypesFactory.cs @@ -1,20 +1,17 @@ -using System; -using System.Collections.Generic; -using System.Data; +using System.Data; -namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax -{ - internal class DbTypesFactory - { - private readonly Dictionary _columnTypeMap = new Dictionary(); - private readonly Dictionary _columnDbTypeMap = new Dictionary(); +namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; - public void Set(DbType dbType, string fieldDefinition) - { - _columnTypeMap[typeof(T)] = fieldDefinition; - _columnDbTypeMap[typeof(T)] = dbType; - } +internal class DbTypesFactory +{ + private readonly Dictionary _columnDbTypeMap = new(); + private readonly Dictionary _columnTypeMap = new(); - public DbTypes Create() => new DbTypes(_columnTypeMap, _columnDbTypeMap); + public void Set(DbType dbType, string fieldDefinition) + { + _columnTypeMap[typeof(T)] = fieldDefinition; + _columnDbTypeMap[typeof(T)] = dbType; } + + public DbTypes Create() => new(_columnTypeMap, _columnDbTypeMap); } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs index 7760f8647665..91cca0d092af 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs @@ -1,166 +1,169 @@ -using System; -using System.Collections.Generic; using System.Data; -using System.Linq.Expressions; using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; using System.Text.RegularExpressions; using NPoco; using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax +namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; + +/// +/// Defines an SqlSyntaxProvider +/// +public interface ISqlSyntaxProvider { + string ProviderName { get; } + string CreateTable { get; } + string DropTable { get; } + string AddColumn { get; } + string DropColumn { get; } + string AlterColumn { get; } + string RenameColumn { get; } + string RenameTable { get; } + string CreateSchema { get; } + string AlterSchema { get; } + string DropSchema { get; } + string CreateIndex { get; } + string DropIndex { get; } + string InsertData { get; } + string UpdateData { get; } + string DeleteData { get; } + string TruncateTable { get; } + string CreateConstraint { get; } + string DeleteConstraint { get; } + + string DeleteDefaultConstraint { get; } + + /// + /// Gets a regex matching aliased fields. + /// + /// + /// Matches "(table.column) AS (alias)" where table, column and alias are properly escaped. + /// + Regex AliasRegex { get; } + + string ConvertIntegerToOrderableString { get; } + string ConvertDateToOrderableString { get; } + string ConvertDecimalToOrderableString { get; } + + /// + /// Returns the default isolation level for the database + /// + IsolationLevel DefaultIsolationLevel { get; } + + string DbProvider { get; } + + IDictionary? ScalarMappers => null; + + DatabaseType GetUpdatedDatabaseType(DatabaseType current, string? connectionString) => + current; // Default implementation. + + string EscapeString(string val); + + string GetWildcardPlaceholder(); + string GetStringColumnEqualComparison(string column, int paramIndex, TextColumnType columnType); + string GetStringColumnWildcardComparison(string column, int paramIndex, TextColumnType columnType); + string GetConcat(params string[] args); + + string GetColumn(DatabaseType dbType, string tableName, string columnName, string columnAlias, + string? referenceName = null, bool forInsert = false); + + string GetQuotedTableName(string? tableName); + string GetQuotedColumnName(string? columnName); + string GetQuotedName(string? name); + bool DoesTableExist(IDatabase db, string tableName); + string GetIndexType(IndexTypes indexTypes); + string GetSpecialDbType(SpecialDbType dbType); + string FormatDateTime(DateTime date, bool includeTime = true); + string Format(TableDefinition table); + string Format(IEnumerable columns); + List Format(IEnumerable indexes); + List Format(IEnumerable foreignKeys); + string FormatPrimaryKey(TableDefinition table); + string GetQuotedValue(string value); + string Format(ColumnDefinition column); + string Format(ColumnDefinition column, string tableName, out IEnumerable sqls); + string Format(IndexDefinition index); + string Format(ForeignKeyDefinition foreignKey); + string FormatColumnRename(string? tableName, string? oldName, string? newName); + string FormatTableRename(string? oldName, string? newName); + + void HandleCreateTable(IDatabase database, TableDefinition tableDefinition, bool skipKeysAndIndexes = false); + + Sql SelectTop(Sql sql, int top); + + bool SupportsClustered(); + bool SupportsIdentityInsert(); + IEnumerable GetTablesInSchema(IDatabase db); + IEnumerable GetColumnsInSchema(IDatabase db); + + /// + /// Returns all constraints defined in the database (Primary keys, foreign keys, unique constraints...) (does not + /// include indexes) + /// + /// + /// + /// A Tuple containing: TableName, ConstraintName + /// + IEnumerable> GetConstraintsPerTable(IDatabase db); + + /// + /// Returns all constraints defined in the database (Primary keys, foreign keys, unique constraints...) (does not + /// include indexes) + /// + /// + /// + /// A Tuple containing: TableName, ColumnName, ConstraintName + /// + IEnumerable> GetConstraintsPerColumn(IDatabase db); + + /// + /// Returns all defined Indexes in the database excluding primary keys + /// + /// + /// + /// A Tuple containing: TableName, IndexName, ColumnName, IsUnique + /// + IEnumerable> GetDefinedIndexes(IDatabase db); + + /// + /// Tries to gets the name of the default constraint on a column. + /// + /// The database. + /// The table name. + /// The column name. + /// The constraint name. + /// A value indicating whether a default constraint was found. + /// + /// + /// Some database engines may not have names for default constraints, + /// in which case the function may return true, but is + /// unspecified. + /// + /// + bool TryGetDefaultConstraint(IDatabase db, string? tableName, string columnName, + [MaybeNullWhen(false)] out string constraintName); + + + string GetFieldNameForUpdate(Expression> fieldSelector, string? tableAlias = null); + + /// + /// Appends the relevant ForUpdate hint. + /// + Sql InsertForUpdateHint(Sql sql); + + /// + /// Appends the relevant ForUpdate hint. + /// + Sql AppendForUpdateHint(Sql sql); + /// - /// Defines an SqlSyntaxProvider + /// Handles left join with nested join /// - public interface ISqlSyntaxProvider - { - DatabaseType GetUpdatedDatabaseType(DatabaseType current, string? connectionString) => - current; // Default implementation. - - string ProviderName { get; } - - string EscapeString(string val); - - string GetWildcardPlaceholder(); - string GetStringColumnEqualComparison(string column, int paramIndex, TextColumnType columnType); - string GetStringColumnWildcardComparison(string column, int paramIndex, TextColumnType columnType); - string GetConcat(params string[] args); - - string GetColumn(DatabaseType dbType, string tableName, string columnName, string columnAlias, string? referenceName = null, bool forInsert = false); - - string GetQuotedTableName(string? tableName); - string GetQuotedColumnName(string? columnName); - string GetQuotedName(string? name); - bool DoesTableExist(IDatabase db, string tableName); - string GetIndexType(IndexTypes indexTypes); - string GetSpecialDbType(SpecialDbType dbType); - string CreateTable { get; } - string DropTable { get; } - string AddColumn { get; } - string DropColumn { get; } - string AlterColumn { get; } - string RenameColumn { get; } - string RenameTable { get; } - string CreateSchema { get; } - string AlterSchema { get; } - string DropSchema { get; } - string CreateIndex { get; } - string DropIndex { get; } - string InsertData { get; } - string UpdateData { get; } - string DeleteData { get; } - string TruncateTable { get; } - string CreateConstraint { get; } - string DeleteConstraint { get; } - - string DeleteDefaultConstraint { get; } - string FormatDateTime(DateTime date, bool includeTime = true); - string Format(TableDefinition table); - string Format(IEnumerable columns); - List Format(IEnumerable indexes); - List Format(IEnumerable foreignKeys); - string FormatPrimaryKey(TableDefinition table); - string GetQuotedValue(string value); - string Format(ColumnDefinition column); - string Format(ColumnDefinition column, string tableName, out IEnumerable sqls); - string Format(IndexDefinition index); - string Format(ForeignKeyDefinition foreignKey); - string FormatColumnRename(string? tableName, string? oldName, string? newName); - string FormatTableRename(string? oldName, string? newName); - - void HandleCreateTable(IDatabase database, TableDefinition tableDefinition, bool skipKeysAndIndexes = false); - - /// - /// Gets a regex matching aliased fields. - /// - /// - /// Matches "(table.column) AS (alias)" where table, column and alias are properly escaped. - /// - Regex AliasRegex { get; } - - Sql SelectTop(Sql sql, int top); - - bool SupportsClustered(); - bool SupportsIdentityInsert(); - - string ConvertIntegerToOrderableString { get; } - string ConvertDateToOrderableString { get; } - string ConvertDecimalToOrderableString { get; } - - /// - /// Returns the default isolation level for the database - /// - IsolationLevel DefaultIsolationLevel { get; } - - string DbProvider { get; } - IEnumerable GetTablesInSchema(IDatabase db); - IEnumerable GetColumnsInSchema(IDatabase db); - - /// - /// Returns all constraints defined in the database (Primary keys, foreign keys, unique constraints...) (does not include indexes) - /// - /// - /// - /// A Tuple containing: TableName, ConstraintName - /// - IEnumerable> GetConstraintsPerTable(IDatabase db); - - /// - /// Returns all constraints defined in the database (Primary keys, foreign keys, unique constraints...) (does not include indexes) - /// - /// - /// - /// A Tuple containing: TableName, ColumnName, ConstraintName - /// - IEnumerable> GetConstraintsPerColumn(IDatabase db); - - /// - /// Returns all defined Indexes in the database excluding primary keys - /// - /// - /// - /// A Tuple containing: TableName, IndexName, ColumnName, IsUnique - /// - IEnumerable> GetDefinedIndexes(IDatabase db); - - /// - /// Tries to gets the name of the default constraint on a column. - /// - /// The database. - /// The table name. - /// The column name. - /// The constraint name. - /// A value indicating whether a default constraint was found. - /// - /// Some database engines may not have names for default constraints, - /// in which case the function may return true, but is - /// unspecified. - /// - bool TryGetDefaultConstraint(IDatabase db, string? tableName, string columnName, [MaybeNullWhen(false)] out string constraintName); - - - string GetFieldNameForUpdate(Expression> fieldSelector, string? tableAlias = null); - - /// - /// Appends the relevant ForUpdate hint. - /// - Sql InsertForUpdateHint(Sql sql); - - /// - /// Appends the relevant ForUpdate hint. - /// - Sql AppendForUpdateHint(Sql sql); - - /// - /// Handles left join with nested join - /// - Sql.SqlJoinClause LeftJoinWithNestedJoin( - Sql sql, - Func, Sql> nestedJoin, - string? alias = null); - - IDictionary? ScalarMappers => null; - } + Sql.SqlJoinClause LeftJoinWithNestedJoin( + Sql sql, + Func, Sql> nestedJoin, + string? alias = null); } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerVersionName.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerVersionName.cs index a3efde1731cf..0083b102a1c4 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerVersionName.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerVersionName.cs @@ -1,21 +1,20 @@ -namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax +namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; + +/// +/// Represents the version name of SQL server (i.e. the year 2008, 2005, etc...) +/// +/// +/// see: https://support.microsoft.com/en-us/kb/321185 +/// +internal enum SqlServerVersionName { - /// - /// Represents the version name of SQL server (i.e. the year 2008, 2005, etc...) - /// - /// - /// see: https://support.microsoft.com/en-us/kb/321185 - /// - internal enum SqlServerVersionName - { - Invalid = -1, - V7 = 0, - V2000 = 1, - V2005 = 2, - V2008 = 3, - V2012 = 4, - V2014 = 5, - V2016 = 6, - Other = 100 - } + Invalid = -1, + V7 = 0, + V2000 = 1, + V2005 = 2, + V2008 = 3, + V2012 = 4, + V2014 = 5, + V2016 = 6, + Other = 100 } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index b2bbd4ac5b9c..56c70515851a 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs @@ -1,9 +1,6 @@ -using System; -using System.Collections.Generic; using System.Data; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Linq; using System.Linq.Expressions; using System.Text; using System.Text.RegularExpressions; @@ -12,603 +9,570 @@ using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax +namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; + +/// +/// Represents the Base Sql Syntax provider implementation. +/// +/// +/// All Sql Syntax provider implementations should derive from this abstract class. +/// +/// +public abstract class SqlSyntaxProviderBase : ISqlSyntaxProvider + where TSyntax : ISqlSyntaxProvider { - /// - /// Represents the Base Sql Syntax provider implementation. - /// - /// - /// All Sql Syntax provider implementations should derive from this abstract class. - /// - /// - public abstract class SqlSyntaxProviderBase : ISqlSyntaxProvider - where TSyntax : ISqlSyntaxProvider + private readonly Lazy _dbTypes; + + protected SqlSyntaxProviderBase() { - private readonly Lazy _dbTypes; + ClauseOrder = new List> + { + FormatString, + FormatType, + FormatNullable, + FormatConstraint, + FormatDefaultValue, + FormatPrimaryKey, + FormatIdentity + }; + + //defaults for all providers + StringLengthColumnDefinitionFormat = StringLengthUnicodeColumnDefinitionFormat; + StringColumnDefinition = string.Format(StringLengthColumnDefinitionFormat, DefaultStringLength); + DecimalColumnDefinition = + string.Format(DecimalColumnDefinitionFormat, DefaultDecimalPrecision, DefaultDecimalScale); + + // ReSharper disable VirtualMemberCallInConstructor + // ok to call virtual GetQuotedXxxName here - they don't depend on any state + var col = Regex.Escape(GetQuotedColumnName("column")).Replace("column", @"\w+"); + var fld = Regex.Escape(GetQuotedTableName("table") + ".").Replace("table", @"\w+") + col; + // ReSharper restore VirtualMemberCallInConstructor + AliasRegex = new Regex("(" + fld + @")\s+AS\s+(" + col + ")", + RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.Compiled); + + _dbTypes = new Lazy(InitColumnTypeMap); + } - protected SqlSyntaxProviderBase() - { - ClauseOrder = new List> - { - FormatString, - FormatType, - FormatNullable, - FormatConstraint, - FormatDefaultValue, - FormatPrimaryKey, - FormatIdentity - }; - - //defaults for all providers - StringLengthColumnDefinitionFormat = StringLengthUnicodeColumnDefinitionFormat; - StringColumnDefinition = string.Format(StringLengthColumnDefinitionFormat, DefaultStringLength); - DecimalColumnDefinition = string.Format(DecimalColumnDefinitionFormat, DefaultDecimalPrecision, DefaultDecimalScale); - - // ReSharper disable VirtualMemberCallInConstructor - // ok to call virtual GetQuotedXxxName here - they don't depend on any state - var col = Regex.Escape(GetQuotedColumnName("column")).Replace("column", @"\w+"); - var fld = Regex.Escape(GetQuotedTableName("table") + ".").Replace("table", @"\w+") + col; - // ReSharper restore VirtualMemberCallInConstructor - AliasRegex = new Regex("(" + fld + @")\s+AS\s+(" + col + ")", RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.Compiled); - - _dbTypes = new Lazy(InitColumnTypeMap); - } + public string StringLengthNonUnicodeColumnDefinitionFormat { get; } = "VARCHAR({0})"; + public virtual string StringLengthUnicodeColumnDefinitionFormat { get; } = "NVARCHAR({0})"; + public string DecimalColumnDefinitionFormat { get; } = "DECIMAL({0},{1})"; - public Regex AliasRegex { get; } + public string DefaultValueFormat { get; } = "DEFAULT ({0})"; + public int DefaultStringLength { get; } = 255; + public int DefaultDecimalPrecision { get; } = 20; + public int DefaultDecimalScale { get; } = 9; - public string GetWildcardPlaceholder() => "%"; + //Set by Constructor + public virtual string StringColumnDefinition { get; } + public string StringLengthColumnDefinitionFormat { get; } - public string StringLengthNonUnicodeColumnDefinitionFormat { get; } = "VARCHAR({0})"; - public virtual string StringLengthUnicodeColumnDefinitionFormat { get; } = "NVARCHAR({0})"; - public string DecimalColumnDefinitionFormat { get; } = "DECIMAL({0},{1})"; + public string AutoIncrementDefinition { get; protected set; } = "AUTOINCREMENT"; + public string IntColumnDefinition { get; protected set; } = "INTEGER"; + public string LongColumnDefinition { get; protected set; } = "BIGINT"; + public string GuidColumnDefinition { get; protected set; } = "GUID"; + public string BoolColumnDefinition { get; protected set; } = "BOOL"; + public string RealColumnDefinition { get; protected set; } = "DOUBLE"; + public string DecimalColumnDefinition { get; protected set; } + public string BlobColumnDefinition { get; protected set; } = "BLOB"; + public string DateTimeColumnDefinition { get; protected set; } = "DATETIME"; + public string DateTimeOffsetColumnDefinition { get; protected set; } = "DATETIMEOFFSET(7)"; + public string TimeColumnDefinition { get; protected set; } = "DATETIME"; - public string DefaultValueFormat { get; } = "DEFAULT ({0})"; - public int DefaultStringLength { get; } = 255; - public int DefaultDecimalPrecision { get; } = 20; - public int DefaultDecimalScale { get; } = 9; + protected IList> ClauseOrder { get; } - //Set by Constructor - public virtual string StringColumnDefinition { get; } - public string StringLengthColumnDefinitionFormat { get; } + protected DbTypes DbTypeMap => _dbTypes.Value; - public string AutoIncrementDefinition { get; protected set; } = "AUTOINCREMENT"; - public string IntColumnDefinition { get; protected set; } = "INTEGER"; - public string LongColumnDefinition { get; protected set; } = "BIGINT"; - public string GuidColumnDefinition { get; protected set; } = "GUID"; - public string BoolColumnDefinition { get; protected set; } = "BOOL"; - public string RealColumnDefinition { get; protected set; } = "DOUBLE"; - public string DecimalColumnDefinition { get; protected set; } - public string BlobColumnDefinition { get; protected set; } = "BLOB"; - public string DateTimeColumnDefinition { get; protected set; } = "DATETIME"; - public string DateTimeOffsetColumnDefinition { get; protected set; } = "DATETIMEOFFSET(7)"; - public string TimeColumnDefinition { get; protected set; } = "DATETIME"; + public virtual string CreateForeignKeyConstraint => + "ALTER TABLE {0} ADD CONSTRAINT {1} FOREIGN KEY ({2}) REFERENCES {3} ({4}){5}{6}"; - protected IList> ClauseOrder { get; } + public virtual string CreateDefaultConstraint => "ALTER TABLE {0} ADD CONSTRAINT {1} DEFAULT ({2}) FOR {3}"; - protected DbTypes DbTypeMap => _dbTypes.Value; + public Regex AliasRegex { get; } - private DbTypes InitColumnTypeMap() - { - var dbTypeMap = new DbTypesFactory(); - dbTypeMap.Set(DbType.String, StringColumnDefinition); - dbTypeMap.Set(DbType.StringFixedLength, StringColumnDefinition); - dbTypeMap.Set(DbType.StringFixedLength, StringColumnDefinition); - dbTypeMap.Set(DbType.String, StringColumnDefinition); - dbTypeMap.Set(DbType.Boolean, BoolColumnDefinition); - dbTypeMap.Set(DbType.Boolean, BoolColumnDefinition); - dbTypeMap.Set(DbType.Guid, GuidColumnDefinition); - dbTypeMap.Set(DbType.Guid, GuidColumnDefinition); - dbTypeMap.Set(DbType.DateTime, DateTimeColumnDefinition); - dbTypeMap.Set(DbType.DateTime, DateTimeColumnDefinition); - dbTypeMap.Set(DbType.Time, TimeColumnDefinition); - dbTypeMap.Set(DbType.Time, TimeColumnDefinition); - dbTypeMap.Set(DbType.DateTimeOffset, DateTimeOffsetColumnDefinition); - dbTypeMap.Set(DbType.DateTimeOffset, DateTimeOffsetColumnDefinition); - - dbTypeMap.Set(DbType.Byte, IntColumnDefinition); - dbTypeMap.Set(DbType.Byte, IntColumnDefinition); - dbTypeMap.Set(DbType.SByte, IntColumnDefinition); - dbTypeMap.Set(DbType.SByte, IntColumnDefinition); - dbTypeMap.Set(DbType.Int16, IntColumnDefinition); - dbTypeMap.Set(DbType.Int16, IntColumnDefinition); - dbTypeMap.Set(DbType.UInt16, IntColumnDefinition); - dbTypeMap.Set(DbType.UInt16, IntColumnDefinition); - dbTypeMap.Set(DbType.Int32, IntColumnDefinition); - dbTypeMap.Set(DbType.Int32, IntColumnDefinition); - dbTypeMap.Set(DbType.UInt32, IntColumnDefinition); - dbTypeMap.Set(DbType.UInt32, IntColumnDefinition); - - dbTypeMap.Set(DbType.Int64, LongColumnDefinition); - dbTypeMap.Set(DbType.Int64, LongColumnDefinition); - dbTypeMap.Set(DbType.UInt64, LongColumnDefinition); - dbTypeMap.Set(DbType.UInt64, LongColumnDefinition); - - dbTypeMap.Set(DbType.Single, RealColumnDefinition); - dbTypeMap.Set(DbType.Single, RealColumnDefinition); - dbTypeMap.Set(DbType.Double, RealColumnDefinition); - dbTypeMap.Set(DbType.Double, RealColumnDefinition); - - dbTypeMap.Set(DbType.Decimal, DecimalColumnDefinition); - dbTypeMap.Set(DbType.Decimal, DecimalColumnDefinition); - - dbTypeMap.Set(DbType.Binary, BlobColumnDefinition); - - return dbTypeMap.Create(); - } + public string GetWildcardPlaceholder() => "%"; - public virtual DatabaseType GetUpdatedDatabaseType(DatabaseType current, string? connectionString) => current; + public virtual DatabaseType GetUpdatedDatabaseType(DatabaseType current, string? connectionString) => current; - public abstract string ProviderName { get; } + public abstract string ProviderName { get; } - public virtual string EscapeString(string val) - { - return NPocoDatabaseExtensions.EscapeAtSymbols(val.Replace("'", "''")); - } + public virtual string EscapeString(string val) => NPocoDatabaseExtensions.EscapeAtSymbols(val.Replace("'", "''")); - public virtual string GetStringColumnEqualComparison(string column, int paramIndex, TextColumnType columnType) - { - //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. - return $"upper({column}) = upper(@{paramIndex})"; - } + public virtual string GetStringColumnEqualComparison(string column, int paramIndex, TextColumnType columnType) => + //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. + $"upper({column}) = upper(@{paramIndex})"; + + public virtual string GetStringColumnWildcardComparison(string column, int paramIndex, TextColumnType columnType) => + //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. + $"upper({column}) LIKE upper(@{paramIndex})"; + + public virtual string GetConcat(params string[] args) => "concat(" + string.Join(",", args) + ")"; + + public virtual string GetQuotedTableName(string? tableName) => $"\"{tableName}\""; + + public virtual string GetQuotedColumnName(string? columnName) => $"\"{columnName}\""; + + public virtual string GetQuotedName(string? name) => $"\"{name}\""; - public virtual string GetStringColumnWildcardComparison(string column, int paramIndex, TextColumnType columnType) + public virtual string GetQuotedValue(string value) => $"'{value}'"; + + public virtual string GetIndexType(IndexTypes indexTypes) + { + string indexType; + + if (indexTypes == IndexTypes.Clustered) { - //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. - return $"upper({column}) LIKE upper(@{paramIndex})"; + indexType = "CLUSTERED"; } - - public virtual string GetConcat(params string[] args) + else { - return "concat(" + string.Join(",", args) + ")"; + indexType = indexTypes == IndexTypes.NonClustered + ? "NONCLUSTERED" + : "UNIQUE NONCLUSTERED"; } - public virtual string GetQuotedTableName(string? tableName) + return indexType; + } + + public virtual string GetSpecialDbType(SpecialDbType dbType) + { + if (dbType == SpecialDbType.NCHAR) { - return $"\"{tableName}\""; + return SpecialDbType.NCHAR; } - public virtual string GetQuotedColumnName(string? columnName) + if (dbType == SpecialDbType.NTEXT) { - return $"\"{columnName}\""; + return SpecialDbType.NTEXT; } - public virtual string GetQuotedName(string? name) + if (dbType == SpecialDbType.NVARCHARMAX) { - return $"\"{name}\""; + return "NVARCHAR(MAX)"; } - public virtual string GetQuotedValue(string value) + return "NVARCHAR"; + } + + public virtual string GetColumn(DatabaseType dbType, string tableName, string columnName, string columnAlias, + string? referenceName = null, bool forInsert = false) + { + tableName = GetQuotedTableName(tableName); + columnName = GetQuotedColumnName(columnName); + var column = tableName + "." + columnName; + if (columnAlias == null) { - return $"'{value}'"; + return column; } - public virtual string GetIndexType(IndexTypes indexTypes) - { - string indexType; + referenceName = referenceName == null ? string.Empty : referenceName + "__"; + columnAlias = GetQuotedColumnName(referenceName + columnAlias); + column += " AS " + columnAlias; + return column; + } - if (indexTypes == IndexTypes.Clustered) - { - indexType = "CLUSTERED"; - } - else - { - indexType = indexTypes == IndexTypes.NonClustered - ? "NONCLUSTERED" - : "UNIQUE NONCLUSTERED"; - } - return indexType; - } - public virtual string GetSpecialDbType(SpecialDbType dbType) - { - if (dbType == SpecialDbType.NCHAR) - { - return SpecialDbType.NCHAR; - } - else if (dbType == SpecialDbType.NTEXT) - { - return SpecialDbType.NTEXT; - } - else if (dbType == SpecialDbType.NVARCHARMAX) - { - return "NVARCHAR(MAX)"; - } + public abstract IsolationLevel DefaultIsolationLevel { get; } + public abstract string DbProvider { get; } - return "NVARCHAR"; - } + public virtual IEnumerable GetTablesInSchema(IDatabase db) => new List(); - public virtual string GetSpecialDbType(SpecialDbType dbType, int customSize) => $"{GetSpecialDbType(dbType)}({customSize})"; + public virtual IEnumerable GetColumnsInSchema(IDatabase db) => new List(); - public virtual string GetColumn(DatabaseType dbType, string tableName, string columnName, string columnAlias, string? referenceName = null, bool forInsert = false) - { - tableName = GetQuotedTableName(tableName); - columnName = GetQuotedColumnName(columnName); - var column = tableName + "." + columnName; - if (columnAlias == null) return column; - - referenceName = referenceName == null ? string.Empty : referenceName + "__"; - columnAlias = GetQuotedColumnName(referenceName + columnAlias); - column += " AS " + columnAlias; - return column; - } + public virtual IEnumerable> GetConstraintsPerTable(IDatabase db) => + new List>(); + public virtual IEnumerable> GetConstraintsPerColumn(IDatabase db) => + new List>(); - public abstract IsolationLevel DefaultIsolationLevel { get; } - public abstract string DbProvider { get; } + public abstract IEnumerable> GetDefinedIndexes(IDatabase db); - public virtual IEnumerable GetTablesInSchema(IDatabase db) - { - return new List(); - } + public abstract bool TryGetDefaultConstraint(IDatabase db, string? tableName, string columnName, + [MaybeNullWhen(false)] out string constraintName); - public virtual IEnumerable GetColumnsInSchema(IDatabase db) - { - return new List(); - } + public virtual string GetFieldNameForUpdate(Expression> fieldSelector, + string? tableAlias = null) => this.GetFieldName(fieldSelector, tableAlias); - public virtual IEnumerable> GetConstraintsPerTable(IDatabase db) - { - return new List>(); - } + public virtual Sql InsertForUpdateHint(Sql sql) => sql; - public virtual IEnumerable> GetConstraintsPerColumn(IDatabase db) - { - return new List>(); - } + public virtual Sql AppendForUpdateHint(Sql sql) => sql; - public abstract IEnumerable> GetDefinedIndexes(IDatabase db); + public abstract Sql.SqlJoinClause LeftJoinWithNestedJoin(Sql sql, + Func, Sql> nestedJoin, string? alias = null); - public abstract bool TryGetDefaultConstraint(IDatabase db, string? tableName, string columnName, [MaybeNullWhen(false)] out string constraintName); - public virtual string GetFieldNameForUpdate(Expression> fieldSelector, string? tableAlias = null) => this.GetFieldName(fieldSelector, tableAlias); + public virtual IDictionary? ScalarMappers => null; - public virtual Sql InsertForUpdateHint(Sql sql) => sql; + public virtual bool DoesTableExist(IDatabase db, string tableName) => GetTablesInSchema(db).Contains(tableName); - public virtual Sql AppendForUpdateHint(Sql sql) => sql; + public virtual bool SupportsClustered() => true; - public abstract Sql.SqlJoinClause LeftJoinWithNestedJoin(Sql sql, Func, Sql> nestedJoin, string? alias = null); + public virtual bool SupportsIdentityInsert() => true; + /// + /// This is used ONLY if we need to format datetime without using SQL parameters (i.e. during migrations) + /// + /// + /// + /// + /// + /// MSSQL has a DateTime standard that is unambiguous and works on all servers: + /// YYYYMMDD HH:mm:ss + /// + public virtual string FormatDateTime(DateTime date, bool includeTime = true) => + // need CultureInfo.InvariantCulture because ":" here is the "time separator" and + // may be converted to something else in different cultures (eg "." in DK). + date.ToString(includeTime ? "yyyyMMdd HH:mm:ss" : "yyyyMMdd", CultureInfo.InvariantCulture); - public virtual IDictionary? ScalarMappers => null; + public virtual string Format(TableDefinition table) + { + var statement = string.Format(CreateTable, GetQuotedTableName(table.Name), Format(table.Columns)); - public virtual bool DoesTableExist(IDatabase db, string tableName) => GetTablesInSchema(db).Contains(tableName); + return statement; + } - public virtual bool SupportsClustered() - { - return true; - } + public virtual List Format(IEnumerable indexes) => indexes.Select(Format).ToList(); - public virtual bool SupportsIdentityInsert() - { - return true; - } + public virtual string Format(IndexDefinition index) + { + var name = string.IsNullOrEmpty(index.Name) + ? $"IX_{index.TableName}_{index.ColumnName}" + : index.Name; - /// - /// This is used ONLY if we need to format datetime without using SQL parameters (i.e. during migrations) - /// - /// - /// - /// - /// - /// MSSQL has a DateTime standard that is unambiguous and works on all servers: - /// YYYYMMDD HH:mm:ss - /// - public virtual string FormatDateTime(DateTime date, bool includeTime = true) - { - // need CultureInfo.InvariantCulture because ":" here is the "time separator" and - // may be converted to something else in different cultures (eg "." in DK). - return date.ToString(includeTime ? "yyyyMMdd HH:mm:ss" : "yyyyMMdd", CultureInfo.InvariantCulture); - } + var columns = index.Columns.Any() + ? string.Join(",", index.Columns.Select(x => GetQuotedColumnName(x.Name))) + : GetQuotedColumnName(index.ColumnName); - public virtual string Format(TableDefinition table) - { - var statement = string.Format(CreateTable, GetQuotedTableName(table.Name), Format(table.Columns)); + return string.Format(CreateIndex, GetIndexType(index.IndexType), " ", GetQuotedName(name), + GetQuotedTableName(index.TableName), columns); + } - return statement; - } + public virtual List Format(IEnumerable foreignKeys) => + foreignKeys.Select(Format).ToList(); + + public virtual string Format(ForeignKeyDefinition foreignKey) + { + var constraintName = string.IsNullOrEmpty(foreignKey.Name) + ? $"FK_{foreignKey.ForeignTable}_{foreignKey.PrimaryTable}_{foreignKey.PrimaryColumns.First()}" + : foreignKey.Name; + + return string.Format(CreateForeignKeyConstraint, + GetQuotedTableName(foreignKey.ForeignTable), + GetQuotedName(constraintName), + GetQuotedColumnName(foreignKey.ForeignColumns.First()), + GetQuotedTableName(foreignKey.PrimaryTable), + GetQuotedColumnName(foreignKey.PrimaryColumns.First()), + FormatCascade("DELETE", foreignKey.OnDelete), + FormatCascade("UPDATE", foreignKey.OnUpdate)); + } - public virtual List Format(IEnumerable indexes) + public virtual string Format(IEnumerable columns) + { + var sb = new StringBuilder(); + foreach (ColumnDefinition column in columns) { - return indexes.Select(Format).ToList(); + sb.Append(Format(column) + ",\n"); } - public virtual string Format(IndexDefinition index) - { - var name = string.IsNullOrEmpty(index.Name) - ? $"IX_{index.TableName}_{index.ColumnName}" - : index.Name; + return sb.ToString().TrimEnd(",\n"); + } - var columns = index.Columns.Any() - ? string.Join(",", index.Columns.Select(x => GetQuotedColumnName(x.Name))) - : GetQuotedColumnName(index.ColumnName); + public virtual string Format(ColumnDefinition column) => + string.Join(" ", ClauseOrder + .Select(action => action(column)) + .Where(clause => string.IsNullOrEmpty(clause) == false)); - return string.Format(CreateIndex, GetIndexType(index.IndexType), " ", GetQuotedName(name), - GetQuotedTableName(index.TableName), columns); - } + public virtual string Format(ColumnDefinition column, string tableName, out IEnumerable sqls) + { + var sql = new StringBuilder(); + sql.Append(FormatString(column)); + sql.Append(" "); + sql.Append(FormatType(column)); + sql.Append(" "); + sql.Append("NULL"); // always nullable + sql.Append(" "); + sql.Append(FormatConstraint(column)); + sql.Append(" "); + sql.Append(FormatDefaultValue(column)); + sql.Append(" "); + sql.Append(FormatPrimaryKey(column)); + sql.Append(" "); + sql.Append(FormatIdentity(column)); + + //var isNullable = column.IsNullable; + + //var constraint = FormatConstraint(column)?.TrimStart("CONSTRAINT "); + //var hasConstraint = !string.IsNullOrWhiteSpace(constraint); + + //var defaultValue = FormatDefaultValue(column); + //var hasDefaultValue = !string.IsNullOrWhiteSpace(defaultValue); + + // TODO: This used to exit if nullable but that means this would never work + // to return SQL if the column was nullable?!? I don't get it. This was here + // 4 years ago, I've removed it so that this works for nullable columns. + //if (isNullable /*&& !hasConstraint && !hasDefaultValue*/) + //{ + // sqls = Enumerable.Empty(); + // return sql.ToString(); + //} + + var msql = new List(); + sqls = msql; + + var alterSql = new StringBuilder(); + alterSql.Append(FormatString(column)); + alterSql.Append(" "); + alterSql.Append(FormatType(column)); + alterSql.Append(" "); + alterSql.Append(FormatNullable(column)); + //alterSql.Append(" "); + //alterSql.Append(FormatPrimaryKey(column)); + //alterSql.Append(" "); + //alterSql.Append(FormatIdentity(column)); + msql.Add(string.Format(AlterColumn, tableName, alterSql)); + + //if (hasConstraint) + //{ + // var dropConstraintSql = string.Format(DeleteConstraint, tableName, constraint); + // msql.Add(dropConstraintSql); + // var constraintType = hasDefaultValue ? defaultValue : ""; + // var createConstraintSql = string.Format(CreateConstraint, tableName, constraint, constraintType, FormatString(column)); + // msql.Add(createConstraintSql); + //} + + return sql.ToString(); + } - public virtual List Format(IEnumerable foreignKeys) + public virtual string FormatPrimaryKey(TableDefinition table) + { + ColumnDefinition? columnDefinition = table.Columns.FirstOrDefault(x => x.IsPrimaryKey); + if (columnDefinition == null) { - return foreignKeys.Select(Format).ToList(); + return string.Empty; } - public virtual string Format(ForeignKeyDefinition foreignKey) - { - var constraintName = string.IsNullOrEmpty(foreignKey.Name) - ? $"FK_{foreignKey.ForeignTable}_{foreignKey.PrimaryTable}_{foreignKey.PrimaryColumns.First()}" - : foreignKey.Name; - - return string.Format(CreateForeignKeyConstraint, - GetQuotedTableName(foreignKey.ForeignTable), - GetQuotedName(constraintName), - GetQuotedColumnName(foreignKey.ForeignColumns.First()), - GetQuotedTableName(foreignKey.PrimaryTable), - GetQuotedColumnName(foreignKey.PrimaryColumns.First()), - FormatCascade("DELETE", foreignKey.OnDelete), - FormatCascade("UPDATE", foreignKey.OnUpdate)); - } + var constraintName = string.IsNullOrEmpty(columnDefinition.PrimaryKeyName) + ? $"PK_{table.Name}" + : columnDefinition.PrimaryKeyName; - public virtual string Format(IEnumerable columns) - { - var sb = new StringBuilder(); - foreach (var column in columns) - { - sb.Append(Format(column) + ",\n"); - } - return sb.ToString().TrimEnd(",\n"); - } + var columns = string.IsNullOrEmpty(columnDefinition.PrimaryKeyColumns) + ? GetQuotedColumnName(columnDefinition.Name) + : string.Join(", ", columnDefinition.PrimaryKeyColumns + .Split(Constants.CharArrays.CommaSpace, StringSplitOptions.RemoveEmptyEntries) + .Select(GetQuotedColumnName)); - public virtual string Format(ColumnDefinition column) - { - return string.Join(" ", ClauseOrder - .Select(action => action(column)) - .Where(clause => string.IsNullOrEmpty(clause) == false)); - } + var primaryKeyPart = string.Concat("PRIMARY KEY", columnDefinition.IsIndexed ? " CLUSTERED" : " NONCLUSTERED"); - public virtual string Format(ColumnDefinition column, string tableName, out IEnumerable sqls) - { - var sql = new StringBuilder(); - sql.Append(FormatString(column)); - sql.Append(" "); - sql.Append(FormatType(column)); - sql.Append(" "); - sql.Append("NULL"); // always nullable - sql.Append(" "); - sql.Append(FormatConstraint(column)); - sql.Append(" "); - sql.Append(FormatDefaultValue(column)); - sql.Append(" "); - sql.Append(FormatPrimaryKey(column)); - sql.Append(" "); - sql.Append(FormatIdentity(column)); - - //var isNullable = column.IsNullable; - - //var constraint = FormatConstraint(column)?.TrimStart("CONSTRAINT "); - //var hasConstraint = !string.IsNullOrWhiteSpace(constraint); - - //var defaultValue = FormatDefaultValue(column); - //var hasDefaultValue = !string.IsNullOrWhiteSpace(defaultValue); - - // TODO: This used to exit if nullable but that means this would never work - // to return SQL if the column was nullable?!? I don't get it. This was here - // 4 years ago, I've removed it so that this works for nullable columns. - //if (isNullable /*&& !hasConstraint && !hasDefaultValue*/) - //{ - // sqls = Enumerable.Empty(); - // return sql.ToString(); - //} - - var msql = new List(); - sqls = msql; - - var alterSql = new StringBuilder(); - alterSql.Append(FormatString(column)); - alterSql.Append(" "); - alterSql.Append(FormatType(column)); - alterSql.Append(" "); - alterSql.Append(FormatNullable(column)); - //alterSql.Append(" "); - //alterSql.Append(FormatPrimaryKey(column)); - //alterSql.Append(" "); - //alterSql.Append(FormatIdentity(column)); - msql.Add(string.Format(AlterColumn, tableName, alterSql)); - - //if (hasConstraint) - //{ - // var dropConstraintSql = string.Format(DeleteConstraint, tableName, constraint); - // msql.Add(dropConstraintSql); - // var constraintType = hasDefaultValue ? defaultValue : ""; - // var createConstraintSql = string.Format(CreateConstraint, tableName, constraint, constraintType, FormatString(column)); - // msql.Add(createConstraintSql); - //} - - return sql.ToString(); - } + return string.Format(CreateConstraint, + GetQuotedTableName(table.Name), + GetQuotedName(constraintName), + primaryKeyPart, + columns); + } - public virtual string FormatPrimaryKey(TableDefinition table) - { - var columnDefinition = table.Columns.FirstOrDefault(x => x.IsPrimaryKey); - if (columnDefinition == null) - return string.Empty; - - var constraintName = string.IsNullOrEmpty(columnDefinition.PrimaryKeyName) - ? $"PK_{table.Name}" - : columnDefinition.PrimaryKeyName; - - var columns = string.IsNullOrEmpty(columnDefinition.PrimaryKeyColumns) - ? GetQuotedColumnName(columnDefinition.Name) - : string.Join(", ", columnDefinition.PrimaryKeyColumns - .Split(Constants.CharArrays.CommaSpace, StringSplitOptions.RemoveEmptyEntries) - .Select(GetQuotedColumnName)); - - var primaryKeyPart = string.Concat("PRIMARY KEY", columnDefinition.IsIndexed ? " CLUSTERED" : " NONCLUSTERED"); - - return string.Format(CreateConstraint, - GetQuotedTableName(table.Name), - GetQuotedName(constraintName), - primaryKeyPart, - columns); - } + public virtual string FormatColumnRename(string? tableName, string? oldName, string? newName) => + string.Format(RenameColumn, + GetQuotedTableName(tableName), + GetQuotedColumnName(oldName), + GetQuotedColumnName(newName)); - public virtual string FormatColumnRename(string? tableName, string? oldName, string? newName) - { - return string.Format(RenameColumn, - GetQuotedTableName(tableName), - GetQuotedColumnName(oldName), - GetQuotedColumnName(newName)); - } + public virtual string FormatTableRename(string? oldName, string? newName) => + string.Format(RenameTable, GetQuotedTableName(oldName), GetQuotedTableName(newName)); - public virtual string FormatTableRename(string? oldName, string? newName) - { - return string.Format(RenameTable, GetQuotedTableName(oldName), GetQuotedTableName(newName)); - } + public abstract Sql SelectTop(Sql sql, int top); - protected virtual string FormatCascade(string onWhat, Rule rule) - { - var action = "NO ACTION"; - switch (rule) - { - case Rule.None: - return ""; - case Rule.Cascade: - action = "CASCADE"; - break; - case Rule.SetNull: - action = "SET NULL"; - break; - case Rule.SetDefault: - action = "SET DEFAULT"; - break; - } + public abstract void HandleCreateTable(IDatabase database, TableDefinition tableDefinition, + bool skipKeysAndIndexes = false); - return $" ON {onWhat} {action}"; - } + public virtual string DeleteDefaultConstraint => + throw new NotSupportedException("Default constraints are not supported"); - protected virtual string FormatString(ColumnDefinition column) - { - return GetQuotedColumnName(column.Name); - } + public virtual string CreateTable => "CREATE TABLE {0} ({1})"; + public virtual string DropTable => "DROP TABLE {0}"; - protected virtual string FormatType(ColumnDefinition column) - { - if (column.Type.HasValue == false && string.IsNullOrEmpty(column.CustomType) == false) - return column.CustomType; + public virtual string AddColumn => "ALTER TABLE {0} ADD {1}"; + public virtual string DropColumn => "ALTER TABLE {0} DROP COLUMN {1}"; + public virtual string AlterColumn => "ALTER TABLE {0} ALTER COLUMN {1}"; + public virtual string RenameColumn => "ALTER TABLE {0} RENAME COLUMN {1} TO {2}"; - if (column.CustomDbType.HasValue) - { - if (column.Size != default) - { - return GetSpecialDbType(column.CustomDbType.Value, column.Size); - } + public virtual string RenameTable => "RENAME TABLE {0} TO {1}"; - return GetSpecialDbType(column.CustomDbType.Value); - } + public virtual string CreateSchema => "CREATE SCHEMA {0}"; + public virtual string AlterSchema => "ALTER SCHEMA {0} TRANSFER {1}.{2}"; + public virtual string DropSchema => "DROP SCHEMA {0}"; - var type = column.Type.HasValue - ? DbTypeMap.ColumnDbTypeMap.First(x => x.Value == column.Type.Value).Key - : column.PropertyType; + public virtual string CreateIndex => "CREATE {0}{1}INDEX {2} ON {3} ({4})"; + public virtual string DropIndex => "DROP INDEX {0}"; - if (type == typeof(string)) - { - var valueOrDefault = column.Size != default ? column.Size : DefaultStringLength; - return string.Format(StringLengthColumnDefinitionFormat, valueOrDefault); - } + public virtual string InsertData => "INSERT INTO {0} ({1}) VALUES ({2})"; + public virtual string UpdateData => "UPDATE {0} SET {1} WHERE {2}"; + public virtual string DeleteData => "DELETE FROM {0} WHERE {1}"; + public virtual string TruncateTable => "TRUNCATE TABLE {0}"; - if (type == typeof(decimal)) - { - var precision = column.Size != default ? column.Size : DefaultDecimalPrecision; - var scale = column.Precision != default ? column.Precision : DefaultDecimalScale; - return string.Format(DecimalColumnDefinitionFormat, precision, scale); - } + public virtual string CreateConstraint => "ALTER TABLE {0} ADD CONSTRAINT {1} {2} ({3})"; + public virtual string DeleteConstraint => "ALTER TABLE {0} DROP CONSTRAINT {1}"; - var definition = DbTypeMap.ColumnTypeMap[type!]; - var dbTypeDefinition = column.Size != default - ? $"{definition}({column.Size})" - : definition; - //NOTE Precision is left out - return dbTypeDefinition; - } + public virtual string ConvertIntegerToOrderableString => "REPLACE(STR({0}, 8), SPACE(1), '0')"; + public virtual string ConvertDateToOrderableString => "CONVERT(nvarchar, {0}, 120)"; + public virtual string ConvertDecimalToOrderableString => "REPLACE(STR({0}, 20, 9), SPACE(1), '0')"; - protected virtual string FormatNullable(ColumnDefinition column) - { - return column.IsNullable ? "NULL" : "NOT NULL"; - } + private DbTypes InitColumnTypeMap() + { + var dbTypeMap = new DbTypesFactory(); + dbTypeMap.Set(DbType.String, StringColumnDefinition); + dbTypeMap.Set(DbType.StringFixedLength, StringColumnDefinition); + dbTypeMap.Set(DbType.StringFixedLength, StringColumnDefinition); + dbTypeMap.Set(DbType.String, StringColumnDefinition); + dbTypeMap.Set(DbType.Boolean, BoolColumnDefinition); + dbTypeMap.Set(DbType.Boolean, BoolColumnDefinition); + dbTypeMap.Set(DbType.Guid, GuidColumnDefinition); + dbTypeMap.Set(DbType.Guid, GuidColumnDefinition); + dbTypeMap.Set(DbType.DateTime, DateTimeColumnDefinition); + dbTypeMap.Set(DbType.DateTime, DateTimeColumnDefinition); + dbTypeMap.Set(DbType.Time, TimeColumnDefinition); + dbTypeMap.Set(DbType.Time, TimeColumnDefinition); + dbTypeMap.Set(DbType.DateTimeOffset, DateTimeOffsetColumnDefinition); + dbTypeMap.Set(DbType.DateTimeOffset, DateTimeOffsetColumnDefinition); + + dbTypeMap.Set(DbType.Byte, IntColumnDefinition); + dbTypeMap.Set(DbType.Byte, IntColumnDefinition); + dbTypeMap.Set(DbType.SByte, IntColumnDefinition); + dbTypeMap.Set(DbType.SByte, IntColumnDefinition); + dbTypeMap.Set(DbType.Int16, IntColumnDefinition); + dbTypeMap.Set(DbType.Int16, IntColumnDefinition); + dbTypeMap.Set(DbType.UInt16, IntColumnDefinition); + dbTypeMap.Set(DbType.UInt16, IntColumnDefinition); + dbTypeMap.Set(DbType.Int32, IntColumnDefinition); + dbTypeMap.Set(DbType.Int32, IntColumnDefinition); + dbTypeMap.Set(DbType.UInt32, IntColumnDefinition); + dbTypeMap.Set(DbType.UInt32, IntColumnDefinition); + + dbTypeMap.Set(DbType.Int64, LongColumnDefinition); + dbTypeMap.Set(DbType.Int64, LongColumnDefinition); + dbTypeMap.Set(DbType.UInt64, LongColumnDefinition); + dbTypeMap.Set(DbType.UInt64, LongColumnDefinition); + + dbTypeMap.Set(DbType.Single, RealColumnDefinition); + dbTypeMap.Set(DbType.Single, RealColumnDefinition); + dbTypeMap.Set(DbType.Double, RealColumnDefinition); + dbTypeMap.Set(DbType.Double, RealColumnDefinition); + + dbTypeMap.Set(DbType.Decimal, DecimalColumnDefinition); + dbTypeMap.Set(DbType.Decimal, DecimalColumnDefinition); + + dbTypeMap.Set(DbType.Binary, BlobColumnDefinition); + + return dbTypeMap.Create(); + } - protected virtual string FormatConstraint(ColumnDefinition column) - { - if (string.IsNullOrEmpty(column.ConstraintName) && column.DefaultValue == null) - return string.Empty; + public virtual string GetSpecialDbType(SpecialDbType dbType, int customSize) => + $"{GetSpecialDbType(dbType)}({customSize})"; - return - $"CONSTRAINT {(string.IsNullOrEmpty(column.ConstraintName) ? GetQuotedName($"DF_{column.TableName}_{column.Name}") : column.ConstraintName)}"; - } + protected virtual string FormatCascade(string onWhat, Rule rule) + { + var action = "NO ACTION"; + switch (rule) + { + case Rule.None: + return ""; + case Rule.Cascade: + action = "CASCADE"; + break; + case Rule.SetNull: + action = "SET NULL"; + break; + case Rule.SetDefault: + action = "SET DEFAULT"; + break; + } + + return $" ON {onWhat} {action}"; + } - protected virtual string FormatDefaultValue(ColumnDefinition column) - { - if (column.DefaultValue == null) - return string.Empty; + protected virtual string FormatString(ColumnDefinition column) => GetQuotedColumnName(column.Name); - // HACK: probably not needed with latest changes - if (string.Equals(column.DefaultValue.ToString(), "GETDATE()", StringComparison.OrdinalIgnoreCase)) - column.DefaultValue = SystemMethods.CurrentDateTime; + protected virtual string FormatType(ColumnDefinition column) + { + if (column.Type.HasValue == false && string.IsNullOrEmpty(column.CustomType) == false) + { + return column.CustomType; + } - // see if this is for a system method - if (column.DefaultValue is SystemMethods) + if (column.CustomDbType.HasValue) + { + if (column.Size != default) { - var method = FormatSystemMethods((SystemMethods)column.DefaultValue); - return string.IsNullOrEmpty(method) ? string.Empty : string.Format(DefaultValueFormat, method); + return GetSpecialDbType(column.CustomDbType.Value, column.Size); } - return string.Format(DefaultValueFormat, GetQuotedValue(column.DefaultValue.ToString()!)); + return GetSpecialDbType(column.CustomDbType.Value); } - protected virtual string FormatPrimaryKey(ColumnDefinition column) + Type type = column.Type.HasValue + ? DbTypeMap.ColumnDbTypeMap.First(x => x.Value == column.Type.Value).Key + : column.PropertyType; + + if (type == typeof(string)) { - return string.Empty; + var valueOrDefault = column.Size != default ? column.Size : DefaultStringLength; + return string.Format(StringLengthColumnDefinitionFormat, valueOrDefault); } - protected abstract string? FormatSystemMethods(SystemMethods systemMethod); - - protected abstract string FormatIdentity(ColumnDefinition column); + if (type == typeof(decimal)) + { + var precision = column.Size != default ? column.Size : DefaultDecimalPrecision; + var scale = column.Precision != default ? column.Precision : DefaultDecimalScale; + return string.Format(DecimalColumnDefinitionFormat, precision, scale); + } - public abstract Sql SelectTop(Sql sql, int top); + var definition = DbTypeMap.ColumnTypeMap[type!]; + var dbTypeDefinition = column.Size != default + ? $"{definition}({column.Size})" + : definition; + //NOTE Precision is left out + return dbTypeDefinition; + } - public abstract void HandleCreateTable(IDatabase database, TableDefinition tableDefinition, bool skipKeysAndIndexes = false); + protected virtual string FormatNullable(ColumnDefinition column) => column.IsNullable ? "NULL" : "NOT NULL"; - public virtual string DeleteDefaultConstraint => throw new NotSupportedException("Default constraints are not supported"); + protected virtual string FormatConstraint(ColumnDefinition column) + { + if (string.IsNullOrEmpty(column.ConstraintName) && column.DefaultValue == null) + { + return string.Empty; + } - public virtual string CreateTable => "CREATE TABLE {0} ({1})"; - public virtual string DropTable => "DROP TABLE {0}"; + return + $"CONSTRAINT {(string.IsNullOrEmpty(column.ConstraintName) ? GetQuotedName($"DF_{column.TableName}_{column.Name}") : column.ConstraintName)}"; + } - public virtual string AddColumn => "ALTER TABLE {0} ADD {1}"; - public virtual string DropColumn => "ALTER TABLE {0} DROP COLUMN {1}"; - public virtual string AlterColumn => "ALTER TABLE {0} ALTER COLUMN {1}"; - public virtual string RenameColumn => "ALTER TABLE {0} RENAME COLUMN {1} TO {2}"; + protected virtual string FormatDefaultValue(ColumnDefinition column) + { + if (column.DefaultValue == null) + { + return string.Empty; + } - public virtual string RenameTable => "RENAME TABLE {0} TO {1}"; + // HACK: probably not needed with latest changes + if (string.Equals(column.DefaultValue.ToString(), "GETDATE()", StringComparison.OrdinalIgnoreCase)) + { + column.DefaultValue = SystemMethods.CurrentDateTime; + } - public virtual string CreateSchema => "CREATE SCHEMA {0}"; - public virtual string AlterSchema => "ALTER SCHEMA {0} TRANSFER {1}.{2}"; - public virtual string DropSchema => "DROP SCHEMA {0}"; + // see if this is for a system method + if (column.DefaultValue is SystemMethods) + { + var method = FormatSystemMethods((SystemMethods)column.DefaultValue); + return string.IsNullOrEmpty(method) ? string.Empty : string.Format(DefaultValueFormat, method); + } - public virtual string CreateIndex => "CREATE {0}{1}INDEX {2} ON {3} ({4})"; - public virtual string DropIndex => "DROP INDEX {0}"; + return string.Format(DefaultValueFormat, GetQuotedValue(column.DefaultValue.ToString()!)); + } - public virtual string InsertData => "INSERT INTO {0} ({1}) VALUES ({2})"; - public virtual string UpdateData => "UPDATE {0} SET {1} WHERE {2}"; - public virtual string DeleteData => "DELETE FROM {0} WHERE {1}"; - public virtual string TruncateTable => "TRUNCATE TABLE {0}"; + protected virtual string FormatPrimaryKey(ColumnDefinition column) => string.Empty; - public virtual string CreateConstraint => "ALTER TABLE {0} ADD CONSTRAINT {1} {2} ({3})"; - public virtual string DeleteConstraint => "ALTER TABLE {0} DROP CONSTRAINT {1}"; - public virtual string CreateForeignKeyConstraint => "ALTER TABLE {0} ADD CONSTRAINT {1} FOREIGN KEY ({2}) REFERENCES {3} ({4}){5}{6}"; - public virtual string CreateDefaultConstraint => "ALTER TABLE {0} ADD CONSTRAINT {1} DEFAULT ({2}) FOR {3}"; + protected abstract string? FormatSystemMethods(SystemMethods systemMethod); - public virtual string ConvertIntegerToOrderableString => "REPLACE(STR({0}, 8), SPACE(1), '0')"; - public virtual string ConvertDateToOrderableString => "CONVERT(nvarchar, {0}, 120)"; - public virtual string ConvertDecimalToOrderableString => "REPLACE(STR({0}, 20, 9), SPACE(1), '0')"; - } + protected abstract string FormatIdentity(ColumnDefinition column); } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs index 6c816f7c92eb..4858b5d983c1 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs @@ -1,57 +1,49 @@ -using System.Collections.Generic; -using System.Linq; -using NPoco; +using NPoco; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax -{ - internal static class SqlSyntaxProviderExtensions - { - public static IEnumerable GetDefinedIndexesDefinitions(this ISqlSyntaxProvider sql, IDatabase db) - { - return sql.GetDefinedIndexes(db) - .Select(x => new DbIndexDefinition(x)).ToArray(); - } +namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; - /// - /// Returns the quotes tableName.columnName combo - /// - /// - /// - /// - /// - public static string GetQuotedColumn(this ISqlSyntaxProvider sql, string tableName, string columnName) - { - return sql.GetQuotedTableName(tableName) + "." + sql.GetQuotedColumnName(columnName); - } +internal static class SqlSyntaxProviderExtensions +{ + public static IEnumerable + GetDefinedIndexesDefinitions(this ISqlSyntaxProvider sql, IDatabase db) => + sql.GetDefinedIndexes(db) + .Select(x => new DbIndexDefinition(x)).ToArray(); - /// - /// This is used to generate a delete query that uses a sub-query to select the data, it is required because there's a very particular syntax that - /// needs to be used to work for all servers - /// - /// - /// - /// See: http://issues.umbraco.org/issue/U4-3876 - /// - public static Sql GetDeleteSubquery(this ISqlSyntaxProvider sqlProvider, string tableName, string columnName, Sql subQuery, WhereInType whereInType = WhereInType.In) - { - //TODO: This is no longer necessary since this used to be a specific requirement for MySql! - // Now we can do a Delete + sub query, see RelationRepository.DeleteByParent for example + /// + /// Returns the quotes tableName.columnName combo + /// + /// + /// + /// + /// + public static string GetQuotedColumn(this ISqlSyntaxProvider sql, string tableName, string columnName) => + sql.GetQuotedTableName(tableName) + "." + sql.GetQuotedColumnName(columnName); - return - new Sql(string.Format( - whereInType == WhereInType.In - ? @"DELETE FROM {0} WHERE {1} IN (SELECT {1} FROM ({2}) x)" - : @"DELETE FROM {0} WHERE {1} NOT IN (SELECT {1} FROM ({2}) x)", - sqlProvider.GetQuotedTableName(tableName), - sqlProvider.GetQuotedColumnName(columnName), - subQuery.SQL), subQuery.Arguments); - } - } + /// + /// This is used to generate a delete query that uses a sub-query to select the data, it is required because there's a + /// very particular syntax that + /// needs to be used to work for all servers + /// + /// + /// + /// See: http://issues.umbraco.org/issue/U4-3876 + /// + public static Sql GetDeleteSubquery(this ISqlSyntaxProvider sqlProvider, string tableName, string columnName, + Sql subQuery, WhereInType whereInType = WhereInType.In) => + //TODO: This is no longer necessary since this used to be a specific requirement for MySql! + // Now we can do a Delete + sub query, see RelationRepository.DeleteByParent for example + new Sql(string.Format( + whereInType == WhereInType.In + ? @"DELETE FROM {0} WHERE {1} IN (SELECT {1} FROM ({2}) x)" + : @"DELETE FROM {0} WHERE {1} NOT IN (SELECT {1} FROM ({2}) x)", + sqlProvider.GetQuotedTableName(tableName), + sqlProvider.GetQuotedColumnName(columnName), + subQuery.SQL), subQuery.Arguments); +} - internal enum WhereInType - { - In, - NotIn - } +internal enum WhereInType +{ + In, + NotIn } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntaxExtensions.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntaxExtensions.cs index fca90e90489d..899d0493a66d 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntaxExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntaxExtensions.cs @@ -1,40 +1,39 @@ -using System; using System.Linq.Expressions; using System.Reflection; using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods to . +/// +public static class SqlSyntaxExtensions { + private static string GetColumnName(this PropertyInfo column) + { + ColumnAttribute? attr = column.FirstAttribute(); + return string.IsNullOrWhiteSpace(attr?.Name) ? column.Name : attr.Name; + } + /// - /// Provides extension methods to . + /// Gets a quoted table and field name. /// - public static class SqlSyntaxExtensions + /// The type of the DTO. + /// An . + /// An expression specifying the field. + /// An optional table alias. + /// + public static string GetFieldName(this ISqlSyntaxProvider sqlSyntax, + Expression> fieldSelector, string? tableAlias = null) { - private static string GetColumnName(this PropertyInfo column) - { - var attr = column.FirstAttribute(); - return string.IsNullOrWhiteSpace(attr?.Name) ? column.Name : attr.Name; - } - - /// - /// Gets a quoted table and field name. - /// - /// The type of the DTO. - /// An . - /// An expression specifying the field. - /// An optional table alias. - /// - public static string GetFieldName(this ISqlSyntaxProvider sqlSyntax, Expression> fieldSelector, string? tableAlias = null) - { - var field = ExpressionHelper.FindProperty(fieldSelector).Item1 as PropertyInfo; - var fieldName = field?.GetColumnName(); + var field = ExpressionHelper.FindProperty(fieldSelector).Item1 as PropertyInfo; + var fieldName = field?.GetColumnName(); - var type = typeof(TDto); - var tableName = tableAlias ?? type.GetTableName(); + Type type = typeof(TDto); + var tableName = tableAlias ?? type.GetTableName(); - return sqlSyntax.GetQuotedTableName(tableName) + "." + sqlSyntax.GetQuotedColumnName(fieldName); - } + return sqlSyntax.GetQuotedTableName(tableName) + "." + sqlSyntax.GetQuotedColumnName(fieldName); } } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlTemplate.cs b/src/Umbraco.Infrastructure/Persistence/SqlTemplate.cs index 716b24bb0750..58ed02a7459f 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlTemplate.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlTemplate.cs @@ -1,124 +1,130 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; +using System.Collections; using NPoco; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +public class SqlTemplate { - public class SqlTemplate - { - private readonly ISqlContext _sqlContext; - private readonly string _sql; - private readonly Dictionary? _args; + private readonly Dictionary? _args; + private readonly string _sql; + private readonly ISqlContext _sqlContext; - // these are created in PocoToSqlExpressionVisitor - internal class TemplateArg + internal SqlTemplate(ISqlContext sqlContext, string sql, object[] args) + { + _sqlContext = sqlContext; + _sql = sql; + if (args.Length > 0) { - public TemplateArg(string? name) - { - Name = name; - } - - public string? Name { get; } - - public override string ToString() - { - return "@" + Name; - } + _args = new Dictionary(); } - internal SqlTemplate(ISqlContext sqlContext, string sql, object[] args) + for (var i = 0; i < args.Length; i++) { - _sqlContext = sqlContext; - _sql = sql; - if (args.Length > 0) - _args = new Dictionary(); - for (var i = 0; i < args.Length; i++) - _args![i] = args[i]; + _args![i] = args[i]; } + } + + public Sql Sql() => new Sql(_sqlContext, _sql); - public Sql Sql() + // must pass the args, all of them, in the proper order, faster + public Sql Sql(params object[] args) + { + // if the type is an "unspeakable name" it is an anonymous compiler-generated object + // see https://stackoverflow.com/questions/9256594 + // => assume it's an anonymous type object containing named arguments + // (of course this means we cannot use *real* objects here and need SqlNamed - bah) + if (args.Length == 1 && args[0].GetType().Name.Contains("<")) { - return new Sql(_sqlContext, _sql); + return SqlNamed(args[0]); } - // must pass the args, all of them, in the proper order, faster - public Sql Sql(params object[] args) + if (args.Length != _args?.Count) { - // if the type is an "unspeakable name" it is an anonymous compiler-generated object - // see https://stackoverflow.com/questions/9256594 - // => assume it's an anonymous type object containing named arguments - // (of course this means we cannot use *real* objects here and need SqlNamed - bah) - if (args.Length == 1 && args[0].GetType().Name.Contains("<")) - return SqlNamed(args[0]); - - if (args.Length != _args?.Count) - throw new ArgumentException("Invalid number of arguments.", nameof(args)); - - if (args.Length == 0) - return new Sql(_sqlContext, true, _sql); + throw new ArgumentException("Invalid number of arguments.", nameof(args)); + } - var isBuilt = !args.Any(x => x is IEnumerable); - return new Sql(_sqlContext, isBuilt, _sql, args); + if (args.Length == 0) + { + return new Sql(_sqlContext, true, _sql); } - // can pass named args, not necessary all of them, slower - // so, not much different from what Where(...) does (ie reflection) - public Sql SqlNamed(object nargs) + var isBuilt = !args.Any(x => x is IEnumerable); + return new Sql(_sqlContext, isBuilt, _sql, args); + } + + // can pass named args, not necessary all of them, slower + // so, not much different from what Where(...) does (ie reflection) + public Sql SqlNamed(object nargs) + { + var isBuilt = true; + var args = new object[_args?.Count ?? 0]; + var properties = nargs.GetType().GetProperties().ToDictionary(x => x.Name, x => x.GetValue(nargs)); + for (var i = 0; i < _args?.Count; i++) { - var isBuilt = true; - var args = new object[_args?.Count ?? 0]; - var properties = nargs.GetType().GetProperties().ToDictionary(x => x.Name, x => x.GetValue(nargs)); - for (var i = 0; i < _args?.Count; i++) + object? value; + if (_args[i] is TemplateArg templateArg) { - object? value; - if (_args[i] is TemplateArg templateArg) + if (!properties.TryGetValue(templateArg.Name ?? string.Empty, out value)) { - if (!properties.TryGetValue(templateArg.Name ?? string.Empty, out value)) - throw new InvalidOperationException($"Missing argument \"{templateArg.Name}\"."); - properties.Remove(templateArg.Name!); - } - else - { - value = _args[i]; + throw new InvalidOperationException($"Missing argument \"{templateArg.Name}\"."); } - args[i] = value!; + properties.Remove(templateArg.Name!); + } + else + { + value = _args[i]; + } - // if value is enumerable then we'll need to expand arguments - if (value is IEnumerable) - isBuilt = false; + args[i] = value!; + + // if value is enumerable then we'll need to expand arguments + if (value is IEnumerable) + { + isBuilt = false; } - if (properties.Count > 0) - throw new InvalidOperationException($"Unknown argument{(properties.Count > 1 ? "s" : "")}: {string.Join(", ", properties.Keys)}"); - return new Sql(_sqlContext, isBuilt, _sql, args); } - internal string ToText() + if (properties.Count > 0) { - var sql = new Sql(_sqlContext, _sql, _args?.Values.ToArray()); - return sql.ToText(); + throw new InvalidOperationException( + $"Unknown argument{(properties.Count > 1 ? "s" : "")}: {string.Join(", ", properties.Keys)}"); } - /// - /// Gets a named argument. - /// - public static object Arg(string name) => new TemplateArg(name); + return new Sql(_sqlContext, isBuilt, _sql, args); + } - /// - /// Gets a WHERE expression argument. - /// - public static T? Arg(string name) => default; + internal string ToText() + { + var sql = new Sql(_sqlContext, _sql, _args?.Values.ToArray()); + return sql.ToText(); + } - /// - /// Gets a WHERE IN expression argument. - /// - public static IEnumerable ArgIn(string name) - { - // don't return an empty enumerable, as it breaks NPoco - return new[] { default (T) }; - } + /// + /// Gets a named argument. + /// + public static object Arg(string name) => new TemplateArg(name); + + /// + /// Gets a WHERE expression argument. + /// + public static T? Arg(string name) => default; + + /// + /// Gets a WHERE IN expression argument. + /// + public static IEnumerable ArgIn(string name) => + // don't return an empty enumerable, as it breaks NPoco + new[] {default(T)}; + + // these are created in PocoToSqlExpressionVisitor + internal class TemplateArg + { + public TemplateArg(string? name) => Name = name; + + public string? Name { get; } + + public override string ToString() => "@" + Name; } } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlTemplates.cs b/src/Umbraco.Infrastructure/Persistence/SqlTemplates.cs index 3c180b439fe5..692369c6c839 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlTemplates.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlTemplates.cs @@ -1,34 +1,26 @@ -using System; -using System.Collections.Concurrent; +using System.Collections.Concurrent; using NPoco; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +public class SqlTemplates { - public class SqlTemplates - { - private readonly ConcurrentDictionary _templates = new ConcurrentDictionary(); - private readonly ISqlContext _sqlContext; + private readonly ISqlContext _sqlContext; + private readonly ConcurrentDictionary _templates = new(); - public SqlTemplates(ISqlContext sqlContext) - { - _sqlContext = sqlContext; - } + public SqlTemplates(ISqlContext sqlContext) => _sqlContext = sqlContext; - // for tests - internal void Clear() - { - _templates.Clear(); - } + // for tests + internal void Clear() => _templates.Clear(); - public SqlTemplate Get(string key, Func, Sql> sqlBuilder) + public SqlTemplate Get(string key, Func, Sql> sqlBuilder) + { + SqlTemplate CreateTemplate(string _) { - SqlTemplate CreateTemplate(string _) - { - var sql = sqlBuilder(new Sql(_sqlContext)); - return new SqlTemplate(_sqlContext, sql.SQL, sql.Arguments); - } - - return _templates.GetOrAdd(key, CreateTemplate); + Sql sql = sqlBuilder(new Sql(_sqlContext)); + return new SqlTemplate(_sqlContext, sql.SQL, sql.Arguments); } + + return _templates.GetOrAdd(key, CreateTemplate); } } diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs index 83ab603a35f3..e364fa441bba 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs @@ -1,204 +1,201 @@ -using System; -using System.Collections.Generic; using System.Data; using System.Data.Common; -using System.Linq; using System.Text; using Microsoft.Extensions.Logging; using NPoco; -using StackExchange.Profiling; using Umbraco.Cms.Infrastructure.Migrations.Install; -using Umbraco.Cms.Infrastructure.Persistence.FaultHandling; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +/// +/// Extends NPoco Database for Umbraco. +/// +/// +/// +/// Is used everywhere in place of the original NPoco Database object, and provides additional features +/// such as profiling, retry policies, logging, etc. +/// +/// Is never created directly but obtained from the . +/// +public class UmbracoDatabase : Database, IUmbracoDatabase { + private readonly ILogger _logger; + private readonly IBulkSqlInsertProvider? _bulkSqlInsertProvider; + private readonly DatabaseSchemaCreatorFactory? _databaseSchemaCreatorFactory; + private readonly IEnumerable? _mapperCollection; + private readonly Guid _instanceGuid = Guid.NewGuid(); + private List? _commands; + + #region Ctor /// - /// Extends NPoco Database for Umbraco. + /// Initializes a new instance of the class. /// /// - /// Is used everywhere in place of the original NPoco Database object, and provides additional features - /// such as profiling, retry policies, logging, etc. - /// Is never created directly but obtained from the . + /// Used by UmbracoDatabaseFactory to create databases. + /// Also used by DatabaseBuilder for creating databases and installing/upgrading. /// - public class UmbracoDatabase : Database, IUmbracoDatabase + public UmbracoDatabase( + string connectionString, + ISqlContext sqlContext, + DbProviderFactory provider, + ILogger logger, + IBulkSqlInsertProvider? bulkSqlInsertProvider, + DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, + IEnumerable? mapperCollection = null) + : base(connectionString, sqlContext.DatabaseType, provider, sqlContext.SqlSyntax.DefaultIsolationLevel) { - private readonly ILogger _logger; - private readonly IBulkSqlInsertProvider? _bulkSqlInsertProvider; - private readonly DatabaseSchemaCreatorFactory? _databaseSchemaCreatorFactory; - private readonly IEnumerable? _mapperCollection; - private readonly Guid _instanceGuid = Guid.NewGuid(); - private List? _commands; - - #region Ctor - - /// - /// Initializes a new instance of the class. - /// - /// - /// Used by UmbracoDatabaseFactory to create databases. - /// Also used by DatabaseBuilder for creating databases and installing/upgrading. - /// - public UmbracoDatabase( - string connectionString, - ISqlContext sqlContext, - DbProviderFactory provider, - ILogger logger, - IBulkSqlInsertProvider? bulkSqlInsertProvider, - DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, - IEnumerable? mapperCollection = null) - : base(connectionString, sqlContext.DatabaseType, provider, sqlContext.SqlSyntax.DefaultIsolationLevel) - { - SqlContext = sqlContext; - _logger = logger; - _bulkSqlInsertProvider = bulkSqlInsertProvider; - _databaseSchemaCreatorFactory = databaseSchemaCreatorFactory; - _mapperCollection = mapperCollection; + SqlContext = sqlContext; + _logger = logger; + _bulkSqlInsertProvider = bulkSqlInsertProvider; + _databaseSchemaCreatorFactory = databaseSchemaCreatorFactory; + _mapperCollection = mapperCollection; - Init(); - } + Init(); + } - /// - /// Initializes a new instance of the class. - /// - /// Internal for unit tests only. - internal UmbracoDatabase( - DbConnection connection, - ISqlContext sqlContext, - ILogger logger, - IBulkSqlInsertProvider bulkSqlInsertProvider) - : base(connection, sqlContext.DatabaseType, sqlContext.SqlSyntax.DefaultIsolationLevel) - { - SqlContext = sqlContext; - _logger = logger; - _bulkSqlInsertProvider = bulkSqlInsertProvider; + /// + /// Initializes a new instance of the class. + /// + /// Internal for unit tests only. + internal UmbracoDatabase( + DbConnection connection, + ISqlContext sqlContext, + ILogger logger, + IBulkSqlInsertProvider bulkSqlInsertProvider) + : base(connection, sqlContext.DatabaseType, sqlContext.SqlSyntax.DefaultIsolationLevel) + { + SqlContext = sqlContext; + _logger = logger; + _bulkSqlInsertProvider = bulkSqlInsertProvider; - Init(); - } + Init(); + } - private void Init() - { - EnableSqlTrace = EnableSqlTraceDefault; - NPocoDatabaseExtensions.ConfigureNPocoBulkExtensions(); + private void Init() + { + EnableSqlTrace = EnableSqlTraceDefault; + NPocoDatabaseExtensions.ConfigureNPocoBulkExtensions(); - if (_mapperCollection != null) - { - Mappers.AddRange(_mapperCollection); - } + if (_mapperCollection != null) + { + Mappers.AddRange(_mapperCollection); } + } - #endregion + #endregion - /// - public ISqlContext SqlContext { get; } + /// + public ISqlContext SqlContext { get; } - #region Testing, Debugging and Troubleshooting + #region Testing, Debugging and Troubleshooting - private bool _enableCount; + private bool _enableCount; #if DEBUG_DATABASES private int _spid = -1; private const bool EnableSqlTraceDefault = true; #else - private string? _instanceId; - private const bool EnableSqlTraceDefault = false; + private string? _instanceId; + private const bool EnableSqlTraceDefault = false; #endif - /// - public string InstanceId => + /// + public string InstanceId => #if DEBUG_DATABASES _instanceGuid.ToString("N").Substring(0, 8) + ':' + _spid; #else - _instanceId ??= _instanceGuid.ToString("N").Substring(0, 8); + _instanceId ??= _instanceGuid.ToString("N").Substring(0, 8); #endif - /// - public bool InTransaction { get; private set; } + /// + public bool InTransaction { get; private set; } - protected override void OnBeginTransaction() - { - base.OnBeginTransaction(); - InTransaction = true; - } + protected override void OnBeginTransaction() + { + base.OnBeginTransaction(); + InTransaction = true; + } - protected override void OnAbortTransaction() - { - InTransaction = false; - base.OnAbortTransaction(); - } + protected override void OnAbortTransaction() + { + InTransaction = false; + base.OnAbortTransaction(); + } - protected override void OnCompleteTransaction() - { - InTransaction = false; - base.OnCompleteTransaction(); - } + protected override void OnCompleteTransaction() + { + InTransaction = false; + base.OnCompleteTransaction(); + } - /// - /// Gets or sets a value indicating whether to log all executed Sql statements. - /// - internal bool EnableSqlTrace { get; set; } + /// + /// Gets or sets a value indicating whether to log all executed Sql statements. + /// + internal bool EnableSqlTrace { get; set; } - /// - /// Gets or sets a value indicating whether to count all executed Sql statements. - /// - public bool EnableSqlCount + /// + /// Gets or sets a value indicating whether to count all executed Sql statements. + /// + public bool EnableSqlCount + { + get => _enableCount; + set { - get => _enableCount; - set - { - _enableCount = value; + _enableCount = value; - if (_enableCount == false) - { - SqlCount = 0; - } + if (_enableCount == false) + { + SqlCount = 0; } } + } - /// - /// Gets the count of all executed Sql statements. - /// - public int SqlCount { get; private set; } + /// + /// Gets the count of all executed Sql statements. + /// + public int SqlCount { get; private set; } - internal bool LogCommands - { - get => _commands != null; - set => _commands = value ? new List() : null; - } + internal bool LogCommands + { + get => _commands != null; + set => _commands = value ? new List() : null; + } - internal IEnumerable? Commands => _commands; + internal IEnumerable? Commands => _commands; - public int BulkInsertRecords(IEnumerable records) => _bulkSqlInsertProvider?.BulkInsertRecords(this, records) ?? 0; + public int BulkInsertRecords(IEnumerable records) => + _bulkSqlInsertProvider?.BulkInsertRecords(this, records) ?? 0; - /// - /// Returns the for the database - /// - public DatabaseSchemaResult ValidateSchema() - { - var dbSchema = _databaseSchemaCreatorFactory?.Create(this); - var databaseSchemaValidationResult = dbSchema?.ValidateSchema(); + /// + /// Returns the for the database + /// + public DatabaseSchemaResult ValidateSchema() + { + DatabaseSchemaCreator? dbSchema = _databaseSchemaCreatorFactory?.Create(this); + DatabaseSchemaResult? databaseSchemaValidationResult = dbSchema?.ValidateSchema(); - return databaseSchemaValidationResult ?? new DatabaseSchemaResult(); - } + return databaseSchemaValidationResult ?? new DatabaseSchemaResult(); + } - /// - /// Returns true if Umbraco database tables are detected to be installed - /// - public bool IsUmbracoInstalled() => ValidateSchema().DetermineHasInstalledVersion(); + /// + /// Returns true if Umbraco database tables are detected to be installed + /// + public bool IsUmbracoInstalled() => ValidateSchema().DetermineHasInstalledVersion(); - #endregion + #endregion - #region OnSomething + #region OnSomething - protected override DbConnection OnConnectionOpened(DbConnection connection) + protected override DbConnection OnConnectionOpened(DbConnection connection) + { + if (connection == null) { - if (connection == null) - { - throw new ArgumentNullException(nameof(connection)); - } + throw new ArgumentNullException(nameof(connection)); + } - // TODO: this should probably move to a SQL Server ProviderSpecificInterceptor. + // TODO: this should probably move to a SQL Server ProviderSpecificInterceptor. #if DEBUG_DATABASES // determines the database connection SPID for debugging if (DatabaseType.IsSqlServer()) @@ -216,8 +213,8 @@ protected override DbConnection OnConnectionOpened(DbConnection connection) } #endif - return connection; - } + return connection; + } #if DEBUG_DATABASES protected override void OnConnectionClosing(DbConnection conn) @@ -227,27 +224,34 @@ protected override void OnConnectionClosing(DbConnection conn) } #endif - protected override void OnException(Exception ex) - { - _logger.LogError(ex, "Exception ({InstanceId}).", InstanceId); - _logger.LogDebug("At:\r\n{StackTrace}", Environment.StackTrace); - - if (EnableSqlTrace == false) - _logger.LogDebug("Sql:\r\n{Sql}", CommandToString(LastSQL, LastArgs)); + protected override void OnException(Exception ex) + { + _logger.LogError(ex, "Exception ({InstanceId}).", InstanceId); + _logger.LogDebug("At:\r\n{StackTrace}", Environment.StackTrace); - base.OnException(ex); + if (EnableSqlTrace == false) + { + _logger.LogDebug("Sql:\r\n{Sql}", CommandToString(LastSQL, LastArgs)); } - private DbCommand? _cmd; + base.OnException(ex); + } + + private DbCommand? _cmd; - protected override void OnExecutingCommand(DbCommand cmd) + protected override void OnExecutingCommand(DbCommand cmd) + { + // if no timeout is specified, and the connection has a longer timeout, use it + if (OneTimeCommandTimeout == 0 && CommandTimeout == 0 && cmd.Connection?.ConnectionTimeout > 30) { - // if no timeout is specified, and the connection has a longer timeout, use it - if (OneTimeCommandTimeout == 0 && CommandTimeout == 0 && cmd.Connection?.ConnectionTimeout > 30) - cmd.CommandTimeout = cmd.Connection.ConnectionTimeout; + cmd.CommandTimeout = cmd.Connection.ConnectionTimeout; + } - if (EnableSqlTrace) - _logger.LogDebug("SQL Trace:\r\n{Sql}", CommandToString(cmd).Replace("{", "{{").Replace("}", "}}")); // TODO: these escapes should be builtin + if (EnableSqlTrace) + { + _logger.LogDebug("SQL Trace:\r\n{Sql}", + CommandToString(cmd).Replace("{", "{{").Replace("}", "}}")); // TODO: these escapes should be builtin + } #if DEBUG_DATABASES // detects whether the command is already in use (eg still has an open reader...) @@ -256,99 +260,103 @@ protected override void OnExecutingCommand(DbCommand cmd) if (refsobj != null) _logger.LogDebug("Oops!" + Environment.NewLine + refsobj); #endif - _cmd = cmd; + _cmd = cmd; - base.OnExecutingCommand(cmd); - } + base.OnExecutingCommand(cmd); + } - private string CommandToString(DbCommand cmd) => CommandToString(cmd.CommandText, cmd.Parameters.Cast().Select(x => x.Value).WhereNotNull().ToArray()); + private string CommandToString(DbCommand cmd) => CommandToString(cmd.CommandText, + cmd.Parameters.Cast().Select(x => x.Value).WhereNotNull().ToArray()); - private string CommandToString(string? sql, object[]? args) - { - var text = new StringBuilder(); + private string CommandToString(string? sql, object[]? args) + { + var text = new StringBuilder(); #if DEBUG_DATABASES text.Append(InstanceId); text.Append(": "); #endif - NPocoSqlExtensions.ToText(sql, args, text); + NPocoSqlExtensions.ToText(sql, args, text); - return text.ToString(); - } + return text.ToString(); + } - protected override void OnExecutedCommand(DbCommand cmd) + protected override void OnExecutedCommand(DbCommand cmd) + { + if (_enableCount) { - if (_enableCount) - SqlCount++; + SqlCount++; + } - _commands?.Add(new CommandInfo(cmd)); + _commands?.Add(new CommandInfo(cmd)); - base.OnExecutedCommand(cmd); - } + base.OnExecutedCommand(cmd); + } - #endregion + #endregion - // used for tracking commands - public class CommandInfo + // used for tracking commands + public class CommandInfo + { + public CommandInfo(IDbCommand cmd) { - public CommandInfo(IDbCommand cmd) + Text = cmd.CommandText; + var parameters = new List(); + foreach (IDbDataParameter parameter in cmd.Parameters) { - Text = cmd.CommandText; - var parameters = new List(); - foreach (IDbDataParameter parameter in cmd.Parameters) - parameters.Add(new ParameterInfo(parameter)); - - Parameters = parameters.ToArray(); + parameters.Add(new ParameterInfo(parameter)); } - public string Text { get; } - - public ParameterInfo[] Parameters { get; } + Parameters = parameters.ToArray(); } - // used for tracking commands - public class ParameterInfo - { - public ParameterInfo(IDbDataParameter parameter) - { - Name = parameter.ParameterName; - Value = parameter.Value; - DbType = parameter.DbType; - Size = parameter.Size; - } + public string Text { get; } + + public ParameterInfo[] Parameters { get; } + } - public string Name { get; } - public object? Value { get; } - public DbType DbType { get; } - public int Size { get; } + // used for tracking commands + public class ParameterInfo + { + public ParameterInfo(IDbDataParameter parameter) + { + Name = parameter.ParameterName; + Value = parameter.Value; + DbType = parameter.DbType; + Size = parameter.Size; } - /// - public new T ExecuteScalar(string sql, params object[] args) - => ExecuteScalar(new Sql(sql, args)); + public string Name { get; } + public object? Value { get; } + public DbType DbType { get; } + public int Size { get; } + } - /// - public new T ExecuteScalar(Sql sql) - => ExecuteScalar(sql.SQL, CommandType.Text, sql.Arguments); + /// + public new T ExecuteScalar(string sql, params object[] args) + => ExecuteScalar(new Sql(sql, args)); - /// - /// - /// Be nice if handled upstream GH issue - /// - public new T ExecuteScalar(string sql, CommandType commandType, params object[] args) - { - if (SqlContext.SqlSyntax.ScalarMappers == null) - { - return base.ExecuteScalar(sql, commandType, args); - } + /// + public new T ExecuteScalar(Sql sql) + => ExecuteScalar(sql.SQL, CommandType.Text, sql.Arguments); - if (!SqlContext.SqlSyntax.ScalarMappers.TryGetValue(typeof(T), out IScalarMapper? mapper)) - { - return base.ExecuteScalar(sql, commandType, args); - } + /// + /// + /// Be nice if handled upstream GH issue + /// + public new T ExecuteScalar(string sql, CommandType commandType, params object[] args) + { + if (SqlContext.SqlSyntax.ScalarMappers == null) + { + return base.ExecuteScalar(sql, commandType, args); + } - var result = base.ExecuteScalar(sql, commandType, args); - return (T)mapper.Map(result); + if (!SqlContext.SqlSyntax.ScalarMappers.TryGetValue(typeof(T), out IScalarMapper? mapper)) + { + return base.ExecuteScalar(sql, commandType, args); } + + var result = base.ExecuteScalar(sql, commandType, args); + return (T)mapper.Map(result); } } diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseExtensions.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseExtensions.cs index fff81a322df7..8e6828afd2c8 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseExtensions.cs @@ -1,75 +1,76 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using NPoco; using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +internal static class UmbracoDatabaseExtensions { - internal static class UmbracoDatabaseExtensions + public static UmbracoDatabase AsUmbracoDatabase(this IUmbracoDatabase database) { - public static UmbracoDatabase AsUmbracoDatabase(this IUmbracoDatabase database) + if (database is not UmbracoDatabase asDatabase) { - if (database is not UmbracoDatabase asDatabase) - { - throw new Exception("oops: database."); - } - - return asDatabase; + throw new Exception("oops: database."); } - /// - /// Gets a dictionary of key/values directly from the database, no scope, nothing. - /// - /// Used by to determine the runtime state. - public static IReadOnlyDictionary? GetFromKeyValueTable(this IUmbracoDatabase database, string keyPrefix) + return asDatabase; + } + + /// + /// Gets a dictionary of key/values directly from the database, no scope, nothing. + /// + /// Used by to determine the runtime state. + public static IReadOnlyDictionary? GetFromKeyValueTable(this IUmbracoDatabase database, + string keyPrefix) + { + if (database is null) { - if (database is null) return null; + return null; + } - // create the wildcard where clause - ISqlSyntaxProvider sqlSyntax = database.SqlContext.SqlSyntax; - var whereParam = sqlSyntax.GetStringColumnWildcardComparison( - sqlSyntax.GetQuotedColumnName("key"), - 0, - TextColumnType.NVarchar); + // create the wildcard where clause + ISqlSyntaxProvider sqlSyntax = database.SqlContext.SqlSyntax; + var whereParam = sqlSyntax.GetStringColumnWildcardComparison( + sqlSyntax.GetQuotedColumnName("key"), + 0, + TextColumnType.NVarchar); - var sql = database.SqlContext.Sql() - .Select() - .From() - .Where(whereParam, keyPrefix + sqlSyntax.GetWildcardPlaceholder()); + Sql? sql = database.SqlContext.Sql() + .Select() + .From() + .Where(whereParam, keyPrefix + sqlSyntax.GetWildcardPlaceholder()); - return database.Fetch(sql) - .ToDictionary(x => x.Key!, x => x.Value); - } + return database.Fetch(sql) + .ToDictionary(x => x.Key!, x => x.Value); + } - /// - /// Returns true if the database contains the specified table - /// - /// - /// - /// - public static bool HasTable(this IUmbracoDatabase database, string tableName) + /// + /// Returns true if the database contains the specified table + /// + /// + /// + /// + public static bool HasTable(this IUmbracoDatabase database, string tableName) + { + try { - try - { - return database.SqlContext.SqlSyntax.GetTablesInSchema(database).Any(table => table.InvariantEquals(tableName)); - } - catch (Exception) - { - return false; // will occur if the database cannot connect - } + return database.SqlContext.SqlSyntax.GetTablesInSchema(database) + .Any(table => table.InvariantEquals(tableName)); + } + catch (Exception) + { + return false; // will occur if the database cannot connect } - - /// - /// Returns true if the database contains no tables - /// - /// - /// - public static bool IsDatabaseEmpty(this IUmbracoDatabase database) - => database.SqlContext.SqlSyntax.GetTablesInSchema(database).Any() == false; - } + + /// + /// Returns true if the database contains no tables + /// + /// + /// + public static bool IsDatabaseEmpty(this IUmbracoDatabase database) + => database.SqlContext.SqlSyntax.GetTablesInSchema(database).Any() == false; } diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs index 8677bc1c7517..72ae92ee8069 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs @@ -1,301 +1,299 @@ -using System; using System.Data.Common; -using System.Threading; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NPoco; using NPoco.FluentMappings; using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Infrastructure.Migrations.Install; -using Umbraco.Cms.Infrastructure.Persistence.FaultHandling; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; - -namespace Umbraco.Cms.Infrastructure.Persistence +using MapperCollection = NPoco.MapperCollection; + +namespace Umbraco.Cms.Infrastructure.Persistence; + +/// +/// Default implementation of . +/// +/// +/// +/// This factory implementation creates and manages an "ambient" database connection. When running +/// within an Http context, "ambient" means "associated with that context". Otherwise, it means "static to +/// the current thread". In this latter case, note that the database connection object is not thread safe. +/// +/// +/// It wraps an NPoco UmbracoDatabaseFactory which is initializes with a proper IPocoDataFactory to ensure +/// that NPoco's plumbing is cached appropriately for the whole application. +/// +/// +// TODO: these comments are not true anymore +// TODO: this class needs not be disposable! +public class UmbracoDatabaseFactory : DisposableObjectSlim, IUmbracoDatabaseFactory { + private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory; + private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator; + private readonly IOptions _globalSettings; + private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; + private readonly IMapperCollection _mappers; + private readonly NPocoMapperCollection _npocoMappers; + private IBulkSqlInsertProvider? _bulkSqlInsertProvider; + private DatabaseType? _databaseType; + + private DbProviderFactory? _dbProviderFactory; + private bool _initialized; + + private object _lock = new(); + + private DatabaseFactory? _npocoDatabaseFactory; + private IPocoDataFactory? _pocoDataFactory; + private MapperCollection? _pocoMappers; + private SqlContext _sqlContext = null!; + private ISqlSyntaxProvider? _sqlSyntax; + + private ConnectionStrings? _umbracoConnectionString; + private bool _upgrading; + + #region Constructors /// - /// Default implementation of . + /// Initializes a new instance of the . /// - /// - /// This factory implementation creates and manages an "ambient" database connection. When running - /// within an Http context, "ambient" means "associated with that context". Otherwise, it means "static to - /// the current thread". In this latter case, note that the database connection object is not thread safe. - /// It wraps an NPoco UmbracoDatabaseFactory which is initializes with a proper IPocoDataFactory to ensure - /// that NPoco's plumbing is cached appropriately for the whole application. - /// - // TODO: these comments are not true anymore - // TODO: this class needs not be disposable! - public class UmbracoDatabaseFactory : DisposableObjectSlim, IUmbracoDatabaseFactory + /// Used by the other ctor and in tests. + public UmbracoDatabaseFactory( + ILogger logger, + ILoggerFactory loggerFactory, + IOptions globalSettings, + IOptionsMonitor connectionStrings, + IMapperCollection mappers, + IDbProviderFactoryCreator dbProviderFactoryCreator, + DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, + NPocoMapperCollection npocoMappers) { - private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator; - private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory; - private readonly NPocoMapperCollection _npocoMappers; - private readonly IOptions _globalSettings; - private readonly IMapperCollection _mappers; - private readonly ILogger _logger; - private readonly ILoggerFactory _loggerFactory; - - private object _lock = new object(); - - private DatabaseFactory? _npocoDatabaseFactory; - private IPocoDataFactory? _pocoDataFactory; - private DatabaseType? _databaseType; - private ISqlSyntaxProvider? _sqlSyntax; - private IBulkSqlInsertProvider? _bulkSqlInsertProvider; - private NPoco.MapperCollection? _pocoMappers; - private SqlContext _sqlContext = null!; - private bool _upgrading; - private bool _initialized; - - private ConnectionStrings? _umbracoConnectionString; - - private DbProviderFactory? _dbProviderFactory = null; - - private DbProviderFactory? DbProviderFactory + _globalSettings = globalSettings; + _mappers = mappers ?? throw new ArgumentNullException(nameof(mappers)); + _dbProviderFactoryCreator = dbProviderFactoryCreator ?? + throw new ArgumentNullException(nameof(dbProviderFactoryCreator)); + _databaseSchemaCreatorFactory = databaseSchemaCreatorFactory ?? + throw new ArgumentNullException(nameof(databaseSchemaCreatorFactory)); + _npocoMappers = npocoMappers; + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _loggerFactory = loggerFactory; + + ConnectionStrings umbracoConnectionString = connectionStrings.Get(Constants.System.UmbracoConnectionName); + if (!umbracoConnectionString.IsConnectionStringConfigured()) { - get - { - if (_dbProviderFactory == null) - { - _dbProviderFactory = string.IsNullOrWhiteSpace(ProviderName) - ? null - : _dbProviderFactoryCreator.CreateFactory(ProviderName); - } - - return _dbProviderFactory; - } + logger.LogDebug("Missing connection string, defer configuration."); + return; // not configured } - #region Constructors - - + Configure(umbracoConnectionString); + } + #endregion - /// - /// Initializes a new instance of the . - /// - /// Used by the other ctor and in tests. - public UmbracoDatabaseFactory( - ILogger logger, - ILoggerFactory loggerFactory, - IOptions globalSettings, - IOptionsMonitor connectionStrings, - IMapperCollection mappers, - IDbProviderFactoryCreator dbProviderFactoryCreator, - DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, - NPocoMapperCollection npocoMappers) + private DbProviderFactory? DbProviderFactory + { + get { - _globalSettings = globalSettings; - _mappers = mappers ?? throw new ArgumentNullException(nameof(mappers)); - _dbProviderFactoryCreator = dbProviderFactoryCreator ?? throw new ArgumentNullException(nameof(dbProviderFactoryCreator)); - _databaseSchemaCreatorFactory = databaseSchemaCreatorFactory ?? throw new ArgumentNullException(nameof(databaseSchemaCreatorFactory)); - _npocoMappers = npocoMappers; - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _loggerFactory = loggerFactory; - - ConnectionStrings umbracoConnectionString = connectionStrings.Get(Constants.System.UmbracoConnectionName); - if (!umbracoConnectionString.IsConnectionStringConfigured()) + if (_dbProviderFactory == null) { - logger.LogDebug("Missing connection string, defer configuration."); - return; // not configured + _dbProviderFactory = string.IsNullOrWhiteSpace(ProviderName) + ? null + : _dbProviderFactoryCreator.CreateFactory(ProviderName); } - Configure(umbracoConnectionString); + return _dbProviderFactory; } + } - #endregion - - /// - public bool Configured + /// + public bool Configured + { + get { - get + lock (_lock) { - lock (_lock) - { - return !ConnectionString.IsNullOrWhiteSpace() && !ProviderName.IsNullOrWhiteSpace(); - } + return !ConnectionString.IsNullOrWhiteSpace() && !ProviderName.IsNullOrWhiteSpace(); } } + } - /// - public bool Initialized => Volatile.Read(ref _initialized); + /// + public bool Initialized => Volatile.Read(ref _initialized); - /// - public string? ConnectionString => _umbracoConnectionString?.ConnectionString; + /// + public string? ConnectionString => _umbracoConnectionString?.ConnectionString; - /// - public string? ProviderName => _umbracoConnectionString?.ProviderName; + /// + public string? ProviderName => _umbracoConnectionString?.ProviderName; - /// - public bool CanConnect => - // actually tries to connect to the database (regardless of configured/initialized) - !ConnectionString.IsNullOrWhiteSpace() && !ProviderName.IsNullOrWhiteSpace() && - DbConnectionExtensions.IsConnectionAvailable(ConnectionString, DbProviderFactory); + /// + public bool CanConnect => + // actually tries to connect to the database (regardless of configured/initialized) + !ConnectionString.IsNullOrWhiteSpace() && !ProviderName.IsNullOrWhiteSpace() && + DbConnectionExtensions.IsConnectionAvailable(ConnectionString, DbProviderFactory); - /// - public ISqlContext SqlContext + /// + public ISqlContext SqlContext + { + get { - get - { - // must be initialized to have a context - EnsureInitialized(); + // must be initialized to have a context + EnsureInitialized(); - return _sqlContext; - } + return _sqlContext; } + } - /// - public IBulkSqlInsertProvider? BulkSqlInsertProvider + /// + public IBulkSqlInsertProvider? BulkSqlInsertProvider + { + get { - get - { - // must be initialized to have a bulk insert provider - EnsureInitialized(); + // must be initialized to have a bulk insert provider + EnsureInitialized(); - return _bulkSqlInsertProvider; - } + return _bulkSqlInsertProvider; } + } - /// - public void ConfigureForUpgrade() => _upgrading = true; + /// + public void ConfigureForUpgrade() => _upgrading = true; - /// - public void Configure(ConnectionStrings umbracoConnectionString) + /// + public void Configure(ConnectionStrings umbracoConnectionString) + { + if (umbracoConnectionString is null) { - if (umbracoConnectionString is null) - { - throw new ArgumentNullException(nameof(umbracoConnectionString)); - } + throw new ArgumentNullException(nameof(umbracoConnectionString)); + } - lock (_lock) + lock (_lock) + { + if (Volatile.Read(ref _initialized)) { - if (Volatile.Read(ref _initialized)) - { - throw new InvalidOperationException("Already initialized."); - } - - _umbracoConnectionString = umbracoConnectionString; + throw new InvalidOperationException("Already initialized."); } - // rest to be lazy-initialized + _umbracoConnectionString = umbracoConnectionString; } - private void EnsureInitialized() => LazyInitializer.EnsureInitialized(ref _sqlContext, ref _initialized, ref _lock, Initialize); + // rest to be lazy-initialized + } - private SqlContext Initialize() - { - _logger.LogDebug("Initializing."); + /// + public IUmbracoDatabase CreateDatabase() + { + // must be initialized to create a database + EnsureInitialized(); + return (IUmbracoDatabase)_npocoDatabaseFactory!.GetDatabase(); + } - if (ConnectionString.IsNullOrWhiteSpace()) - { - throw new InvalidOperationException("The factory has not been configured with a proper connection string."); - } + private void EnsureInitialized() => + LazyInitializer.EnsureInitialized(ref _sqlContext, ref _initialized, ref _lock, Initialize); - if (ProviderName.IsNullOrWhiteSpace()) - { - throw new InvalidOperationException("The factory has not been configured with a proper provider name."); - } + private SqlContext Initialize() + { + _logger.LogDebug("Initializing."); - if (DbProviderFactory == null) - { - throw new Exception($"Can't find a provider factory for provider name \"{ProviderName}\"."); - } + if (ConnectionString.IsNullOrWhiteSpace()) + { + throw new InvalidOperationException("The factory has not been configured with a proper connection string."); + } - _databaseType = DatabaseType.Resolve(DbProviderFactory.GetType().Name, ProviderName); - if (_databaseType == null) - { - throw new Exception($"Can't find an NPoco database type for provider name \"{ProviderName}\"."); - } + if (ProviderName.IsNullOrWhiteSpace()) + { + throw new InvalidOperationException("The factory has not been configured with a proper provider name."); + } - _sqlSyntax = _dbProviderFactoryCreator.GetSqlSyntaxProvider(ProviderName!); - if (_sqlSyntax == null) - { - throw new Exception($"Can't find a sql syntax provider for provider name \"{ProviderName}\"."); - } + if (DbProviderFactory == null) + { + throw new Exception($"Can't find a provider factory for provider name \"{ProviderName}\"."); + } - _bulkSqlInsertProvider = _dbProviderFactoryCreator.CreateBulkSqlInsertProvider(ProviderName!); + _databaseType = DatabaseType.Resolve(DbProviderFactory.GetType().Name, ProviderName); + if (_databaseType == null) + { + throw new Exception($"Can't find an NPoco database type for provider name \"{ProviderName}\"."); + } - _databaseType = _sqlSyntax.GetUpdatedDatabaseType(_databaseType, ConnectionString); + _sqlSyntax = _dbProviderFactoryCreator.GetSqlSyntaxProvider(ProviderName!); + if (_sqlSyntax == null) + { + throw new Exception($"Can't find a sql syntax provider for provider name \"{ProviderName}\"."); + } - // ensure we have only 1 set of mappers, and 1 PocoDataFactory, for all database - // so that everything NPoco is properly cached for the lifetime of the application - _pocoMappers = new NPoco.MapperCollection(); - // add all registered mappers for NPoco - _pocoMappers.AddRange(_npocoMappers); + _bulkSqlInsertProvider = _dbProviderFactoryCreator.CreateBulkSqlInsertProvider(ProviderName!); - _pocoMappers.AddRange(_dbProviderFactoryCreator.ProviderSpecificMappers(ProviderName!)); + _databaseType = _sqlSyntax.GetUpdatedDatabaseType(_databaseType, ConnectionString); - var factory = new FluentPocoDataFactory(GetPocoDataFactoryResolver, _pocoMappers); - _pocoDataFactory = factory; - var config = new FluentConfig(xmappers => factory); + // ensure we have only 1 set of mappers, and 1 PocoDataFactory, for all database + // so that everything NPoco is properly cached for the lifetime of the application + _pocoMappers = new MapperCollection(); + // add all registered mappers for NPoco + _pocoMappers.AddRange(_npocoMappers); - // create the database factory - _npocoDatabaseFactory = DatabaseFactory.Config(cfg => - { - cfg.UsingDatabase(CreateDatabaseInstance) // creating UmbracoDatabase instances - .WithFluentConfig(config); // with proper configuration + _pocoMappers.AddRange(_dbProviderFactoryCreator.ProviderSpecificMappers(ProviderName!)); - foreach (IProviderSpecificInterceptor interceptor in _dbProviderFactoryCreator.GetProviderSpecificInterceptors(ProviderName!)) - { - cfg.WithInterceptor(interceptor); - } - }); + var factory = new FluentPocoDataFactory(GetPocoDataFactoryResolver, _pocoMappers); + _pocoDataFactory = factory; + var config = new FluentConfig(xmappers => factory); - if (_npocoDatabaseFactory == null) + // create the database factory + _npocoDatabaseFactory = DatabaseFactory.Config(cfg => + { + cfg.UsingDatabase(CreateDatabaseInstance) // creating UmbracoDatabase instances + .WithFluentConfig(config); // with proper configuration + + foreach (IProviderSpecificInterceptor interceptor in _dbProviderFactoryCreator + .GetProviderSpecificInterceptors(ProviderName!)) { - throw new NullReferenceException("The call to UmbracoDatabaseFactory.Config yielded a null UmbracoDatabaseFactory instance."); + cfg.WithInterceptor(interceptor); } + }); - _logger.LogDebug("Initialized."); - - return new SqlContext(_sqlSyntax, _databaseType, _pocoDataFactory, _mappers); - } - - /// - public IUmbracoDatabase CreateDatabase() + if (_npocoDatabaseFactory == null) { - // must be initialized to create a database - EnsureInitialized(); - return (IUmbracoDatabase) _npocoDatabaseFactory!.GetDatabase(); + throw new NullReferenceException( + "The call to UmbracoDatabaseFactory.Config yielded a null UmbracoDatabaseFactory instance."); } - // gets initialized poco data builders - private InitializedPocoDataBuilder GetPocoDataFactoryResolver(Type type, IPocoDataFactory factory) - => new UmbracoPocoDataBuilder(type, _pocoMappers, _upgrading).Init(); + _logger.LogDebug("Initialized."); - // method used by NPoco's UmbracoDatabaseFactory to actually create the database instance - private UmbracoDatabase? CreateDatabaseInstance() - { - if (ConnectionString is null || SqlContext is null || DbProviderFactory is null) - { - return null; - } + return new SqlContext(_sqlSyntax, _databaseType, _pocoDataFactory, _mappers); + } - return new UmbracoDatabase( - ConnectionString, - SqlContext, - DbProviderFactory, - _loggerFactory.CreateLogger(), - _bulkSqlInsertProvider, - _databaseSchemaCreatorFactory, - _pocoMappers); - } + // gets initialized poco data builders + private InitializedPocoDataBuilder GetPocoDataFactoryResolver(Type type, IPocoDataFactory factory) + => new UmbracoPocoDataBuilder(type, _pocoMappers, _upgrading).Init(); - protected override void DisposeResources() + // method used by NPoco's UmbracoDatabaseFactory to actually create the database instance + private UmbracoDatabase? CreateDatabaseInstance() + { + if (ConnectionString is null || SqlContext is null || DbProviderFactory is null) { - // this is weird, because hybrid accessors store different databases per - // thread, so we don't really know what we are disposing here... - // besides, we don't really want to dispose the factory, which is a singleton... - - // TODO: the class does not need be disposable - //var db = _umbracoDatabaseAccessor.UmbracoDatabase; - //_umbracoDatabaseAccessor.UmbracoDatabase = null; - //db?.Dispose(); - Volatile.Write(ref _initialized, false); + return null; } + + return new UmbracoDatabase( + ConnectionString, + SqlContext, + DbProviderFactory, + _loggerFactory.CreateLogger(), + _bulkSqlInsertProvider, + _databaseSchemaCreatorFactory, + _pocoMappers); } + + protected override void DisposeResources() => + // this is weird, because hybrid accessors store different databases per + // thread, so we don't really know what we are disposing here... + // besides, we don't really want to dispose the factory, which is a singleton... + // TODO: the class does not need be disposable + //var db = _umbracoDatabaseAccessor.UmbracoDatabase; + //_umbracoDatabaseAccessor.UmbracoDatabase = null; + //db?.Dispose(); + Volatile.Write(ref _initialized, false); } diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoPocoDataBuilder.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoPocoDataBuilder.cs index 753563faffde..7b62c212e3c8 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoPocoDataBuilder.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoPocoDataBuilder.cs @@ -1,31 +1,33 @@ -using System; using NPoco; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +/// +/// Umbraco's implementation of NPoco . +/// +/// +/// +/// NPoco PocoDataBuilder analyzes DTO classes and returns infos about the tables and +/// their columns. +/// +/// +/// In some very special occasions, a class may expose a column that we do not want to +/// use. This is essentially when adding a column to the User table: if the code wants the +/// column to exist, and it does not exist yet in the database, because a given migration has +/// not run, then the user cannot log into the site, and cannot upgrade = catch 22. +/// +/// +/// So far, this is very manual. We don't try to be clever and figure out whether the +/// columns exist already. We just ignore it. +/// +/// Beware, the application MUST restart when this class behavior changes. +/// You can override the GetColmunnInfo method to control which columns this includes +/// +internal class UmbracoPocoDataBuilder : PocoDataBuilder { - /// - /// Umbraco's implementation of NPoco . - /// - /// - /// NPoco PocoDataBuilder analyzes DTO classes and returns infos about the tables and - /// their columns. - /// In some very special occasions, a class may expose a column that we do not want to - /// use. This is essentially when adding a column to the User table: if the code wants the - /// column to exist, and it does not exist yet in the database, because a given migration has - /// not run, then the user cannot log into the site, and cannot upgrade = catch 22. - /// So far, this is very manual. We don't try to be clever and figure out whether the - /// columns exist already. We just ignore it. - /// Beware, the application MUST restart when this class behavior changes. - /// You can override the GetColmunnInfo method to control which columns this includes - /// - internal class UmbracoPocoDataBuilder : PocoDataBuilder - { - private readonly bool _upgrading; + private readonly bool _upgrading; - public UmbracoPocoDataBuilder(Type type, MapperCollection? mapper, bool upgrading) - : base(type, mapper) - { - _upgrading = upgrading; - } - } + public UmbracoPocoDataBuilder(Type type, MapperCollection? mapper, bool upgrading) + : base(type, mapper) => + _upgrading = upgrading; } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs index 8b6663051ef5..573095d30731 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs @@ -1,10 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Umbraco.Cms.Core.IO; @@ -16,428 +13,477 @@ using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Abstract class for block editor based editors +/// +public abstract class BlockEditorPropertyEditor : DataEditor { - /// - /// Abstract class for block editor based editors - /// - public abstract class BlockEditorPropertyEditor : DataEditor - { - public const string ContentTypeKeyPropertyKey = "contentTypeKey"; - public const string UdiPropertyKey = "udi"; + public const string ContentTypeKeyPropertyKey = "contentTypeKey"; + public const string UdiPropertyKey = "udi"; - public BlockEditorPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory, - PropertyEditorCollection propertyEditors) - : base(dataValueEditorFactory) - => PropertyEditors = propertyEditors; + public BlockEditorPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + PropertyEditorCollection propertyEditors) + : base(dataValueEditorFactory) + => PropertyEditors = propertyEditors; - private PropertyEditorCollection PropertyEditors { get; } + private PropertyEditorCollection PropertyEditors { get; } - #region Value Editor + #region Value Editor - protected override IDataValueEditor CreateValueEditor() => DataValueEditorFactory.Create(Attribute!); + protected override IDataValueEditor CreateValueEditor() => + DataValueEditorFactory.Create(Attribute!); - internal class BlockEditorPropertyValueEditor : DataValueEditor, IDataValueReference + internal class BlockEditorPropertyValueEditor : DataValueEditor, IDataValueReference + { + private readonly BlockEditorValues _blockEditorValues; + private readonly IDataTypeService _dataTypeService; + private readonly ILogger _logger; + private readonly PropertyEditorCollection _propertyEditors; + + public BlockEditorPropertyValueEditor( + DataEditorAttribute attribute, + PropertyEditorCollection propertyEditors, + IDataTypeService dataTypeService, + IContentTypeService contentTypeService, + ILocalizedTextService textService, + ILogger logger, + IShortStringHelper shortStringHelper, + IJsonSerializer jsonSerializer, + IIOHelper ioHelper, + IPropertyValidationService propertyValidationService) + : base(textService, shortStringHelper, jsonSerializer, ioHelper, attribute) { - private readonly PropertyEditorCollection _propertyEditors; - private readonly IDataTypeService _dataTypeService; - private readonly ILogger _logger; - private readonly BlockEditorValues _blockEditorValues; - - public BlockEditorPropertyValueEditor( - DataEditorAttribute attribute, - PropertyEditorCollection propertyEditors, - IDataTypeService dataTypeService, - IContentTypeService contentTypeService, - ILocalizedTextService textService, - ILogger logger, - IShortStringHelper shortStringHelper, - IJsonSerializer jsonSerializer, - IIOHelper ioHelper, - IPropertyValidationService propertyValidationService) - : base(textService, shortStringHelper, jsonSerializer, ioHelper, attribute) - { - _propertyEditors = propertyEditors; - _dataTypeService = dataTypeService; - _logger = logger; + _propertyEditors = propertyEditors; + _dataTypeService = dataTypeService; + _logger = logger; - _blockEditorValues = new BlockEditorValues(new BlockListEditorDataConverter(), contentTypeService, _logger); - Validators.Add(new BlockEditorValidator(propertyValidationService, _blockEditorValues,contentTypeService)); - Validators.Add(new MinMaxValidator(_blockEditorValues, textService)); - } + _blockEditorValues = new BlockEditorValues(new BlockListEditorDataConverter(), contentTypeService, _logger); + Validators.Add(new BlockEditorValidator(propertyValidationService, _blockEditorValues, contentTypeService)); + Validators.Add(new MinMaxValidator(_blockEditorValues, textService)); + } - public IEnumerable GetReferences(object? value) - { - var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); + public IEnumerable GetReferences(object? value) + { + var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); - var result = new List(); - var blockEditorData = _blockEditorValues.DeserializeAndClean(rawJson); - if (blockEditorData == null) - return Enumerable.Empty(); + var result = new List(); + BlockEditorData? blockEditorData = _blockEditorValues.DeserializeAndClean(rawJson); + if (blockEditorData == null) + { + return Enumerable.Empty(); + } - // loop through all content and settings data - foreach (var row in blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData)) + // loop through all content and settings data + foreach (BlockItemData row in blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue + .SettingsData)) + { + foreach (KeyValuePair prop in row.PropertyValues) { - foreach (var prop in row.PropertyValues) - { - var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; + IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; - var valueEditor = propEditor?.GetValueEditor(); - if (!(valueEditor is IDataValueReference reference)) continue; + IDataValueEditor? valueEditor = propEditor?.GetValueEditor(); + if (!(valueEditor is IDataValueReference reference)) + { + continue; + } - var val = prop.Value.Value?.ToString(); + var val = prop.Value.Value?.ToString(); - var refs = reference.GetReferences(val); + IEnumerable refs = reference.GetReferences(val); - result.AddRange(refs); - } + result.AddRange(refs); } - - return result; } - #region Convert database // editor + return result; + } + + #region Convert database // editor - // note: there is NO variant support here + // note: there is NO variant support here - /// - /// Ensure that sub-editor values are translated through their ToEditor methods - /// - /// - /// - /// - /// - /// - public override object ToEditor(IProperty property, string? culture = null, string? segment = null) - { - var val = property.GetValue(culture, segment); - var valEditors = new Dictionary(); + /// + /// Ensure that sub-editor values are translated through their ToEditor methods + /// + /// + /// + /// + /// + /// + public override object ToEditor(IProperty property, string? culture = null, string? segment = null) + { + var val = property.GetValue(culture, segment); + var valEditors = new Dictionary(); - BlockEditorData? blockEditorData; - try - { - blockEditorData = _blockEditorValues.DeserializeAndClean(val); - } - catch (JsonSerializationException) - { - // if this occurs it means the data is invalid, shouldn't happen but has happened if we change the data format. - return string.Empty; - } + BlockEditorData? blockEditorData; + try + { + blockEditorData = _blockEditorValues.DeserializeAndClean(val); + } + catch (JsonSerializationException) + { + // if this occurs it means the data is invalid, shouldn't happen but has happened if we change the data format. + return string.Empty; + } - if (blockEditorData == null) - return string.Empty; + if (blockEditorData == null) + { + return string.Empty; + } - void MapBlockItemData(List items) + void MapBlockItemData(List items) + { + foreach (BlockItemData row in items) { - foreach (var row in items) + foreach (KeyValuePair prop in row.PropertyValues) { - foreach (var prop in row.PropertyValues) + // create a temp property with the value + // - force it to be culture invariant as the block editor can't handle culture variant element properties + prop.Value.PropertyType.Variations = ContentVariation.Nothing; + var tempProp = new Property(prop.Value.PropertyType); + tempProp.SetValue(prop.Value.Value); + + IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; + if (propEditor == null) { - // create a temp property with the value - // - force it to be culture invariant as the block editor can't handle culture variant element properties - prop.Value.PropertyType.Variations = ContentVariation.Nothing; - var tempProp = new Property(prop.Value.PropertyType); - tempProp.SetValue(prop.Value.Value); - - var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; - if (propEditor == null) - { - // NOTE: This logic was borrowed from Nested Content and I'm unsure why it exists. - // if the property editor doesn't exist I think everything will break anyways? - // update the raw value since this is what will get serialized out - row.RawPropertyValues[prop.Key] = tempProp.GetValue()?.ToString(); - continue; - } - - var dataType = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId); - if (dataType == null) - { - // deal with weird situations by ignoring them (no comment) - row.PropertyValues.Remove(prop.Key); - _logger.LogWarning( - "ToEditor removed property value {PropertyKey} in row {RowId} for property type {PropertyTypeAlias}", - prop.Key, row.Key, property.PropertyType.Alias); - continue; - } - - if (!valEditors.TryGetValue(dataType.Id, out var valEditor)) - { - var tempConfig = dataType.Configuration; - valEditor = propEditor.GetValueEditor(tempConfig); - - valEditors.Add(dataType.Id, valEditor); - } - - var convValue = valEditor.ToEditor(tempProp); - + // NOTE: This logic was borrowed from Nested Content and I'm unsure why it exists. + // if the property editor doesn't exist I think everything will break anyways? // update the raw value since this is what will get serialized out - row.RawPropertyValues[prop.Key] = convValue; + row.RawPropertyValues[prop.Key] = tempProp.GetValue()?.ToString(); + continue; } + + IDataType? dataType = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId); + if (dataType == null) + { + // deal with weird situations by ignoring them (no comment) + row.PropertyValues.Remove(prop.Key); + _logger.LogWarning( + "ToEditor removed property value {PropertyKey} in row {RowId} for property type {PropertyTypeAlias}", + prop.Key, row.Key, property.PropertyType.Alias); + continue; + } + + if (!valEditors.TryGetValue(dataType.Id, out IDataValueEditor? valEditor)) + { + var tempConfig = dataType.Configuration; + valEditor = propEditor.GetValueEditor(tempConfig); + + valEditors.Add(dataType.Id, valEditor); + } + + var convValue = valEditor.ToEditor(tempProp); + + // update the raw value since this is what will get serialized out + row.RawPropertyValues[prop.Key] = convValue; } } + } - MapBlockItemData(blockEditorData.BlockValue.ContentData); - MapBlockItemData(blockEditorData.BlockValue.SettingsData); + MapBlockItemData(blockEditorData.BlockValue.ContentData); + MapBlockItemData(blockEditorData.BlockValue.SettingsData); - // return json convertable object - return blockEditorData.BlockValue; - } + // return json convertable object + return blockEditorData.BlockValue; + } - /// - /// Ensure that sub-editor values are translated through their FromEditor methods - /// - /// - /// - /// - public override object? FromEditor(ContentPropertyData editorValue, object? currentValue) + /// + /// Ensure that sub-editor values are translated through their FromEditor methods + /// + /// + /// + /// + public override object? FromEditor(ContentPropertyData editorValue, object? currentValue) + { + if (editorValue.Value == null || string.IsNullOrWhiteSpace(editorValue.Value.ToString())) { - if (editorValue.Value == null || string.IsNullOrWhiteSpace(editorValue.Value.ToString())) - return null; + return null; + } - BlockEditorData? blockEditorData; - try - { - blockEditorData = _blockEditorValues.DeserializeAndClean(editorValue.Value); - } - catch (JsonSerializationException) - { - // if this occurs it means the data is invalid, shouldn't happen but has happened if we change the data format. - return string.Empty; - } + BlockEditorData? blockEditorData; + try + { + blockEditorData = _blockEditorValues.DeserializeAndClean(editorValue.Value); + } + catch (JsonSerializationException) + { + // if this occurs it means the data is invalid, shouldn't happen but has happened if we change the data format. + return string.Empty; + } - if (blockEditorData == null || blockEditorData.BlockValue.ContentData.Count == 0) - return string.Empty; + if (blockEditorData == null || blockEditorData.BlockValue.ContentData.Count == 0) + { + return string.Empty; + } - void MapBlockItemData(List items) + void MapBlockItemData(List items) + { + foreach (BlockItemData row in items) { - foreach (var row in items) + foreach (KeyValuePair prop in row.PropertyValues) { - foreach (var prop in row.PropertyValues) - { - // Fetch the property types prevalue - var propConfiguration = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId)?.Configuration; + // Fetch the property types prevalue + var propConfiguration = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId) + ?.Configuration; - // Lookup the property editor - var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; - if (propEditor == null) continue; + // Lookup the property editor + IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; + if (propEditor == null) + { + continue; + } - // Create a fake content property data object - var contentPropData = new ContentPropertyData(prop.Value.Value, propConfiguration); + // Create a fake content property data object + var contentPropData = new ContentPropertyData(prop.Value.Value, propConfiguration); - // Get the property editor to do it's conversion - var newValue = propEditor.GetValueEditor().FromEditor(contentPropData, prop.Value.Value); + // Get the property editor to do it's conversion + var newValue = propEditor.GetValueEditor().FromEditor(contentPropData, prop.Value.Value); - // update the raw value since this is what will get serialized out - row.RawPropertyValues[prop.Key] = newValue; - } + // update the raw value since this is what will get serialized out + row.RawPropertyValues[prop.Key] = newValue; } } - - MapBlockItemData(blockEditorData.BlockValue.ContentData); - MapBlockItemData(blockEditorData.BlockValue.SettingsData); - - // return json - return JsonConvert.SerializeObject(blockEditorData.BlockValue, Formatting.None); } - #endregion + MapBlockItemData(blockEditorData.BlockValue.ContentData); + MapBlockItemData(blockEditorData.BlockValue.SettingsData); + + // return json + return JsonConvert.SerializeObject(blockEditorData.BlockValue, Formatting.None); } - /// - /// Validates the min/max of the block editor - /// - private class MinMaxValidator : IValueValidator + #endregion + } + + /// + /// Validates the min/max of the block editor + /// + private class MinMaxValidator : IValueValidator + { + private readonly BlockEditorValues _blockEditorValues; + private readonly ILocalizedTextService _textService; + + public MinMaxValidator(BlockEditorValues blockEditorValues, ILocalizedTextService textService) { - private readonly BlockEditorValues _blockEditorValues; - private readonly ILocalizedTextService _textService; + _blockEditorValues = blockEditorValues; + _textService = textService; + } - public MinMaxValidator(BlockEditorValues blockEditorValues, ILocalizedTextService textService) + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + { + var blockConfig = (BlockListConfiguration?)dataTypeConfiguration; + if (blockConfig == null) { - _blockEditorValues = blockEditorValues; - _textService = textService; + yield break; } - public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + BlockListConfiguration.NumberRange? validationLimit = blockConfig.ValidationLimit; + if (validationLimit == null) { - var blockConfig = (BlockListConfiguration?)dataTypeConfiguration; - if (blockConfig == null) yield break; - - var validationLimit = blockConfig.ValidationLimit; - if (validationLimit == null) yield break; + yield break; + } - var blockEditorData = _blockEditorValues.DeserializeAndClean(value); + BlockEditorData? blockEditorData = _blockEditorValues.DeserializeAndClean(value); - if ((blockEditorData == null && validationLimit.Min.HasValue && validationLimit.Min > 0) - || (blockEditorData != null && validationLimit.Min.HasValue && blockEditorData.Layout?.Count() < validationLimit.Min)) - { - yield return new ValidationResult( - _textService.Localize("validation", "entriesShort", new[] + if ((blockEditorData == null && validationLimit.Min.HasValue && validationLimit.Min > 0) + || (blockEditorData != null && validationLimit.Min.HasValue && + blockEditorData.Layout?.Count() < validationLimit.Min)) + { + yield return new ValidationResult( + _textService.Localize("validation", "entriesShort", + new[] { validationLimit.Min.ToString(), (validationLimit.Min - (blockEditorData?.Layout?.Count() ?? 0)).ToString() }), - new[] { "minCount" }); - } + new[] {"minCount"}); + } - if (blockEditorData != null && validationLimit.Max.HasValue && blockEditorData.Layout?.Count() > validationLimit.Max) - { - yield return new ValidationResult( - _textService.Localize("validation", "entriesExceed", new[] + if (blockEditorData != null && validationLimit.Max.HasValue && + blockEditorData.Layout?.Count() > validationLimit.Max) + { + yield return new ValidationResult( + _textService.Localize("validation", "entriesExceed", + new[] { validationLimit.Max.ToString(), (blockEditorData.Layout.Count() - validationLimit.Max).ToString() }), - new[] { "maxCount" }); - } + new[] {"maxCount"}); } } + } - internal class BlockEditorValidator : ComplexEditorValidator - { - private readonly BlockEditorValues _blockEditorValues; - private readonly IContentTypeService _contentTypeService; + internal class BlockEditorValidator : ComplexEditorValidator + { + private readonly BlockEditorValues _blockEditorValues; + private readonly IContentTypeService _contentTypeService; - public BlockEditorValidator(IPropertyValidationService propertyValidationService, BlockEditorValues blockEditorValues, IContentTypeService contentTypeService) - : base(propertyValidationService) - { - _blockEditorValues = blockEditorValues; - _contentTypeService = contentTypeService; - } + public BlockEditorValidator(IPropertyValidationService propertyValidationService, + BlockEditorValues blockEditorValues, IContentTypeService contentTypeService) + : base(propertyValidationService) + { + _blockEditorValues = blockEditorValues; + _contentTypeService = contentTypeService; + } - protected override IEnumerable GetElementTypeValidation(object? value) + protected override IEnumerable GetElementTypeValidation(object? value) + { + BlockEditorData? blockEditorData = _blockEditorValues.DeserializeAndClean(value); + if (blockEditorData != null) { - var blockEditorData = _blockEditorValues.DeserializeAndClean(value); - if (blockEditorData != null) + // There is no guarantee that the client will post data for every property defined in the Element Type but we still + // need to validate that data for each property especially for things like 'required' data to work. + // Lookup all element types for all content/settings and then we can populate any empty properties. + var allElements = blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData) + .ToList(); + var allElementTypes = _contentTypeService.GetAll(allElements.Select(x => x.ContentTypeKey).ToArray()) + .ToDictionary(x => x.Key); + + foreach (BlockItemData row in allElements) { - // There is no guarantee that the client will post data for every property defined in the Element Type but we still - // need to validate that data for each property especially for things like 'required' data to work. - // Lookup all element types for all content/settings and then we can populate any empty properties. - var allElements = blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData).ToList(); - var allElementTypes = _contentTypeService.GetAll(allElements.Select(x => x.ContentTypeKey).ToArray()).ToDictionary(x => x.Key); - - foreach (var row in allElements) + if (!allElementTypes.TryGetValue(row.ContentTypeKey, out IContentType? elementType)) { - if (!allElementTypes.TryGetValue(row.ContentTypeKey, out var elementType)) - throw new InvalidOperationException($"No element type found with key {row.ContentTypeKey}"); + throw new InvalidOperationException($"No element type found with key {row.ContentTypeKey}"); + } - // now ensure missing properties - foreach (var elementTypeProp in elementType.CompositionPropertyTypes) + // now ensure missing properties + foreach (IPropertyType elementTypeProp in elementType.CompositionPropertyTypes) + { + if (!row.PropertyValues.ContainsKey(elementTypeProp.Alias)) { - if (!row.PropertyValues.ContainsKey(elementTypeProp.Alias)) - { - // set values to null - row.PropertyValues[elementTypeProp.Alias] = new BlockItemData.BlockPropertyValue(null, elementTypeProp); - row.RawPropertyValues[elementTypeProp.Alias] = null; - } + // set values to null + row.PropertyValues[elementTypeProp.Alias] = + new BlockItemData.BlockPropertyValue(null, elementTypeProp); + row.RawPropertyValues[elementTypeProp.Alias] = null; } + } - var elementValidation = new ElementTypeValidationModel(row.ContentTypeAlias, row.Key); - foreach (var prop in row.PropertyValues) - { - elementValidation.AddPropertyTypeValidation( - new PropertyTypeValidationModel(prop.Value.PropertyType, prop.Value.Value)); - } - yield return elementValidation; + var elementValidation = new ElementTypeValidationModel(row.ContentTypeAlias, row.Key); + foreach (KeyValuePair prop in row.PropertyValues) + { + elementValidation.AddPropertyTypeValidation( + new PropertyTypeValidationModel(prop.Value.PropertyType, prop.Value.Value)); } + + yield return elementValidation; } } } + } - /// - /// Used to deserialize json values and clean up any values based on the existence of element types and layout structure - /// - internal class BlockEditorValues + /// + /// Used to deserialize json values and clean up any values based on the existence of element types and layout + /// structure + /// + internal class BlockEditorValues + { + private readonly Lazy> _contentTypes; + private readonly BlockEditorDataConverter _dataConverter; + private readonly ILogger _logger; + + public BlockEditorValues(BlockEditorDataConverter dataConverter, IContentTypeService contentTypeService, + ILogger logger) { - private readonly Lazy> _contentTypes; - private readonly BlockEditorDataConverter _dataConverter; - private readonly ILogger _logger; + _contentTypes = + new Lazy>(() => contentTypeService.GetAll().ToDictionary(c => c.Key)); + _dataConverter = dataConverter; + _logger = logger; + } - public BlockEditorValues(BlockEditorDataConverter dataConverter, IContentTypeService contentTypeService, ILogger logger) - { - _contentTypes = new Lazy>(() => contentTypeService.GetAll().ToDictionary(c => c.Key)); - _dataConverter = dataConverter; - _logger = logger; - } + private IContentType? GetElementType(BlockItemData item) + { + _contentTypes.Value.TryGetValue(item.ContentTypeKey, out IContentType? contentType); + return contentType; + } - private IContentType? GetElementType(BlockItemData item) + public BlockEditorData? DeserializeAndClean(object? propertyValue) + { + if (propertyValue == null || string.IsNullOrWhiteSpace(propertyValue.ToString())) { - _contentTypes.Value.TryGetValue(item.ContentTypeKey, out var contentType); - return contentType; + return null; } - public BlockEditorData? DeserializeAndClean(object? propertyValue) + BlockEditorData blockEditorData = _dataConverter.Deserialize(propertyValue.ToString()!); + + if (blockEditorData.BlockValue.ContentData.Count == 0) { - if (propertyValue == null || string.IsNullOrWhiteSpace(propertyValue.ToString())) - return null; + // if there's no content ensure there's no settings too + blockEditorData.BlockValue.SettingsData.Clear(); + return null; + } - var blockEditorData = _dataConverter.Deserialize(propertyValue.ToString()!); + var contentTypePropertyTypes = new Dictionary>(); - if (blockEditorData.BlockValue.ContentData.Count == 0) - { - // if there's no content ensure there's no settings too - blockEditorData.BlockValue.SettingsData.Clear(); - return null; - } + // filter out any content that isn't referenced in the layout references + foreach (BlockItemData block in blockEditorData.BlockValue.ContentData.Where(x => + blockEditorData.References.Any(r => x.Udi is not null && r.ContentUdi == x.Udi))) + { + ResolveBlockItemData(block, contentTypePropertyTypes); + } - var contentTypePropertyTypes = new Dictionary>(); + // filter out any settings that isn't referenced in the layout references + foreach (BlockItemData block in blockEditorData.BlockValue.SettingsData.Where(x => + blockEditorData.References.Any(r => + r.SettingsUdi is not null && x.Udi is not null && r.SettingsUdi == x.Udi))) + { + ResolveBlockItemData(block, contentTypePropertyTypes); + } - // filter out any content that isn't referenced in the layout references - foreach (var block in blockEditorData.BlockValue.ContentData.Where(x => blockEditorData.References.Any(r => x.Udi is not null && r.ContentUdi == x.Udi))) - { - ResolveBlockItemData(block, contentTypePropertyTypes); - } - // filter out any settings that isn't referenced in the layout references - foreach (var block in blockEditorData.BlockValue.SettingsData.Where(x => blockEditorData.References.Any(r => r.SettingsUdi is not null && x.Udi is not null && r.SettingsUdi == x.Udi))) - { - ResolveBlockItemData(block, contentTypePropertyTypes); - } + // remove blocks that couldn't be resolved + blockEditorData.BlockValue.ContentData.RemoveAll(x => x.ContentTypeAlias.IsNullOrWhiteSpace()); + blockEditorData.BlockValue.SettingsData.RemoveAll(x => x.ContentTypeAlias.IsNullOrWhiteSpace()); - // remove blocks that couldn't be resolved - blockEditorData.BlockValue.ContentData.RemoveAll(x => x.ContentTypeAlias.IsNullOrWhiteSpace()); - blockEditorData.BlockValue.SettingsData.RemoveAll(x => x.ContentTypeAlias.IsNullOrWhiteSpace()); + return blockEditorData; + } - return blockEditorData; + private bool ResolveBlockItemData(BlockItemData block, + Dictionary> contentTypePropertyTypes) + { + IContentType? contentType = GetElementType(block); + if (contentType == null) + { + return false; } - private bool ResolveBlockItemData(BlockItemData block, Dictionary> contentTypePropertyTypes) + // get the prop types for this content type but keep a dictionary of found ones so we don't have to keep re-looking and re-creating + // objects on each iteration. + if (!contentTypePropertyTypes.TryGetValue(contentType.Alias, + out Dictionary? propertyTypes)) { - var contentType = GetElementType(block); - if (contentType == null) - return false; - - // get the prop types for this content type but keep a dictionary of found ones so we don't have to keep re-looking and re-creating - // objects on each iteration. - if (!contentTypePropertyTypes.TryGetValue(contentType.Alias, out var propertyTypes)) - propertyTypes = contentTypePropertyTypes[contentType.Alias] = contentType.CompositionPropertyTypes.ToDictionary(x => x.Alias, x => x); + propertyTypes = contentTypePropertyTypes[contentType.Alias] = + contentType.CompositionPropertyTypes.ToDictionary(x => x.Alias, x => x); + } - var propValues = new Dictionary(); + var propValues = new Dictionary(); - // find any keys that are not real property types and remove them - foreach (var prop in block.RawPropertyValues.ToList()) + // find any keys that are not real property types and remove them + foreach (KeyValuePair prop in block.RawPropertyValues.ToList()) + { + // doesn't exist so remove it + if (!propertyTypes.TryGetValue(prop.Key, out IPropertyType? propType)) { - // doesn't exist so remove it - if (!propertyTypes.TryGetValue(prop.Key, out var propType)) - { - block.RawPropertyValues.Remove(prop.Key); - _logger.LogWarning("The property {PropertyKey} for block {BlockKey} was removed because the property type {PropertyTypeAlias} was not found on {ContentTypeAlias}", - prop.Key, block.Key, prop.Key, contentType.Alias); - } - else - { - // set the value to include the resolved property type - propValues[prop.Key] = new BlockItemData.BlockPropertyValue(prop.Value, propType); - } + block.RawPropertyValues.Remove(prop.Key); + _logger.LogWarning( + "The property {PropertyKey} for block {BlockKey} was removed because the property type {PropertyTypeAlias} was not found on {ContentTypeAlias}", + prop.Key, block.Key, prop.Key, contentType.Alias); + } + else + { + // set the value to include the resolved property type + propValues[prop.Key] = new BlockItemData.BlockPropertyValue(prop.Value, propType); } - - block.ContentTypeAlias = contentType.Alias; - block.PropertyValues = propValues; - - return true; } - } - #endregion + block.ContentTypeAlias = contentType.Alias; + block.PropertyValues = propValues; + return true; + } } + + #endregion } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyHandler.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyHandler.cs index 28691af7ba2b..3ab859b27f1f 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyHandler.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyHandler.cs @@ -1,217 +1,224 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// A handler for Block editors used to bind to notifications +/// +public class BlockEditorPropertyHandler : ComplexPropertyEditorContentNotificationHandler { - /// - /// A handler for Block editors used to bind to notifications - /// - public class BlockEditorPropertyHandler : ComplexPropertyEditorContentNotificationHandler - { - private readonly BlockListEditorDataConverter _converter = new BlockListEditorDataConverter(); - private readonly ILogger _logger; + private readonly BlockListEditorDataConverter _converter = new(); + private readonly ILogger _logger; + + public BlockEditorPropertyHandler(ILogger logger) => _logger = logger; + + protected override string EditorAlias => Constants.PropertyEditors.Aliases.BlockList; - public BlockEditorPropertyHandler(ILogger logger) + protected override string FormatPropertyValue(string rawJson, bool onlyMissingKeys) + { + // the block editor doesn't ever have missing UDIs so when this is true there's nothing to process + if (onlyMissingKeys) { - _logger = logger; + return rawJson; } - protected override string EditorAlias => Constants.PropertyEditors.Aliases.BlockList; + return ReplaceBlockListUdis(rawJson); + } - protected override string FormatPropertyValue(string rawJson, bool onlyMissingKeys) + // internal for tests + internal string ReplaceBlockListUdis(string rawJson, Func? createGuid = null) + { + // used so we can test nicely + if (createGuid == null) { - // the block editor doesn't ever have missing UDIs so when this is true there's nothing to process - if (onlyMissingKeys) - return rawJson; - - return ReplaceBlockListUdis(rawJson, null); + createGuid = () => Guid.NewGuid(); } - // internal for tests - internal string ReplaceBlockListUdis(string rawJson, Func? createGuid = null) + if (string.IsNullOrWhiteSpace(rawJson) || !rawJson.DetectIsJson()) { - // used so we can test nicely - if (createGuid == null) - createGuid = () => Guid.NewGuid(); + return rawJson; + } - if (string.IsNullOrWhiteSpace(rawJson) || !rawJson.DetectIsJson()) - return rawJson; + // Parse JSON + // This will throw a FormatException if there are null UDIs (expected) + BlockEditorData blockListValue = _converter.Deserialize(rawJson); - // Parse JSON - // This will throw a FormatException if there are null UDIs (expected) - var blockListValue = _converter.Deserialize(rawJson); + UpdateBlockListRecursively(blockListValue, createGuid); - UpdateBlockListRecursively(blockListValue, createGuid); + return JsonConvert.SerializeObject(blockListValue.BlockValue, Formatting.None); + } - return JsonConvert.SerializeObject(blockListValue.BlockValue, Formatting.None); - } + private void UpdateBlockListRecursively(BlockEditorData blockListData, Func createGuid) + { + var oldToNew = new Dictionary(); + MapOldToNewUdis(oldToNew, blockListData.BlockValue.ContentData, createGuid); + MapOldToNewUdis(oldToNew, blockListData.BlockValue.SettingsData, createGuid); - private void UpdateBlockListRecursively(BlockEditorData blockListData, Func createGuid) + for (var i = 0; i < blockListData.References.Count; i++) { - var oldToNew = new Dictionary(); - MapOldToNewUdis(oldToNew, blockListData.BlockValue.ContentData, createGuid); - MapOldToNewUdis(oldToNew, blockListData.BlockValue.SettingsData, createGuid); + ContentAndSettingsReference reference = blockListData.References[i]; + var hasContentMap = oldToNew.TryGetValue(reference.ContentUdi, out Udi? contentMap); + Udi? settingsMap = null; + var hasSettingsMap = reference.SettingsUdi is not null && + oldToNew.TryGetValue(reference.SettingsUdi, out settingsMap); - for (var i = 0; i < blockListData.References.Count; i++) + if (hasContentMap) { - var reference = blockListData.References[i]; - var hasContentMap = oldToNew.TryGetValue(reference.ContentUdi, out var contentMap); - Udi? settingsMap = null; - var hasSettingsMap = reference.SettingsUdi is not null && oldToNew.TryGetValue(reference.SettingsUdi, out settingsMap); - - if (hasContentMap) - { - // replace the reference - blockListData.References.RemoveAt(i); - blockListData.References.Insert(i, new ContentAndSettingsReference(contentMap!, hasSettingsMap ? settingsMap : null)); - } + // replace the reference + blockListData.References.RemoveAt(i); + blockListData.References.Insert(i, + new ContentAndSettingsReference(contentMap!, hasSettingsMap ? settingsMap : null)); } + } - // build the layout with the new UDIs - var layout = (JArray?)blockListData.Layout; - layout?.Clear(); - foreach (var reference in blockListData.References) + // build the layout with the new UDIs + var layout = (JArray?)blockListData.Layout; + layout?.Clear(); + foreach (ContentAndSettingsReference reference in blockListData.References) + { + layout?.Add(JObject.FromObject(new BlockListLayoutItem { - layout?.Add(JObject.FromObject(new BlockListLayoutItem - { - ContentUdi = reference.ContentUdi, - SettingsUdi = reference.SettingsUdi - })); - } + ContentUdi = reference.ContentUdi, SettingsUdi = reference.SettingsUdi + })); + } - RecursePropertyValues(blockListData.BlockValue.ContentData, createGuid); - RecursePropertyValues(blockListData.BlockValue.SettingsData, createGuid); - } + RecursePropertyValues(blockListData.BlockValue.ContentData, createGuid); + RecursePropertyValues(blockListData.BlockValue.SettingsData, createGuid); + } - private void RecursePropertyValues(IEnumerable blockData, Func createGuid) + private void RecursePropertyValues(IEnumerable blockData, Func createGuid) + { + foreach (BlockItemData data in blockData) { - foreach (var data in blockData) + // check if we need to recurse (make a copy of the dictionary since it will be modified) + foreach (KeyValuePair propertyAliasToBlockItemData in new Dictionary( + data.RawPropertyValues)) { - // check if we need to recurse (make a copy of the dictionary since it will be modified) - foreach (var propertyAliasToBlockItemData in new Dictionary(data.RawPropertyValues)) + if (propertyAliasToBlockItemData.Value is JToken jtoken) { - if (propertyAliasToBlockItemData.Value is JToken jtoken) + if (ProcessJToken(jtoken, createGuid, out JToken result)) { - if (ProcessJToken(jtoken, createGuid, out var result)) - { - // need to re-save this back to the RawPropertyValues - data.RawPropertyValues[propertyAliasToBlockItemData.Key] = result; - } + // need to re-save this back to the RawPropertyValues + data.RawPropertyValues[propertyAliasToBlockItemData.Key] = result; } - else + } + else + { + var asString = propertyAliasToBlockItemData.Value?.ToString(); + + if (asString != null && asString.DetectIsJson()) { - var asString = propertyAliasToBlockItemData.Value?.ToString(); + // this gets a little ugly because there could be some other complex editor that contains another block editor + // and since we would have no idea how to parse that, all we can do is try JSON Path to find another block editor + // of our type + JToken? json = null; + try + { + json = JToken.Parse(asString); + } + catch (Exception) + { + // See issue https://github.com/umbraco/Umbraco-CMS/issues/10879 + // We are detecting JSON data by seeing if a string is surrounded by [] or {} + // If people enter text like [PLACEHOLDER] JToken parsing fails, it's safe to ignore though + // Logging this just in case in the future we find values that are not safe to ignore + _logger.LogWarning( + "The property {PropertyAlias} on content type {ContentTypeKey} has a value of: {BlockItemValue} - this was recognized as JSON but could not be parsed", + data.Key, propertyAliasToBlockItemData.Key, asString); + } - if (asString != null && asString.DetectIsJson()) + if (json != null && ProcessJToken(json, createGuid, out JToken result)) { - // this gets a little ugly because there could be some other complex editor that contains another block editor - // and since we would have no idea how to parse that, all we can do is try JSON Path to find another block editor - // of our type - JToken? json = null; - try - { - json = JToken.Parse(asString); - } - catch (Exception) - { - // See issue https://github.com/umbraco/Umbraco-CMS/issues/10879 - // We are detecting JSON data by seeing if a string is surrounded by [] or {} - // If people enter text like [PLACEHOLDER] JToken parsing fails, it's safe to ignore though - // Logging this just in case in the future we find values that are not safe to ignore - _logger.LogWarning( "The property {PropertyAlias} on content type {ContentTypeKey} has a value of: {BlockItemValue} - this was recognized as JSON but could not be parsed", - data.Key, propertyAliasToBlockItemData.Key, asString); - } - - if (json != null && ProcessJToken(json, createGuid, out var result)) - { - // need to re-save this back to the RawPropertyValues - data.RawPropertyValues[propertyAliasToBlockItemData.Key] = result; - } + // need to re-save this back to the RawPropertyValues + data.RawPropertyValues[propertyAliasToBlockItemData.Key] = result; } } } } } + } - private bool ProcessJToken(JToken json, Func createGuid, out JToken result) - { - var updated = false; - result = json; + private bool ProcessJToken(JToken json, Func createGuid, out JToken result) + { + var updated = false; + result = json; - // select all tokens (flatten) - var allProperties = json.SelectTokens("$..*").Select(x => x.Parent as JProperty).WhereNotNull().ToList(); - foreach (var prop in allProperties) + // select all tokens (flatten) + var allProperties = json.SelectTokens("$..*").Select(x => x.Parent as JProperty).WhereNotNull().ToList(); + foreach (JProperty prop in allProperties) + { + if (prop.Name == Constants.PropertyEditors.Aliases.BlockList) { - if (prop.Name == Constants.PropertyEditors.Aliases.BlockList) + // get it's parent 'layout' and it's parent's container + var layout = prop.Parent?.Parent as JProperty; + if (layout != null && layout.Parent is JObject layoutJson) { - // get it's parent 'layout' and it's parent's container - var layout = prop.Parent?.Parent as JProperty; - if (layout != null && layout.Parent is JObject layoutJson) - { - // recurse - var blockListValue = _converter.ConvertFrom(layoutJson); - UpdateBlockListRecursively(blockListValue, createGuid); + // recurse + BlockEditorData blockListValue = _converter.ConvertFrom(layoutJson); + UpdateBlockListRecursively(blockListValue, createGuid); - // set new value - if (layoutJson.Parent != null) - { - // we can replace the object - layoutJson.Replace(JObject.FromObject(blockListValue.BlockValue)); - updated = true; - } - else - { - // if there is no parent it means that this json property was the root, in which case we just return - result = JObject.FromObject(blockListValue.BlockValue); - return true; - } + // set new value + if (layoutJson.Parent != null) + { + // we can replace the object + layoutJson.Replace(JObject.FromObject(blockListValue.BlockValue)); + updated = true; + } + else + { + // if there is no parent it means that this json property was the root, in which case we just return + result = JObject.FromObject(blockListValue.BlockValue); + return true; } } - else if (prop.Name != "layout" && prop.Name != "contentData" && prop.Name != "settingsData" && prop.Name != "contentTypeKey") + } + else if (prop.Name != "layout" && prop.Name != "contentData" && prop.Name != "settingsData" && + prop.Name != "contentTypeKey") + { + // this is an arbitrary property that could contain a nested complex editor + var propVal = prop.Value?.ToString(); + // check if this might contain a nested Block Editor + if (!propVal.IsNullOrWhiteSpace() && (propVal?.DetectIsJson() ?? false) && + propVal.InvariantContains(Constants.PropertyEditors.Aliases.BlockList)) { - // this is an arbitrary property that could contain a nested complex editor - var propVal = prop.Value?.ToString(); - // check if this might contain a nested Block Editor - if (!propVal.IsNullOrWhiteSpace() && (propVal?.DetectIsJson() ?? false) && propVal.InvariantContains(Constants.PropertyEditors.Aliases.BlockList)) + if (_converter.TryDeserialize(propVal, out BlockEditorData? nestedBlockData)) { - if (_converter.TryDeserialize(propVal, out var nestedBlockData)) - { - // recurse - UpdateBlockListRecursively(nestedBlockData, createGuid); - // set the value to the updated one - prop.Value = JObject.FromObject(nestedBlockData.BlockValue); - updated = true; - } + // recurse + UpdateBlockListRecursively(nestedBlockData, createGuid); + // set the value to the updated one + prop.Value = JObject.FromObject(nestedBlockData.BlockValue); + updated = true; } } } - - return updated; } - private void MapOldToNewUdis(Dictionary oldToNew, IEnumerable blockData, Func createGuid) + return updated; + } + + private void MapOldToNewUdis(Dictionary oldToNew, IEnumerable blockData, + Func createGuid) + { + foreach (BlockItemData data in blockData) { - foreach (var data in blockData) + // This should never happen since a FormatException will be thrown if one is empty but we'll keep this here + if (data.Udi is null) { - // This should never happen since a FormatException will be thrown if one is empty but we'll keep this here - if (data.Udi is null) - throw new InvalidOperationException("Block data cannot contain a null UDI"); - - // replace the UDIs - var newUdi = GuidUdi.Create(Constants.UdiEntityType.Element, createGuid()); - oldToNew[data.Udi] = newUdi; - data.Udi = newUdi; + throw new InvalidOperationException("Block data cannot contain a null UDI"); } + + // replace the UDIs + Udi newUdi = Udi.Create(Constants.UdiEntityType.Element, createGuid()); + oldToNew[data.Udi] = newUdi; + data.Udi = newUdi; } } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockListConfigurationEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockListConfigurationEditor.cs index a3b3d6233890..f3b5cb1138b5 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockListConfigurationEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockListConfigurationEditor.cs @@ -1,19 +1,15 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +internal class BlockListConfigurationEditor : ConfigurationEditor { - internal class BlockListConfigurationEditor : ConfigurationEditor + public BlockListConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : + base(ioHelper, editorConfigurationParser) { - public BlockListConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - } - } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs index c8be6adf4009..b9e8799c578a 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs @@ -1,58 +1,54 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; -using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents a block list property editor. +/// +[DataEditor( + Constants.PropertyEditors.Aliases.BlockList, + "Block List", + "blocklist", + ValueType = ValueTypes.Json, + Group = Constants.PropertyEditors.Groups.Lists, + Icon = "icon-thumbnail-list")] +public class BlockListPropertyEditor : BlockEditorPropertyEditor { - /// - /// Represents a block list property editor. - /// - [DataEditor( - Constants.PropertyEditors.Aliases.BlockList, - "Block List", - "blocklist", - ValueType = ValueTypes.Json, - Group = Constants.PropertyEditors.Groups.Lists, - Icon = "icon-thumbnail-list")] - public class BlockListPropertyEditor : BlockEditorPropertyEditor + private readonly IEditorConfigurationParser _editorConfigurationParser; + private readonly IIOHelper _ioHelper; + + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public BlockListPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + PropertyEditorCollection propertyEditors, + IIOHelper ioHelper) + : this(dataValueEditorFactory, propertyEditors, ioHelper, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + + public BlockListPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + PropertyEditorCollection propertyEditors, + IIOHelper ioHelper, + IEditorConfigurationParser editorConfigurationParser) + : base(dataValueEditorFactory, propertyEditors) { - private readonly IIOHelper _ioHelper; - private readonly IEditorConfigurationParser _editorConfigurationParser; - - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public BlockListPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory, - PropertyEditorCollection propertyEditors, - IIOHelper ioHelper) - : this(dataValueEditorFactory, propertyEditors, ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } - - public BlockListPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory, - PropertyEditorCollection propertyEditors, - IIOHelper ioHelper, - IEditorConfigurationParser editorConfigurationParser) - : base(dataValueEditorFactory, propertyEditors) - { - _ioHelper = ioHelper; - _editorConfigurationParser = editorConfigurationParser; - } - - #region Pre Value Editor - - protected override IConfigurationEditor CreateConfigurationEditor() => new BlockListConfigurationEditor(_ioHelper, _editorConfigurationParser); - - #endregion + _ioHelper = ioHelper; + _editorConfigurationParser = editorConfigurationParser; } + + #region Pre Value Editor + + protected override IConfigurationEditor CreateConfigurationEditor() => + new BlockListConfigurationEditor(_ioHelper, _editorConfigurationParser); + + #endregion } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/CheckBoxListPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/CheckBoxListPropertyEditor.cs index 226024f8b939..c576dd776971 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/CheckBoxListPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/CheckBoxListPropertyEditor.cs @@ -1,59 +1,60 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// A property editor to allow multiple checkbox selection of pre-defined items. +/// +[DataEditor( + Constants.PropertyEditors.Aliases.CheckBoxList, + "Checkbox list", + "checkboxlist", + Icon = "icon-bulleted-list", + Group = Constants.PropertyEditors.Groups.Lists)] +public class CheckBoxListPropertyEditor : DataEditor { + private readonly IEditorConfigurationParser _editorConfigurationParser; + private readonly IIOHelper _ioHelper; + private readonly ILocalizedTextService _textService; + + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public CheckBoxListPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + ILocalizedTextService textService, + IIOHelper ioHelper) + : this(dataValueEditorFactory, textService, ioHelper, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + /// - /// A property editor to allow multiple checkbox selection of pre-defined items. + /// The constructor will setup the property editor based on the attribute if one is found /// - [DataEditor( - Constants.PropertyEditors.Aliases.CheckBoxList, - "Checkbox list", - "checkboxlist", - Icon = "icon-bulleted-list", - Group = Constants.PropertyEditors.Groups.Lists)] - public class CheckBoxListPropertyEditor : DataEditor + public CheckBoxListPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + ILocalizedTextService textService, + IIOHelper ioHelper, + IEditorConfigurationParser editorConfigurationParser) + : base(dataValueEditorFactory) { - private readonly ILocalizedTextService _textService; - private readonly IIOHelper _ioHelper; - private readonly IEditorConfigurationParser _editorConfigurationParser; - - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public CheckBoxListPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory, - ILocalizedTextService textService, - IIOHelper ioHelper) - : this(dataValueEditorFactory, textService, ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } - - /// - /// The constructor will setup the property editor based on the attribute if one is found - /// - public CheckBoxListPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory, - ILocalizedTextService textService, - IIOHelper ioHelper, - IEditorConfigurationParser editorConfigurationParser) - : base(dataValueEditorFactory) - { - _textService = textService; - _ioHelper = ioHelper; - _editorConfigurationParser = editorConfigurationParser; - } + _textService = textService; + _ioHelper = ioHelper; + _editorConfigurationParser = editorConfigurationParser; + } - /// - protected override IConfigurationEditor CreateConfigurationEditor() => new ValueListConfigurationEditor(_textService, _ioHelper, _editorConfigurationParser); + /// + protected override IConfigurationEditor CreateConfigurationEditor() => + new ValueListConfigurationEditor(_textService, _ioHelper, _editorConfigurationParser); - /// - protected override IDataValueEditor CreateValueEditor() => DataValueEditorFactory.Create(Attribute!); - } + /// + protected override IDataValueEditor CreateValueEditor() => + DataValueEditorFactory.Create(Attribute!); } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs index ff72a7778805..0f21aef88226 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs @@ -1,10 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using System.Runtime.Serialization; using System.Text.RegularExpressions; using Newtonsoft.Json.Linq; @@ -13,170 +10,187 @@ using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +internal class ColorPickerConfigurationEditor : ConfigurationEditor { - internal class ColorPickerConfigurationEditor : ConfigurationEditor + private readonly IJsonSerializer _jsonSerializer; + + public ColorPickerConfigurationEditor(IIOHelper ioHelper, IJsonSerializer jsonSerializer, + IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) + { + _jsonSerializer = jsonSerializer; + ConfigurationField items = Fields.First(x => x.Key == "items"); + + // customize the items field + items.View = "views/propertyeditors/colorpicker/colorpicker.prevalues.html"; + items.Description = "Add, remove or sort colors"; + items.Name = "Colors"; + items.Validators.Add(new ColorListValidator()); + } + + public override Dictionary ToConfigurationEditor(ColorPickerConfiguration? configuration) { - private readonly IJsonSerializer _jsonSerializer; - public ColorPickerConfigurationEditor(IIOHelper ioHelper, IJsonSerializer jsonSerializer, IEditorConfigurationParser editorConfigurationParser) - : base(ioHelper, editorConfigurationParser) + List? configuredItems = configuration?.Items; // ordered + object editorItems; + + if (configuredItems == null) { - _jsonSerializer = jsonSerializer; - var items = Fields.First(x => x.Key == "items"); - - // customize the items field - items.View = "views/propertyeditors/colorpicker/colorpicker.prevalues.html"; - items.Description = "Add, remove or sort colors"; - items.Name = "Colors"; - items.Validators.Add(new ColorListValidator()); + editorItems = new object(); } - - public override Dictionary ToConfigurationEditor(ColorPickerConfiguration? configuration) + else { - var configuredItems = configuration?.Items; // ordered - object editorItems; - - if (configuredItems == null) + var d = new Dictionary(); + editorItems = d; + var sortOrder = 0; + foreach (ValueListConfiguration.ValueListItem item in configuredItems) { - editorItems = new object(); - } - else - { - var d = new Dictionary(); - editorItems = d; - var sortOrder = 0; - foreach (var item in configuredItems) - d[item.Id.ToString()] = GetItemValue(item, configuration!.UseLabel, sortOrder++); + d[item.Id.ToString()] = GetItemValue(item, configuration!.UseLabel, sortOrder++); } + } - var useLabel = configuration?.UseLabel ?? false; + var useLabel = configuration?.UseLabel ?? false; - return new Dictionary - { - { "items", editorItems }, - { "useLabel", useLabel } - }; - } + return new Dictionary {{"items", editorItems}, {"useLabel", useLabel}}; + } - private object GetItemValue(ValueListConfiguration.ValueListItem item, bool useLabel, int sortOrder) - { - // in: ValueListItem, Id = , Value = | { "value": "", "label": "