From 3961c4c233ddc20d4efd2417436537f2488ee8da Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Fri, 11 Mar 2022 16:14:20 +0000 Subject: [PATCH] v10 SQLite support + distributed locking abstractions (#11922) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Created Persistence.SQLite project skeleton. * SQLite database initialization * Various changes and hacks to make things work. * WIP integration tests * Fix thread safety tests * Fix tests that relied on tie breaker sorting. Spent a fair amount of time looking for a less lazy fix but gave up. * Convert right join to left join ContentTypeRepository.PerformGetByQuery SQLite doesn't support right join * Fix test Can_Generate_Delete_SubQuery_Statement Worth noting that NPoco.DatabaseTypes.SQLiteDatabaseType doesn't override EscapeSqlIdentifier so NPoco will escape with []. SQLite docs say > "A keyword enclosed in square brackets is an identifier. This is not standard SQL. This quoting mechanism is used by MS Access and SQL Server and is included in SQLite for compatibility." Also could have updated SqliteSyntaxProvider to match npoco but decided against it. * Fixes for paginated custom order by * Fix tests broken by lack of unique indexes. * Fix SqlServerTableByTableTest tests. These tests didn't actually do anything as the tables already exist so schema creator just returned. Did however point out that the default implementation for DoesTableExist just returns false so added a default naive implementation. * Fix ValidateLoginSession - SelectTop must come later * dry up database cleanup * Fix up db migration tests. We can't drop pk in sqlite without recreating table. Test looks to be testing that add column works as intended which we can test. * Prevent schema creation errors. * SQLite ignore lock tests, WAL back on. * Fix package schema tests * Fix NPocoFetchTests - case sensitivity not under test * Fix AdvancedMigrationTests (where possible) Migrations probably need a good look later. Maybe nuke old migrations and only support moving to v10 from v9. If we do that can do some cleanup. * Cleanup test database configuration * Run integration tests against SQLite on build agent. * Drop MS.Data.SQLite System.Data.SQLite was quicker to roll out due to more CLR type mapping * YAML * Skip Umbraco.Tests.Integration.SqlCe * Drop SqlServerTableByTable tests. Until this week they did nothing anyway as they with NewSchemaPerTest so the tests all passed as CreateTable was no op (already exists). Also all of the tables are created in an empty database by SchemaValidationTest.cs DatabaseSchemaCreation_Produces_DatabaseSchemaResult_With_Zero_Errors * Might aswell run against macOS also. * Copy azure pipelines task header layout * Delete SQLCe projects * Remove SQL CE specific code. * Remove SQL CE NuSpec, template params, build script setup * Delete umbraco-netcore-only.sln * Add SkipTests solution configuration and use for codeql * Remove reference to deleted nuspec file. * Refactor ConnectionStrings WRT DataDirectory placeholder & ProviderName. At this point you can try out SQLite support by setting the following in appsettings.json and then completing the install process. "ConnectionStrings": { "umbracoDbDSN": "Data Source=|DataDirectory|/umbraco.sqlite", "umbracoDbDSN_ProviderName": "System.Data.SQLite" }, Not currently possible via installer UI without provider name pre-set in configuration. * Switch to Microsoft.Data.Sqlite Some gross hacks but will be good to find out if this works with apple silicon. * Enable selection of SQLite via installer UI (also quick install) * Remove SqlServerDbProviderFactoryCreator to cleanup a TODO * Move SQL Server support to its own class library * Add persistence dependencies to Umbraco.CMS metapackage * Bugfix packages delete query Created invalid query for SQLite. * Try out cypress tests Linux + SQLite * Prevent cypress test artifact upload failure on attempt 2+ * LocalDb bugfixes * Drop redundant enum * Move SqlClient constant * Misc whitespace * Remove IsSqlCe extension (TODO: drop non 9->10 migrations later). * Umbraco.Persistence.* -> Umbraco.Cms.Persistence.* * Display quick install defaults and per provider default database name. * Misc remove old comment * little re-arrange * Remove almost all usages of IsSqlite extension. * visual adjustments * Custom Database Configuration is last step and should then say Install. * use text instead of disabled inputs * move legend, rename to Install * Update SqlMainDomLock to work without distributed locks. * Added IDistributedLockingMechanism interface and in memory impl. * Drop locking from ISqlSyntaxProvider & wire up scope to abstraction. * Added SqlServerDistributedLockingMechanism * Move distributed locking interfaces and exceptions to Core + xmldocs. * Fix tests, Misc cleanup, Add SQL distributed locking integration tests * Provide mechanism to specify DistributedLockingMechanism in config (even if added by composer) * Nomplementation -> NoImplementation * Fix misleading comment * Integration tests use SqlServerDistributedLockingMechanism when possible * Handle up-gradable locks SqlServerDistributedLockingMechanism. TODO: InMemoryDistributedLockingMechanism. Note: Nuked SqlServerDistributedLockingMechanismTests, will still sleep at night. Is covered by Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.LockTests * Make tests pass for InMemoryDistributedLockingMechanism, pretty hacky. * Tweak constraints on WithCollectionBuilder so i can drop bad constructor * Added SqliteDistributedLockingMechanism * Dropped InMemoryDistributedMechanism + magic InMemoryDistributedMechanism was pretty rubbish and now we have a decent implementation for SQLite as we no longer block readers see 8d1f42b. Also drop the CollectionBuilder setup, instead do the same as we do for syntax providers etc, it's more automagical so we never require an explicit selection although we are allowing for it. However keeping the optional IUmbracoBuilder constructor param for CollectionBuilders as it's extremely useful. * Fix quick install "" database name. * Hide Database Configuration section when a connection string is pre-set. Doesn't seem worth it to extract db name from connection string. * Ensure wal test 2+ * Fix logging inconsistencies. * Ensure in transaction when obtaining locks + no-op the SQLite read lock. There's no point in running the query just to make a single test pass. * Fix installer database display names * Allow SQLite shared cache without losing deferred transactions * Opt into shared cache for new SQLite databases + fix filename * Fix misc inconsistency in .gitignore * Prefer our interceptor interface * Restore DEBUG_DATABASES code OnConnectionOpened in case it's used. * Back to private cache. * Added retry strategy for SQLite + refactor out SQL server specific stuff * Fix SQL server tests. * Misc - Orphaned comment, incorrect casing. * InMemory SQLite test database & turn shared cache back on everywhere. Co-authored-by: Niels Lyngsø --- .devcontainer/devcontainer.json | 2 +- .github/workflows/codeql-analysis.yml | 2 +- .gitignore | 1 + .vscode/tasks.json | 2 +- build/NuSpecs/UmbracoCms.SqlCe.nuspec | 43 -- build/NuSpecs/UmbracoCms.nuspec | 2 + build/azure-pipelines.yml | 252 +++++++-- build/build.ps1 | 33 +- .../.template.config/dotnetcli.host.json | 5 - .../.template.config/ide.host.json | 7 - .../.template.config/template.json | 6 - .../UmbracoProject/UmbracoProject.csproj | 2 - .../Constants.cs | 12 + .../Dtos/ColumnInSchemaDto.cs | 2 +- .../Dtos/ConstraintPerColumnDto.cs | 2 +- .../Dtos/ConstraintPerTableDto.cs | 2 +- .../Dtos/DefaultConstraintPerColumnDto.cs | 2 +- .../Dtos/DefinedIndexDto.cs | 2 +- .../SqlServerAddMiniProfilerInterceptor.cs | 11 + .../SqlServerAddRetryPolicyInterceptor.cs | 34 ++ .../SqlServerConnectionInterceptor.cs | 16 + .../Services}/BulkDataReader.cs | 4 +- .../MicrosoftSqlSyntaxProviderBase.cs | 52 +- .../Services}/PocoDataDataReader.cs | 6 +- .../SqlAzureDatabaseProviderMetadata.cs | 101 ++++ .../SqlLocalDbDatabaseProviderMetadata.cs | 66 +++ .../SqlServerBulkSqlInsertProvider.cs | 13 +- .../Services}/SqlServerDatabaseCreator.cs | 8 +- .../SqlServerDatabaseProviderMetadata.cs | 57 ++ .../SqlServerDistributedLockingMechanism.cs | 169 ++++++ .../Services}/SqlServerSyntaxProvider.cs | 226 ++++---- .../SqlServerComposer.cs | 14 + .../Umbraco.Cms.Persistence.SqlServer.csproj | 26 + .../UmbracoBuilderExtensions.cs | 42 ++ .../Constants.cs | 12 + .../SqliteAddMiniProfilerInterceptor.cs | 11 + .../SqliteAddPreferDeferredInterceptor.cs | 12 + .../SqliteAddRetryPolicyInterceptor.cs | 16 + .../SqliteConnectionInterceptor.cs | 16 + .../Mappers/SqliteGuidScalarMapper.cs | 24 + .../Mappers/SqlitePocoGuidMapper.cs | 33 ++ .../Services/SqliteBulkSqlInsertProvider.cs | 55 ++ .../Services/SqliteDatabaseCreator.cs | 48 ++ .../SqliteDatabaseProviderMetadata.cs | 72 +++ .../SqliteDistributedLockingMechanism.cs | 157 ++++++ .../Services/SqliteExceptionExtensions.cs | 12 + ...itePreferDeferredTransactionsConnection.cs | 124 +++++ .../Services/SqliteSpecificMapperFactory.cs | 16 + .../Services/SqliteSyntaxProvider.cs | 424 +++++++++++++++ .../SqliteTransientErrorDetectionStrategy.cs | 17 + .../SqliteComposer.cs | 14 + .../Umbraco.Cms.Persistence.Sqlite.csproj | 21 + .../UmbracoBuilderExtensions.cs | 41 ++ .../Configuration/ConfigConnectionString.cs | 92 ---- .../ConfigureConnectionStrings.cs | 40 ++ .../Configuration/Models/ConnectionStrings.cs | 55 +- .../Configuration/Models/GlobalSettings.cs | 25 +- .../Validation/GlobalSettingsValidator.cs | 4 +- src/Umbraco.Core/Constants-Configuration.cs | 1 + .../Constants-DatabaseProviders.cs | 11 - src/Umbraco.Core/Constants-System.cs | 5 +- .../DependencyInjection/IUmbracoBuilder.cs | 2 +- .../UmbracoBuilder.Configuration.cs | 2 + .../DependencyInjection/UmbracoBuilder.cs | 19 +- .../DistributedLocking/DistributedLockType.cs | 10 + .../Exceptions/DistributedLockingException.cs | 26 + .../DistributedLockingTimeoutException.cs | 15 + .../DistributedReadLockTimeoutException.cs | 15 + .../DistributedWriteLockTimeoutException.cs | 15 + .../DistributedLocking/IDistributedLock.cs | 19 + .../IDistributedLockingMechanism.cs | 51 ++ .../IDistributedLockingMechanismFactory.cs | 9 + .../ConfigConnectionStringExtensions.cs | 15 - .../Extensions/ConnectionStringExtensions.cs | 35 ++ .../Install/Models/DatabaseModel.cs | 8 +- .../Install/Models/DatabaseType.cs | 11 - .../Persistence/Constants-DbProviderNames.cs | 12 - .../Configuration/JsonConfigManipulator.cs | 5 +- .../UmbracoBuilder.CoreServices.cs | 3 + ...faultDistributedLockingMechanismFactory.cs | 64 +++ .../Install/InstallHelper.cs | 9 +- .../InstallSteps/DatabaseConfigureStep.cs | 93 +--- .../InstallSteps/DatabaseUpgradeStep.cs | 2 +- .../Install/InstallSteps/NewInstallStep.cs | 23 +- .../Create/Table/CreateTableOfDtoBuilder.cs | 11 +- .../DeleteKeysAndIndexesBuilder.cs | 15 + .../Migrations/Install/DatabaseBuilder.cs | 228 ++------ .../Migrations/Install/DatabaseDataCreator.cs | 2 +- .../Install/DatabaseSchemaCreator.cs | 42 +- .../Migrations/MigrationBase_Extra.cs | 14 +- .../Migrations/MigrationExpressionBase.cs | 2 +- .../V_8_0_0/RenameMediaVersionTable.cs | 16 +- .../Upgrade/V_8_0_0/VariantsMigration.cs | 42 +- .../V_8_15_0/AddCmsContentNuByteColumn.cs | 17 +- .../V_8_15_0/UpdateCmsPropertyGroupIdSeed.cs | 5 +- .../V_9_0_0/DictionaryTablesIndexes.cs | 16 - .../UpdateExternalLoginToUseKeyInsteadOfId.cs | 18 +- .../Persistence/BasicBulkSqlInsertProvider.cs | 36 -- ...onnectionStringDatabaseProviderMetadata.cs | 55 ++ .../DefinitionFactory.cs | 2 +- .../Persistence/DbProviderFactoryCreator.cs | 12 +- .../FaultHandling/RetryDbConnection.cs | 2 +- .../FaultHandling/RetryPolicyFactory.cs | 2 + .../Persistence/IDatabaseProviderMetadata.cs | 90 ++++ .../Persistence/IDbProviderFactoryCreator.cs | 2 + .../IProviderSpecificInterceptor.cs | 28 + .../Persistence/IScalarMapper.cs | 13 + .../Persistence/IUmbracoDatabaseFactory.cs | 3 +- .../NPocoDatabaseTypeExtensions.cs | 32 +- .../Persistence/NPocoSqlExtensions.cs | 112 +--- .../Implement/ContentTypeRepository.cs | 4 +- .../Implement/ContentTypeRepositoryBase.cs | 10 +- .../CreatedPackageSchemaRepository.cs | 8 +- .../Implement/EntityRepository.cs | 3 +- .../Implement/ExternalLoginRepository.cs | 2 +- .../Implement/RelationRepository.cs | 8 +- .../Repositories/Implement/TagRepository.cs | 11 +- .../Implement/TrackedReferencesRepository.cs | 26 +- .../Repositories/Implement/UserRepository.cs | 4 +- .../Persistence/ScalarMapper.cs | 14 + .../SqlServerDbProviderFactoryCreator.cs | 53 -- .../Persistence/SqlSyntax/ColumnInfo.cs | 9 + .../SqlSyntax/ISqlSyntaxProvider.cs | 37 +- .../SqlSyntax/SqlSyntaxProviderBase.cs | 45 +- .../SqlSyntax/SqlSyntaxProviderExtensions.cs | 2 +- .../Persistence/SqlSyntaxExtensions.cs | 11 +- .../Persistence/UmbracoDatabase.cs | 72 ++- .../Persistence/UmbracoDatabaseFactory.cs | 165 ++---- .../Runtime/RuntimeState.cs | 3 +- .../Runtime/SqlMainDomLock.cs | 81 +-- src/Umbraco.Infrastructure/Scoping/Scope.cs | 226 ++++---- .../Scoping/ScopeProvider.cs | 5 + .../SqlCeBulkSqlInsertProvider.cs | 90 ---- .../SqlCeDatabaseCreator.cs | 16 - .../SqlCeImageMapper.cs | 70 --- .../SqlCeSpecificMapperFactory.cs | 11 - .../SqlCeSyntaxProvider.cs | 322 ----------- .../Umbraco.Persistence.SqlCe.csproj | 39 -- .../Persistence/NuCacheContentRepository.cs | 2 +- .../Install/InstallApiController.cs | 8 +- .../UmbracoBuilderExtensions.cs | 67 +-- .../installer/steps/database.controller.js | 69 ++- .../src/installer/steps/database.html | 113 ++-- .../src/installer/steps/user.controller.js | 8 +- .../src/installer/steps/user.html | 150 +++--- .../src/less/installer.less | 14 +- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 11 +- .../ModelToSqlExpressionHelperBenchmarks.cs | 1 + .../SqlTemplatesBenchmark.cs | 1 + .../Umbraco.Tests.Benchmarks.csproj | 1 + tests/Umbraco.Tests.Common/TestHelperBase.cs | 2 - .../TestHelpers/TestDatabase.cs | 1 + .../Umbraco.Tests.Common.csproj | 1 + .../Persistence/DatabaseBuilderTests.cs | 85 --- .../Umbraco.Tests.Integration.SqlCe.csproj | 32 -- .../GlobalSetupTeardown.cs | 30 +- .../Implementations/TestHelper.cs | 4 +- .../UmbracoTestServerTestBase.cs | 5 + .../Testing/BaseTestDatabase.cs | 165 ++---- .../Testing/LocalDbTestDatabase.cs | 12 +- .../Testing/SqlServerBaseTestDatabase.cs | 119 ++++ ...stDatabase.cs => SqlServerTestDatabase.cs} | 36 +- .../Testing/SqliteTestDatabase.cs | 164 ++++++ .../Testing/TestDatabaseFactory.cs | 59 +- .../Testing/TestDatabaseSettings.cs | 17 + .../Testing/TestDbMeta.cs | 23 +- .../Testing/UmbracoIntegrationTest.cs | 8 +- .../Testing/UmbracoIntegrationTestBase.cs | 164 +++--- .../Migrations/AdvancedMigrationTests.cs | 59 +- .../Packaging/CreatedPackageSchemaTests.cs | 6 +- .../Persistence/LocksTests.cs | 116 +++- .../Persistence/NPocoTests/NPocoFetchTests.cs | 2 +- .../Repositories/DocumentRepositoryTest.cs | 1 + .../Repositories/MacroRepositoryTest.cs | 5 +- .../ServerRegistrationRepositoryTest.cs | 5 +- .../Persistence/SchemaValidationTest.cs | 6 +- .../Persistence/SqlServerTableByTableTest.cs | 508 ------------------ .../SqlServerSyntaxProviderTests.cs | 14 +- .../Persistence/UnitOfWorkTests.cs | 9 + .../ContentTypeServiceVariantsTests.cs | 26 +- .../Services/ThreadSafetyServiceTest.cs | 31 +- .../Umbraco.Tests.Integration.csproj | 5 + .../appsettings.Tests.json | 11 + .../Customizations/UmbracoCustomizations.cs | 9 - .../TestHelpers/BaseUsingSqlSyntax.cs | 1 + .../TestHelpers/TestHelper.cs | 5 +- .../ConfigureConnectionStringsTests.cs | 101 ++++ .../Models/ConnectionStringsTests.cs | 33 +- .../Umbraco.Core/Components/ComponentTests.cs | 8 +- .../GlobalSettingsValidatorTests.cs | 6 +- .../UmbracoBuilderExtensionsTests.cs | 2 +- .../ScopedNotificationPublisherTests.cs | 3 + .../Migrations/MigrationPlanTests.cs | 1 + .../Migrations/PostMigrationTests.cs | 1 + .../Persistence/BulkDataReaderTests.cs | 1 + .../NPocoTests/NPocoSqlTemplateTests.cs | 1 + .../Persistence/Querying/ExpressionTests.cs | 1 + ... SqlAzureDatabaseProviderMetadataTests.cs} | 23 +- .../Scoping/ScopeUnitTests.cs | 46 +- umbraco-netcore-only.sln | 225 -------- umbraco.sln | 58 +- 201 files changed, 4346 insertions(+), 3564 deletions(-) delete mode 100644 build/NuSpecs/UmbracoCms.SqlCe.nuspec create mode 100644 src/Umbraco.Cms.Persistence.SqlServer/Constants.cs rename src/{Umbraco.Infrastructure/Persistence => Umbraco.Cms.Persistence.SqlServer}/Dtos/ColumnInSchemaDto.cs (91%) rename src/{Umbraco.Infrastructure/Persistence => Umbraco.Cms.Persistence.SqlServer}/Dtos/ConstraintPerColumnDto.cs (85%) rename src/{Umbraco.Infrastructure/Persistence => Umbraco.Cms.Persistence.SqlServer}/Dtos/ConstraintPerTableDto.cs (81%) rename src/{Umbraco.Infrastructure/Persistence => Umbraco.Cms.Persistence.SqlServer}/Dtos/DefaultConstraintPerColumnDto.cs (87%) rename src/{Umbraco.Infrastructure/Persistence => Umbraco.Cms.Persistence.SqlServer}/Dtos/DefinedIndexDto.cs (87%) create mode 100644 src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerAddMiniProfilerInterceptor.cs create mode 100644 src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerAddRetryPolicyInterceptor.cs create mode 100644 src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerConnectionInterceptor.cs rename src/{Umbraco.Infrastructure/Persistence => Umbraco.Cms.Persistence.SqlServer/Services}/BulkDataReader.cs (99%) rename src/{Umbraco.Infrastructure/Persistence/SqlSyntax => Umbraco.Cms.Persistence.SqlServer/Services}/MicrosoftSqlSyntaxProviderBase.cs (78%) rename src/{Umbraco.Infrastructure/Persistence => Umbraco.Cms.Persistence.SqlServer/Services}/PocoDataDataReader.cs (97%) create mode 100644 src/Umbraco.Cms.Persistence.SqlServer/Services/SqlAzureDatabaseProviderMetadata.cs create mode 100644 src/Umbraco.Cms.Persistence.SqlServer/Services/SqlLocalDbDatabaseProviderMetadata.cs rename src/{Umbraco.Infrastructure/Persistence => Umbraco.Cms.Persistence.SqlServer/Services}/SqlServerBulkSqlInsertProvider.cs (88%) rename src/{Umbraco.Infrastructure/Persistence => Umbraco.Cms.Persistence.SqlServer/Services}/SqlServerDatabaseCreator.cs (92%) create mode 100644 src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseProviderMetadata.cs create mode 100644 src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDistributedLockingMechanism.cs rename src/{Umbraco.Infrastructure/Persistence/SqlSyntax => Umbraco.Cms.Persistence.SqlServer/Services}/SqlServerSyntaxProvider.cs (76%) create mode 100644 src/Umbraco.Cms.Persistence.SqlServer/SqlServerComposer.cs create mode 100644 src/Umbraco.Cms.Persistence.SqlServer/Umbraco.Cms.Persistence.SqlServer.csproj create mode 100644 src/Umbraco.Cms.Persistence.SqlServer/UmbracoBuilderExtensions.cs create mode 100644 src/Umbraco.Cms.Persistence.Sqlite/Constants.cs create mode 100644 src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddMiniProfilerInterceptor.cs create mode 100644 src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddPreferDeferredInterceptor.cs create mode 100644 src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddRetryPolicyInterceptor.cs create mode 100644 src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteConnectionInterceptor.cs create mode 100644 src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqliteGuidScalarMapper.cs create mode 100644 src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoGuidMapper.cs create mode 100644 src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteBulkSqlInsertProvider.cs create mode 100644 src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseCreator.cs create mode 100644 src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseProviderMetadata.cs create mode 100644 src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDistributedLockingMechanism.cs create mode 100644 src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteExceptionExtensions.cs create mode 100644 src/Umbraco.Cms.Persistence.Sqlite/Services/SqlitePreferDeferredTransactionsConnection.cs create mode 100644 src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSpecificMapperFactory.cs create mode 100644 src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs create mode 100644 src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteTransientErrorDetectionStrategy.cs create mode 100644 src/Umbraco.Cms.Persistence.Sqlite/SqliteComposer.cs create mode 100644 src/Umbraco.Cms.Persistence.Sqlite/Umbraco.Cms.Persistence.Sqlite.csproj create mode 100644 src/Umbraco.Cms.Persistence.Sqlite/UmbracoBuilderExtensions.cs delete mode 100644 src/Umbraco.Core/Configuration/ConfigConnectionString.cs create mode 100644 src/Umbraco.Core/Configuration/ConfigureConnectionStrings.cs delete mode 100644 src/Umbraco.Core/Constants-DatabaseProviders.cs create mode 100644 src/Umbraco.Core/DistributedLocking/DistributedLockType.cs create mode 100644 src/Umbraco.Core/DistributedLocking/Exceptions/DistributedLockingException.cs create mode 100644 src/Umbraco.Core/DistributedLocking/Exceptions/DistributedLockingTimeoutException.cs create mode 100644 src/Umbraco.Core/DistributedLocking/Exceptions/DistributedReadLockTimeoutException.cs create mode 100644 src/Umbraco.Core/DistributedLocking/Exceptions/DistributedWriteLockTimeoutException.cs create mode 100644 src/Umbraco.Core/DistributedLocking/IDistributedLock.cs create mode 100644 src/Umbraco.Core/DistributedLocking/IDistributedLockingMechanism.cs create mode 100644 src/Umbraco.Core/DistributedLocking/IDistributedLockingMechanismFactory.cs delete mode 100644 src/Umbraco.Core/Extensions/ConfigConnectionStringExtensions.cs create mode 100644 src/Umbraco.Core/Extensions/ConnectionStringExtensions.cs delete mode 100644 src/Umbraco.Core/Install/Models/DatabaseType.cs delete mode 100644 src/Umbraco.Core/Persistence/Constants-DbProviderNames.cs create mode 100644 src/Umbraco.Infrastructure/DistributedLocking/DefaultDistributedLockingMechanismFactory.cs delete mode 100644 src/Umbraco.Infrastructure/Persistence/BasicBulkSqlInsertProvider.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/CustomConnectionStringDatabaseProviderMetadata.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/IDatabaseProviderMetadata.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/IProviderSpecificInterceptor.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/IScalarMapper.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/ScalarMapper.cs delete mode 100644 src/Umbraco.Infrastructure/Persistence/SqlServerDbProviderFactoryCreator.cs delete mode 100644 src/Umbraco.Persistence.SqlCe/SqlCeBulkSqlInsertProvider.cs delete mode 100644 src/Umbraco.Persistence.SqlCe/SqlCeDatabaseCreator.cs delete mode 100644 src/Umbraco.Persistence.SqlCe/SqlCeImageMapper.cs delete mode 100644 src/Umbraco.Persistence.SqlCe/SqlCeSpecificMapperFactory.cs delete mode 100644 src/Umbraco.Persistence.SqlCe/SqlCeSyntaxProvider.cs delete mode 100644 src/Umbraco.Persistence.SqlCe/Umbraco.Persistence.SqlCe.csproj delete mode 100644 tests/Umbraco.Tests.Integration.SqlCe/Umbraco.Infrastructure/Persistence/DatabaseBuilderTests.cs delete mode 100644 tests/Umbraco.Tests.Integration.SqlCe/Umbraco.Tests.Integration.SqlCe.csproj create mode 100644 tests/Umbraco.Tests.Integration/Testing/SqlServerBaseTestDatabase.cs rename tests/Umbraco.Tests.Integration/Testing/{SqlDeveloperTestDatabase.cs => SqlServerTestDatabase.cs} (75%) create mode 100644 tests/Umbraco.Tests.Integration/Testing/SqliteTestDatabase.cs delete mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SqlServerTableByTableTest.cs create mode 100644 tests/Umbraco.Tests.Integration/appsettings.Tests.json create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Configuration/ConfigureConnectionStringsTests.cs rename tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/{DatabaseContextTests.cs => SqlAzureDatabaseProviderMetadataTests.cs} (72%) delete mode 100644 umbraco-netcore-only.sln diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c1c120d0cf..dfda3a4f94 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -21,7 +21,7 @@ "profileName": "mssql-container" } ], - "omnisharp.defaultLaunchSolution": "umbraco-netcore-only.sln", + "omnisharp.defaultLaunchSolution": "umbraco.sln", "omnisharp.enableDecompilationSupport": true, "omnisharp.enableRoslynAnalyzers": true }, diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 33d3e851c7..add4e13c77 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -28,7 +28,7 @@ jobs: dotnet-version: '6.0.x' - name: dotnet build - run: dotnet build umbraco-netcore-only.sln # also runs npm build + run: dotnet build umbraco.sln -c SkipTests - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 diff --git a/.gitignore b/.gitignore index c69474ac30..59b050f634 100644 --- a/.gitignore +++ b/.gitignore @@ -93,6 +93,7 @@ tests/Umbraco.Tests.AcceptanceTest/cypress/support/chainable.ts tests/Umbraco.Tests.AcceptanceTest/cypress/videos/ tests/Umbraco.Tests.Integration.SqlCe/DatabaseContextTests.sdf tests/Umbraco.Tests.Integration.SqlCe/umbraco/Data/TEMP/ +tests/Umbraco.Tests.Integration/appsettings.Tests.Local.json tests/Umbraco.Tests.Integration/TEMP/* tests/Umbraco.Tests.Integration/umbraco/Data/ tests/Umbraco.Tests.Integration/umbraco/logs/ diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 9d2a5248e8..1d4324a34d 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -42,7 +42,7 @@ "type": "process", "args": [ "build", - "${workspaceFolder}/src/umbraco-netcore-only.sln", + "${workspaceFolder}/src/umbraco.sln", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], diff --git a/build/NuSpecs/UmbracoCms.SqlCe.nuspec b/build/NuSpecs/UmbracoCms.SqlCe.nuspec deleted file mode 100644 index 0a95a02d2a..0000000000 --- a/build/NuSpecs/UmbracoCms.SqlCe.nuspec +++ /dev/null @@ -1,43 +0,0 @@ - - - - Umbraco.Cms.SqlCe - 9.0.0 - Umbraco Cms Sql Ce Add-on - Umbraco HQ - Umbraco HQ - MIT - https://umbraco.com/ - https://umbraco.com/dist/nuget/logo-small.png - false - Contains the SQL CE assemblies needed to run Umbraco Cms. This package only contains assemblies and can be used for package development. Use the UmbracoCms package to setup Umbraco in Visual Studio as an ASP.NET Core project. - Contains the SQL CE assemblies needed to run Umbraco Cms - en-US - umbraco - - - - - - - - - - - - - - - - - - - - - - - diff --git a/build/NuSpecs/UmbracoCms.nuspec b/build/NuSpecs/UmbracoCms.nuspec index 00515660fe..127fd28220 100644 --- a/build/NuSpecs/UmbracoCms.nuspec +++ b/build/NuSpecs/UmbracoCms.nuspec @@ -20,6 +20,8 @@ + + diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Constants.cs b/src/Umbraco.Cms.Persistence.SqlServer/Constants.cs new file mode 100644 index 0000000000..19ec0738c8 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.SqlServer/Constants.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Cms.Persistence.SqlServer; + +/// +/// Constants related to SQLite. +/// +public static class Constants +{ + /// + /// SQLite provider name. + /// + public const string ProviderName = "Microsoft.Data.SqlClient"; +} diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ColumnInSchemaDto.cs b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/ColumnInSchemaDto.cs similarity index 91% rename from src/Umbraco.Infrastructure/Persistence/Dtos/ColumnInSchemaDto.cs rename to src/Umbraco.Cms.Persistence.SqlServer/Dtos/ColumnInSchemaDto.cs index c5c1c158e2..5f574c2607 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ColumnInSchemaDto.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/ColumnInSchemaDto.cs @@ -1,6 +1,6 @@ using NPoco; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Persistence.SqlServer.Dtos { internal class ColumnInSchemaDto { diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ConstraintPerColumnDto.cs b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/ConstraintPerColumnDto.cs similarity index 85% rename from src/Umbraco.Infrastructure/Persistence/Dtos/ConstraintPerColumnDto.cs rename to src/Umbraco.Cms.Persistence.SqlServer/Dtos/ConstraintPerColumnDto.cs index c8a05b41d7..bd8e32899c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ConstraintPerColumnDto.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/ConstraintPerColumnDto.cs @@ -1,6 +1,6 @@ using NPoco; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Persistence.SqlServer.Dtos { internal class ConstraintPerColumnDto { diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ConstraintPerTableDto.cs b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/ConstraintPerTableDto.cs similarity index 81% rename from src/Umbraco.Infrastructure/Persistence/Dtos/ConstraintPerTableDto.cs rename to src/Umbraco.Cms.Persistence.SqlServer/Dtos/ConstraintPerTableDto.cs index c8bbf17114..e1633233cf 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ConstraintPerTableDto.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/ConstraintPerTableDto.cs @@ -1,6 +1,6 @@ using NPoco; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Persistence.SqlServer.Dtos { internal class ConstraintPerTableDto { diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/DefaultConstraintPerColumnDto.cs b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/DefaultConstraintPerColumnDto.cs similarity index 87% rename from src/Umbraco.Infrastructure/Persistence/Dtos/DefaultConstraintPerColumnDto.cs rename to src/Umbraco.Cms.Persistence.SqlServer/Dtos/DefaultConstraintPerColumnDto.cs index 445f38f53c..67011aabdb 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/DefaultConstraintPerColumnDto.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/DefaultConstraintPerColumnDto.cs @@ -1,6 +1,6 @@ using NPoco; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Persistence.SqlServer.Dtos { internal class DefaultConstraintPerColumnDto { diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/DefinedIndexDto.cs b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/DefinedIndexDto.cs similarity index 87% rename from src/Umbraco.Infrastructure/Persistence/Dtos/DefinedIndexDto.cs rename to src/Umbraco.Cms.Persistence.SqlServer/Dtos/DefinedIndexDto.cs index 79a7de2273..231577074d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/DefinedIndexDto.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Dtos/DefinedIndexDto.cs @@ -1,6 +1,6 @@ using NPoco; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Persistence.SqlServer.Dtos { internal class DefinedIndexDto { diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerAddMiniProfilerInterceptor.cs b/src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerAddMiniProfilerInterceptor.cs new file mode 100644 index 0000000000..7c5df6c497 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerAddMiniProfilerInterceptor.cs @@ -0,0 +1,11 @@ +using System.Data.Common; +using NPoco; +using StackExchange.Profiling; + +namespace Umbraco.Cms.Persistence.SqlServer.Interceptors; + +public class SqlServerAddMiniProfilerInterceptor : SqlServerConnectionInterceptor +{ + public override DbConnection OnConnectionOpened(IDatabase database, DbConnection conn) + => new StackExchange.Profiling.Data.ProfiledDbConnection(conn, MiniProfiler.Current); +} diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerAddRetryPolicyInterceptor.cs b/src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerAddRetryPolicyInterceptor.cs new file mode 100644 index 0000000000..bdf5745d42 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerAddRetryPolicyInterceptor.cs @@ -0,0 +1,34 @@ +using System.Data.Common; +using Microsoft.Extensions.Options; +using NPoco; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Infrastructure.Persistence.FaultHandling; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Persistence.SqlServer.Interceptors; + +public class SqlServerAddRetryPolicyInterceptor : SqlServerConnectionInterceptor +{ + private readonly IOptionsMonitor _connectionStrings; + + public SqlServerAddRetryPolicyInterceptor(IOptionsMonitor connectionStrings) + => _connectionStrings = connectionStrings; + + public override DbConnection OnConnectionOpened(IDatabase database, DbConnection conn) + { + if (!_connectionStrings.CurrentValue.IsConnectionStringConfigured()) + { + return conn; + } + + RetryPolicy? connectionRetryPolicy = RetryPolicyFactory.GetDefaultSqlConnectionRetryPolicyByConnectionString(_connectionStrings.CurrentValue.ConnectionString); + RetryPolicy? commandRetryPolicy = RetryPolicyFactory.GetDefaultSqlCommandRetryPolicyByConnectionString(_connectionStrings.CurrentValue.ConnectionString); + + if (connectionRetryPolicy == null && commandRetryPolicy == null) + { + return conn; + } + + return new RetryDbConnection(conn, connectionRetryPolicy, commandRetryPolicy); + } +} diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerConnectionInterceptor.cs b/src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerConnectionInterceptor.cs new file mode 100644 index 0000000000..499a8a05fe --- /dev/null +++ b/src/Umbraco.Cms.Persistence.SqlServer/Interceptors/SqlServerConnectionInterceptor.cs @@ -0,0 +1,16 @@ +using System.Data.Common; +using NPoco; +using Umbraco.Cms.Infrastructure.Persistence; + +namespace Umbraco.Cms.Persistence.SqlServer.Interceptors; + +public abstract class SqlServerConnectionInterceptor : IProviderSpecificConnectionInterceptor +{ + public string ProviderName => Constants.ProviderName; + + public abstract DbConnection OnConnectionOpened(IDatabase database, DbConnection conn); + + public virtual void OnConnectionClosing(IDatabase database, DbConnection conn) + { + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/BulkDataReader.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/BulkDataReader.cs similarity index 99% rename from src/Umbraco.Infrastructure/Persistence/BulkDataReader.cs rename to src/Umbraco.Cms.Persistence.SqlServer/Services/BulkDataReader.cs index 42c3ff1865..2093963bb4 100644 --- a/src/Umbraco.Infrastructure/Persistence/BulkDataReader.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/BulkDataReader.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using System.Collections.ObjectModel; using System.Data; using System.Data.Common; @@ -7,7 +5,7 @@ using System.Diagnostics; using System.Globalization; using Microsoft.Data.SqlClient; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Persistence.SqlServer.Services { /// /// A base implementation of that is suitable for . diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/MicrosoftSqlSyntaxProviderBase.cs similarity index 78% rename from src/Umbraco.Infrastructure/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs rename to src/Umbraco.Cms.Persistence.SqlServer/Services/MicrosoftSqlSyntaxProviderBase.cs index f045f379e4..18e73e88c2 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/MicrosoftSqlSyntaxProviderBase.cs @@ -1,11 +1,12 @@ -using System; using System.Data; -using System.Linq; +using Microsoft.Extensions.Logging; +using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Persistence; -using Umbraco.Cms.Infrastructure.Persistence.Querying; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; +using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax +namespace Umbraco.Cms.Persistence.SqlServer.Services { /// /// Abstract class for defining MS sql implementations @@ -14,8 +15,12 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax public abstract class MicrosoftSqlSyntaxProviderBase : SqlSyntaxProviderBase where TSyntax : ISqlSyntaxProvider { + private readonly ILogger _logger; + protected MicrosoftSqlSyntaxProviderBase() { + _logger = StaticApplicationLogging.CreateLogger(); + AutoIncrementDefinition = "IDENTITY(1,1)"; GuidColumnDefinition = "UniqueIdentifier"; RealColumnDefinition = "FLOAT"; @@ -34,7 +39,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax if (tableName.Contains(".") == false) return $"[{tableName}]"; - var tableNameParts = tableName.Split(Constants.CharArrays.Period, 2); + var tableNameParts = tableName.Split(Cms.Core.Constants.CharArrays.Period, 2); return $"[{tableNameParts[0]}].[{tableNameParts[1]}]"; } @@ -175,5 +180,42 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax } return sqlDbType; } + + public override void HandleCreateTable(IDatabase database, TableDefinition tableDefinition, bool skipKeysAndIndexes = false) + { + var createSql = Format(tableDefinition); + var createPrimaryKeySql = FormatPrimaryKey(tableDefinition); + List foreignSql = Format(tableDefinition.ForeignKeys); + + _logger.LogInformation("Create table:\n {Sql}", createSql); + database.Execute(new Sql(createSql)); + + if (skipKeysAndIndexes) + { + return; + } + + //If any statements exists for the primary key execute them here + if (string.IsNullOrEmpty(createPrimaryKeySql) == false) + { + _logger.LogInformation("Create Primary Key:\n {Sql}", createPrimaryKeySql); + database.Execute(new Sql(createPrimaryKeySql)); + } + + List indexSql = Format(tableDefinition.Indexes); + //Loop through index statements and execute sql + foreach (var sql in indexSql) + { + _logger.LogInformation("Create Index:\n {Sql}", sql); + database.Execute(new Sql(sql)); + } + + //Loop through foreignkey statements and execute sql + foreach (var sql in foreignSql) + { + _logger.LogInformation("Create Foreign Key:\n {Sql}", sql); + database.Execute(new Sql(sql)); + } + } } } diff --git a/src/Umbraco.Infrastructure/Persistence/PocoDataDataReader.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/PocoDataDataReader.cs similarity index 97% rename from src/Umbraco.Infrastructure/Persistence/PocoDataDataReader.cs rename to src/Umbraco.Cms.Persistence.SqlServer/Services/PocoDataDataReader.cs index c3875d3770..088b9de563 100644 --- a/src/Umbraco.Infrastructure/Persistence/PocoDataDataReader.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/PocoDataDataReader.cs @@ -1,14 +1,10 @@ -using System; -using System.Collections.Generic; using System.Data; -using System.Linq; using NPoco; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Persistence.SqlServer.Services { /// /// A data reader used for reading collections of PocoData entity types diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlAzureDatabaseProviderMetadata.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlAzureDatabaseProviderMetadata.cs new file mode 100644 index 0000000000..cf74a8549f --- /dev/null +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlAzureDatabaseProviderMetadata.cs @@ -0,0 +1,101 @@ +using System.Runtime.Serialization; +using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Persistence.SqlServer.Services; + +/// +/// Provider metadata for SQL Azure +/// +[DataContract] +public class SqlAzureDatabaseProviderMetadata : IDatabaseProviderMetadata +{ + /// + public Guid Id => new ("7858e827-8951-4fe0-a7fe-6883011b1f1b"); + + /// + public int SortOrder => 3; + + /// + public string DisplayName => "Azure SQL"; + + /// + public string DefaultDatabaseName => string.Empty; + + /// + public string ProviderName => Constants.ProviderName; + + /// + public bool SupportsQuickInstall => false; + + /// + public bool IsAvailable => true; + + /// + public bool RequiresServer => true; + + /// + public string ServerPlaceholder => "umbraco-database.database.windows.net"; + + /// + public bool RequiresCredentials => true; + + /// + public bool SupportsIntegratedAuthentication => false; + + /// + public bool RequiresConnectionTest => true; + + /// + public bool ForceCreateDatabase => false; + + /// + public string GenerateConnectionString(DatabaseModel databaseModel) + { + var server = databaseModel.Server; + var databaseName = databaseModel.DatabaseName; + var user = databaseModel.Login; + var password = databaseModel.Password; + + if (server.Contains(".") && ServerStartsWithTcp(server) == false) + server = $"tcp:{server}"; + + if (server.Contains(".") == false && ServerStartsWithTcp(server)) + { + string serverName = server.Contains(",") + ? server.Substring(0, server.IndexOf(",", StringComparison.Ordinal)) + : server; + + var portAddition = string.Empty; + + if (server.Contains(",")) + portAddition = server.Substring(server.IndexOf(",", StringComparison.Ordinal)); + + server = $"{serverName}.database.windows.net{portAddition}"; + } + + if (ServerStartsWithTcp(server) == false) + server = $"tcp:{server}.database.windows.net"; + + if (server.Contains(",") == false) + server = $"{server},1433"; + + if (user.Contains("@") == false) + { + var userDomain = server; + + if (ServerStartsWithTcp(server)) + userDomain = userDomain.Substring(userDomain.IndexOf(":", StringComparison.Ordinal) + 1); + + if (userDomain.Contains(".")) + userDomain = userDomain.Substring(0, userDomain.IndexOf(".", StringComparison.Ordinal)); + + user = $"{user}@{userDomain}"; + } + + return $"Server={server};Database={databaseName};User ID={user};Password={password}"; + } + + private static bool ServerStartsWithTcp(string server) => server.InvariantStartsWith("tcp:"); +} diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlLocalDbDatabaseProviderMetadata.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlLocalDbDatabaseProviderMetadata.cs new file mode 100644 index 0000000000..84e784731f --- /dev/null +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlLocalDbDatabaseProviderMetadata.cs @@ -0,0 +1,66 @@ +using System.Runtime.Serialization; +using Microsoft.Data.SqlClient; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Infrastructure.Persistence; + +namespace Umbraco.Cms.Persistence.SqlServer.Services; + +/// +/// Provider metadata for SQL Server LocalDb +/// +[DataContract] +public class SqlLocalDbDatabaseProviderMetadata : IDatabaseProviderMetadata +{ + /// + public Guid Id => new ("05a7e9ed-aa6a-43af-a309-63422c87c675"); + + /// + public int SortOrder => 1; + + /// + public string DisplayName => "SQL Server Express LocalDB"; + + /// + public string DefaultDatabaseName => Core.Constants.System.UmbracoDefaultDatabaseName; + + /// + public string ProviderName => Constants.ProviderName; + + /// + public bool SupportsQuickInstall => true; + + /// + public bool IsAvailable => new LocalDb().IsAvailable; + + /// + public bool RequiresServer => false; + + /// + public string ServerPlaceholder => null; + + /// + public bool RequiresCredentials => false; + + /// + public bool SupportsIntegratedAuthentication => false; + + /// + public bool RequiresConnectionTest => false; + + /// + public bool ForceCreateDatabase => true; + + /// + public string GenerateConnectionString(DatabaseModel databaseModel) + { + var builder = new SqlConnectionStringBuilder + { + DataSource = @"(localdb)\MSSQLLocalDB", + AttachDBFilename = @$"{ConnectionStrings.DataDirectoryPlaceholder}\{databaseModel.DatabaseName}.mdf", + IntegratedSecurity = true + }; + + return builder.ConnectionString; + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/SqlServerBulkSqlInsertProvider.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerBulkSqlInsertProvider.cs similarity index 88% rename from src/Umbraco.Infrastructure/Persistence/SqlServerBulkSqlInsertProvider.cs rename to src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerBulkSqlInsertProvider.cs index ee2689b9e3..cfd30bbd90 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlServerBulkSqlInsertProvider.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerBulkSqlInsertProvider.cs @@ -1,20 +1,17 @@ -using System; -using System.Collections.Generic; using System.Data; -using System.Linq; using Microsoft.Data.SqlClient; using NPoco; -using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; +using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Persistence.SqlServer.Services { /// /// A bulk sql insert provider for Sql Server /// public class SqlServerBulkSqlInsertProvider : IBulkSqlInsertProvider { - public string ProviderName => Cms.Core.Constants.DatabaseProviders.SqlServer; + public string ProviderName => Constants.ProviderName; public int BulkInsertRecords(IUmbracoDatabase database, IEnumerable records) { @@ -24,9 +21,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence var pocoData = database.PocoDataFactory.ForType(typeof(T)); if (pocoData == null) throw new InvalidOperationException("Could not find PocoData for " + typeof(T)); - return database.DatabaseType.IsSqlServer2008OrLater() - ? BulkInsertRecordsSqlServer(database, pocoData, recordsA) - : BasicBulkSqlInsertProvider.BulkInsertRecordsWithCommands(database, recordsA); + return BulkInsertRecordsSqlServer(database, pocoData, recordsA); } /// diff --git a/src/Umbraco.Infrastructure/Persistence/SqlServerDatabaseCreator.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseCreator.cs similarity index 92% rename from src/Umbraco.Infrastructure/Persistence/SqlServerDatabaseCreator.cs rename to src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseCreator.cs index 63aab47047..205519d0b1 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlServerDatabaseCreator.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseCreator.cs @@ -1,13 +1,11 @@ -using System; -using System.IO; using Microsoft.Data.SqlClient; -using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Persistence.SqlServer.Services { public class SqlServerDatabaseCreator : IDatabaseCreator { - public string ProviderName => Constants.DatabaseProviders.SqlServer; + public string ProviderName => Constants.ProviderName; public void Create(string connectionString) { diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseProviderMetadata.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseProviderMetadata.cs new file mode 100644 index 0000000000..8c840f1778 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDatabaseProviderMetadata.cs @@ -0,0 +1,57 @@ +using System.Runtime.Serialization; +using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Infrastructure.Persistence; + +namespace Umbraco.Cms.Persistence.SqlServer.Services; + +/// +/// Provider metadata for SQL Server +/// +[DataContract] +public class SqlServerDatabaseProviderMetadata : IDatabaseProviderMetadata +{ + /// + public Guid Id => new ("5e1ad149-1951-4b74-90bf-2ac2aada9e73"); + + /// + public int SortOrder => 2; + + /// + public string DisplayName => "SQL Server"; + + /// + public string DefaultDatabaseName => string.Empty; + + /// + public string ProviderName => Constants.ProviderName; + + /// + public bool SupportsQuickInstall => false; + + /// + public bool IsAvailable => true; + + /// + public bool RequiresServer => true; + + /// + public string ServerPlaceholder => "(local)\\SQLEXPRESS"; + + /// + public bool RequiresCredentials => true; + + /// + public bool SupportsIntegratedAuthentication => true; + + /// + public bool RequiresConnectionTest => true; + + /// + public bool ForceCreateDatabase => false; + + /// + public string GenerateConnectionString(DatabaseModel databaseModel) => + databaseModel.IntegratedAuth + ? $"Server={databaseModel.Server};Database={databaseModel.DatabaseName};Integrated Security=true" + : $"Server={databaseModel.Server};Database={databaseModel.DatabaseName};User Id={databaseModel.Login};Password={databaseModel.Password}"; +} diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDistributedLockingMechanism.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDistributedLockingMechanism.cs new file mode 100644 index 0000000000..d85ef56e3b --- /dev/null +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDistributedLockingMechanism.cs @@ -0,0 +1,169 @@ +using System.Data; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DistributedLocking; +using Umbraco.Cms.Core.DistributedLocking.Exceptions; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Scoping; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Persistence.SqlServer.Services; + +/// +/// SQL Server implementation of . +/// +public class SqlServerDistributedLockingMechanism : IDistributedLockingMechanism +{ + private readonly ILogger _logger; + private readonly Lazy _scopeAccessor; // Hooray it's a circular dependency. + private readonly IOptionsMonitor _globalSettings; + private readonly IOptionsMonitor _connectionStrings; + + /// + /// Initializes a new instance of the class. + /// + public SqlServerDistributedLockingMechanism( + ILogger logger, + Lazy scopeAccessor, + IOptionsMonitor globalSettings, + IOptionsMonitor connectionStrings) + { + _logger = logger; + _scopeAccessor = scopeAccessor; + _globalSettings = globalSettings; + _connectionStrings = connectionStrings; + } + + /// + public bool Enabled => _connectionStrings.CurrentValue.IsConnectionStringConfigured() && + _connectionStrings.CurrentValue.ProviderName == Constants.ProviderName; + + /// + public IDistributedLock ReadLock(int lockId, TimeSpan? obtainLockTimeout = null) + { + obtainLockTimeout ??= _globalSettings.CurrentValue.DistributedLockingReadLockDefaultTimeout; + return new SqlServerDistributedLock(this, lockId, DistributedLockType.ReadLock, obtainLockTimeout.Value); + } + + /// + public IDistributedLock WriteLock(int lockId, TimeSpan? obtainLockTimeout = null) + { + obtainLockTimeout ??= _globalSettings.CurrentValue.DistributedLockingWriteLockDefaultTimeout; + return new SqlServerDistributedLock(this, lockId, DistributedLockType.WriteLock, obtainLockTimeout.Value); + } + + private class SqlServerDistributedLock : IDistributedLock + { + private readonly SqlServerDistributedLockingMechanism _parent; + private readonly TimeSpan _timeout; + + public SqlServerDistributedLock( + SqlServerDistributedLockingMechanism parent, + int lockId, + DistributedLockType lockType, + TimeSpan timeout) + { + _parent = parent; + _timeout = timeout; + LockId = lockId; + LockType = lockType; + + _parent._logger.LogDebug("Requesting {lockType} for id {id}", LockType, LockId); + + try + { + switch (lockType) + { + case DistributedLockType.ReadLock: + ObtainReadLock(); + break; + case DistributedLockType.WriteLock: + ObtainWriteLock(); + break; + default: + throw new ArgumentOutOfRangeException(nameof(lockType), lockType, @"Unsupported lockType"); + } + } + catch (SqlException ex) when (ex.Number == 1222) + { + if (LockType == DistributedLockType.ReadLock) + { + throw new DistributedReadLockTimeoutException(LockId); + } + + throw new DistributedWriteLockTimeoutException(LockId); + } + + _parent._logger.LogDebug("Acquired {lockType} for id {id}", LockType, LockId); + } + + public int LockId { get; } + + public DistributedLockType LockType { get; } + + public void Dispose() + { + // Mostly no op, cleaned up by completing transaction in scope. + _parent._logger.LogDebug("Dropped {lockType} for id {id}", LockType, LockId); + } + + public override string ToString() + => $"SqlServerDistributedLock({LockId}, {LockType}"; + + private void ObtainReadLock() + { + IUmbracoDatabase db = _parent._scopeAccessor.Value.AmbientScope.Database; + + if (!db.InTransaction) + { + throw new InvalidOperationException("SqlServerDistributedLockingMechanism requires a transaction to function."); + } + + if (db.Transaction.IsolationLevel < IsolationLevel.ReadCommitted) + { + throw new InvalidOperationException("A transaction with minimum ReadCommitted isolation level is required."); + } + + const string query = "SELECT value FROM umbracoLock WITH (REPEATABLEREAD) WHERE id=@id"; + + db.Execute("SET LOCK_TIMEOUT " + _timeout.TotalMilliseconds + ";"); + + var i = db.ExecuteScalar(query, new {id = LockId}); + + if (i == null) + { + // ensure we are actually locking! + throw new ArgumentException(@$"LockObject with id={LockId} does not exist.", nameof(LockId)); + } + } + + private void ObtainWriteLock() + { + IUmbracoDatabase db = _parent._scopeAccessor.Value.AmbientScope.Database; + + if (!db.InTransaction) + { + throw new InvalidOperationException("SqlServerDistributedLockingMechanism requires a transaction to function."); + } + + if (db.Transaction.IsolationLevel < IsolationLevel.ReadCommitted) + { + throw new InvalidOperationException("A transaction with minimum ReadCommitted isolation level is required."); + } + + const string query = @"UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id"; + + db.Execute("SET LOCK_TIMEOUT " + _timeout.TotalMilliseconds + ";"); + + var i = db.Execute(query, new {id = LockId}); + + if (i == 0) + { + // ensure we are actually locking! + throw new ArgumentException($"LockObject with id={LockId} does not exist."); + } + } + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerSyntaxProvider.cs similarity index 76% rename from src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs rename to src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerSyntaxProvider.cs index 2db603ad1a..763f52e9be 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerSyntaxProvider.cs @@ -1,18 +1,17 @@ -using System; -using System.Collections.Generic; using System.Data; -using System.Linq; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Persistence.SqlServer.Dtos; using Umbraco.Extensions; +using ColumnInfo = Umbraco.Cms.Infrastructure.Persistence.SqlSyntax.ColumnInfo; -namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax +namespace Umbraco.Cms.Persistence.SqlServer.Services { /// /// Represents an SqlSyntaxProvider for Sql Server. @@ -20,15 +19,22 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax public class SqlServerSyntaxProvider : MicrosoftSqlSyntaxProviderBase { private readonly IOptions _globalSettings; + private readonly ILogger _logger; public SqlServerSyntaxProvider(IOptions globalSettings) + : this(globalSettings, StaticApplicationLogging.CreateLogger()) { - _globalSettings = globalSettings; } - public override string ProviderName => Cms.Core.Constants.DatabaseProviders.SqlServer; + public SqlServerSyntaxProvider(IOptions globalSettings, ILogger logger) + { + _globalSettings = globalSettings; + _logger = logger; + } - public ServerVersionInfo ServerVersion { get; private set; } + public override string ProviderName => Constants.ProviderName; + + public ServerVersionInfo? ServerVersion { get; private set; } public enum VersionName { @@ -56,6 +62,22 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax Azure = 5 } + public override DatabaseType GetUpdatedDatabaseType(DatabaseType current, string connectionString) + { + var setting = _globalSettings.Value.DatabaseFactoryServerVersion; + var fromSettings = false; + + if (setting.IsNullOrWhiteSpace() || !setting.StartsWith("SqlServer.") + || !Enum.TryParse(setting.Substring("SqlServer.".Length), out var versionName, true)) + { + versionName = GetSetVersion(connectionString, ProviderName, _logger).ProductVersionName; + } + + _logger.LogDebug("SqlServer {SqlServerVersion}, DatabaseType is {DatabaseType} ({Source}).", versionName, DatabaseType.SqlServer2012, fromSettings ? "settings" : "detected"); + + return DatabaseType.SqlServer2012; + } + public class ServerVersionInfo { public ServerVersionInfo() @@ -87,7 +109,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax private static VersionName MapProductVersion(string productVersion) { - var firstPart = string.IsNullOrWhiteSpace(productVersion) ? "??" : productVersion.Split(Constants.CharArrays.Period)[0]; + var firstPart = string.IsNullOrWhiteSpace(productVersion) ? "??" : productVersion.Split(Cms.Core.Constants.CharArrays.Period)[0]; switch (firstPart) { case "??": @@ -266,107 +288,6 @@ where tbl.[name]=@0 and col.[name]=@1;", tableName, columnName) return result > 0; } - public override void WriteLock(IDatabase db, TimeSpan timeout, int lockId) - { - // soon as we get Database, a transaction is started - - if (db.Transaction.IsolationLevel < IsolationLevel.ReadCommitted) - throw new InvalidOperationException("A transaction with minimum ReadCommitted isolation level is required."); - - ObtainWriteLock(db, timeout, lockId); - } - - public override void WriteLock(IDatabase db, params int[] lockIds) - { - WriteLock(db, _globalSettings.Value.SqlWriteLockTimeOut, lockIds); - } - - public void WriteLock(IDatabase db, TimeSpan timeout, params int[] lockIds) - { - if (db is null) - { - throw new ArgumentNullException(nameof(db)); - } - - if (db.Transaction is null) - { - throw new ArgumentException(nameof(db) + "." + nameof(db.Transaction) + " is null"); - } - - // soon as we get Database, a transaction is started - - if (db.Transaction.IsolationLevel < IsolationLevel.ReadCommitted) - { - throw new InvalidOperationException("A transaction with minimum ReadCommitted isolation level is required."); - } - - foreach (var lockId in lockIds) - { - ObtainWriteLock(db, timeout, lockId); - } - } - - private static void ObtainWriteLock(IDatabase db, TimeSpan timeout, int lockId) - { - db.Execute("SET LOCK_TIMEOUT " + timeout.TotalMilliseconds + ";"); - var i = db.Execute( - @"UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id", - new {id = lockId}); - if (i == 0) // ensure we are actually locking! - { - throw new ArgumentException($"LockObject with id={lockId} does not exist."); - } - } - - public override void ReadLock(IDatabase db, TimeSpan timeout, int lockId) - { - // soon as we get Database, a transaction is started - - if (db.Transaction.IsolationLevel < IsolationLevel.ReadCommitted) - throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required."); - - ObtainReadLock(db, timeout, lockId); - } - - public override void ReadLock(IDatabase db, params int[] lockIds) - { - if (db is null) - { - throw new ArgumentNullException(nameof(db)); - } - - if (db.Transaction is null) - { - throw new ArgumentException(nameof(db) + "." + nameof(db.Transaction) + " is null"); - } - - // soon as we get Database, a transaction is started - - if (db.Transaction.IsolationLevel < IsolationLevel.ReadCommitted) - { - throw new InvalidOperationException("A transaction with minimum ReadCommitted isolation level is required."); - } - - foreach (var lockId in lockIds) - { - ObtainReadLock(db, null, lockId); - } - } - - private static void ObtainReadLock(IDatabase db, TimeSpan? timeout, int lockId) - { - if (timeout.HasValue) - { - db.Execute(@"SET LOCK_TIMEOUT " + timeout.Value.TotalMilliseconds + ";"); - } - - var i = db.ExecuteScalar("SELECT value FROM umbracoLock WITH (REPEATABLEREAD) WHERE id=@id", new {id = lockId}); - if (i == null) // ensure we are actually locking! - { - throw new ArgumentException($"LockObject with id={lockId} does not exist.", nameof(lockId)); - } - } - public override string FormatColumnRename(string tableName, string oldName, string newName) { return string.Format(RenameColumn, tableName, oldName, newName); @@ -433,5 +354,90 @@ where tbl.[name]=@0 and col.[name]=@1;", tableName, columnName) return string.Format(CreateIndex, GetIndexType(index.IndexType), " ", GetQuotedName(name), GetQuotedTableName(index.TableName), columns, includeColumns); } + + + public override Sql InsertForUpdateHint(Sql sql) + { + // go find the first FROM clause, and append the lock hint + Sql s = sql; + var updated = false; + + while (s != null) + { + var sqlText = SqlInspector.GetSqlText(s); + if (sqlText.StartsWith("FROM ", StringComparison.OrdinalIgnoreCase)) + { + SqlInspector.SetSqlText(s, sqlText + " WITH (UPDLOCK)"); + updated = true; + break; + } + + s = SqlInspector.GetSqlRhs(sql); + } + + if (updated) + SqlInspector.Reset(sql); + + return sql; + } + + public override Sql AppendForUpdateHint(Sql sql) + => sql.Append(" WITH (UPDLOCK) "); + + public override Sql.SqlJoinClause LeftJoinWithNestedJoin( + Sql sql, + Func, + Sql> nestedJoin, + string? alias = null) + { + Type type = typeof(TDto); + + var tableName = GetQuotedTableName(type.GetTableName()); + var join = tableName; + + if (alias != null) + { + var quotedAlias = GetQuotedTableName(alias); + join += " " + quotedAlias; + } + + var nestedSql = new Sql(sql.SqlContext); + nestedSql = nestedJoin(nestedSql); + + Sql.SqlJoinClause sqlJoin = sql.LeftJoin(join); + sql.Append(nestedSql); + return sqlJoin; + } + + #region Sql Inspection + + private static SqlInspectionUtilities _sqlInspector; + + private static SqlInspectionUtilities SqlInspector => _sqlInspector ?? (_sqlInspector = new SqlInspectionUtilities()); + + private class SqlInspectionUtilities + { + private readonly Func _getSqlText; + private readonly Action _setSqlText; + private readonly Func _getSqlRhs; + private readonly Action _setSqlFinal; + + public SqlInspectionUtilities() + { + (_getSqlText, _setSqlText) = ReflectionUtilities.EmitFieldGetterAndSetter("_sql"); + _getSqlRhs = ReflectionUtilities.EmitFieldGetter("_rhs"); + _setSqlFinal = ReflectionUtilities.EmitFieldSetter("_sqlFinal"); + } + + public string GetSqlText(Sql sql) => _getSqlText(sql); + + public void SetSqlText(Sql sql, string sqlText) => _setSqlText(sql, sqlText); + + public Sql GetSqlRhs(Sql sql) => _getSqlRhs(sql); + + public void Reset(Sql sql) => _setSqlFinal(sql, null); + } + + #endregion } } diff --git a/src/Umbraco.Cms.Persistence.SqlServer/SqlServerComposer.cs b/src/Umbraco.Cms.Persistence.SqlServer/SqlServerComposer.cs new file mode 100644 index 0000000000..60d64c09df --- /dev/null +++ b/src/Umbraco.Cms.Persistence.SqlServer/SqlServerComposer.cs @@ -0,0 +1,14 @@ +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.DependencyInjection; + +namespace Umbraco.Cms.Persistence.SqlServer; + +/// +/// Automatically adds SQL Server support to Umbraco when this project is referenced. +/// +public class SqlServerComposer : IComposer +{ + /// + public void Compose(IUmbracoBuilder builder) + => builder.AddUmbracoSqlServerSupport(); +} diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Umbraco.Cms.Persistence.SqlServer.csproj b/src/Umbraco.Cms.Persistence.SqlServer/Umbraco.Cms.Persistence.SqlServer.csproj new file mode 100644 index 0000000000..d73e5293f7 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.SqlServer/Umbraco.Cms.Persistence.SqlServer.csproj @@ -0,0 +1,26 @@ + + + + net6.0 + enable + enable + + Umbraco.Cms.Persistence.SqlServer + Umbraco.Cms.Persistence.SqlServer + Adds support for SQL Server to Umbraco CMS. + + + + + + + + + <_Parameter1>Umbraco.Tests.Integration + + + <_Parameter1>Umbraco.Tests.UnitTests + + + + diff --git a/src/Umbraco.Cms.Persistence.SqlServer/UmbracoBuilderExtensions.cs b/src/Umbraco.Cms.Persistence.SqlServer/UmbracoBuilderExtensions.cs new file mode 100644 index 0000000000..5e4e68a0dc --- /dev/null +++ b/src/Umbraco.Cms.Persistence.SqlServer/UmbracoBuilderExtensions.cs @@ -0,0 +1,42 @@ +using System.Data.Common; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.DistributedLocking; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; +using Umbraco.Cms.Persistence.SqlServer.Interceptors; +using Umbraco.Cms.Persistence.SqlServer.Services; + +namespace Umbraco.Cms.Persistence.SqlServer; + +/// +/// SQLite support extensions for IUmbracoBuilder. +/// +public static class UmbracoBuilderExtensions +{ + /// + /// Add required services for SQL Server support. + /// + public static IUmbracoBuilder AddUmbracoSqlServerSupport(this IUmbracoBuilder builder) + { + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + + DbProviderFactories.UnregisterFactory(Constants.ProviderName); + DbProviderFactories.RegisterFactory(Constants.ProviderName, SqlClientFactory.Instance); + + return builder; + } +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Constants.cs b/src/Umbraco.Cms.Persistence.Sqlite/Constants.cs new file mode 100644 index 0000000000..76e408423c --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Constants.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Cms.Persistence.Sqlite; + +/// +/// Constants related to SQLite. +/// +public static class Constants +{ + /// + /// SQLite provider name. + /// + public const string ProviderName = "Microsoft.Data.SQLite"; +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddMiniProfilerInterceptor.cs b/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddMiniProfilerInterceptor.cs new file mode 100644 index 0000000000..eb76319040 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddMiniProfilerInterceptor.cs @@ -0,0 +1,11 @@ +using System.Data.Common; +using NPoco; +using StackExchange.Profiling; + +namespace Umbraco.Cms.Persistence.Sqlite.Interceptors; + +public class SqliteAddMiniProfilerInterceptor : SqliteConnectionInterceptor +{ + public override DbConnection OnConnectionOpened(IDatabase database, DbConnection conn) + => new StackExchange.Profiling.Data.ProfiledDbConnection(conn, MiniProfiler.Current); +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddPreferDeferredInterceptor.cs b/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddPreferDeferredInterceptor.cs new file mode 100644 index 0000000000..ef22e9c0b6 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddPreferDeferredInterceptor.cs @@ -0,0 +1,12 @@ +using System.Data.Common; +using Microsoft.Data.Sqlite; +using NPoco; +using Umbraco.Cms.Persistence.Sqlite.Services; + +namespace Umbraco.Cms.Persistence.Sqlite.Interceptors; + +public class SqliteAddPreferDeferredInterceptor : SqliteConnectionInterceptor +{ + public override DbConnection OnConnectionOpened(IDatabase database, DbConnection conn) + => new SqlitePreferDeferredTransactionsConnection(conn as SqliteConnection ?? throw new InvalidOperationException()); +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddRetryPolicyInterceptor.cs b/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddRetryPolicyInterceptor.cs new file mode 100644 index 0000000000..8010b8b696 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteAddRetryPolicyInterceptor.cs @@ -0,0 +1,16 @@ +using System.Data.Common; +using NPoco; +using Umbraco.Cms.Infrastructure.Persistence.FaultHandling; +using Umbraco.Cms.Persistence.Sqlite.Services; + +namespace Umbraco.Cms.Persistence.Sqlite.Interceptors; + +public class SqliteAddRetryPolicyInterceptor : SqliteConnectionInterceptor +{ + public override DbConnection OnConnectionOpened(IDatabase database, DbConnection conn) + { + RetryStrategy retryStrategy = RetryStrategy.DefaultExponential; + var commandRetryPolicy = new RetryPolicy(new SqliteTransientErrorDetectionStrategy(), retryStrategy); + return new RetryDbConnection(conn, null, commandRetryPolicy); + } +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteConnectionInterceptor.cs b/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteConnectionInterceptor.cs new file mode 100644 index 0000000000..b74533d2f6 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Interceptors/SqliteConnectionInterceptor.cs @@ -0,0 +1,16 @@ +using System.Data.Common; +using NPoco; +using Umbraco.Cms.Infrastructure.Persistence; + +namespace Umbraco.Cms.Persistence.Sqlite.Interceptors; + +public abstract class SqliteConnectionInterceptor : IProviderSpecificConnectionInterceptor +{ + public string ProviderName => Constants.ProviderName; + + public abstract DbConnection OnConnectionOpened(IDatabase database, DbConnection conn); + + public virtual void OnConnectionClosing(IDatabase database, DbConnection conn) + { + } +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqliteGuidScalarMapper.cs b/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqliteGuidScalarMapper.cs new file mode 100644 index 0000000000..bd9bb1924d --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqliteGuidScalarMapper.cs @@ -0,0 +1,24 @@ +using Umbraco.Cms.Infrastructure.Persistence; + +namespace Umbraco.Cms.Persistence.Sqlite.Mappers; + +public class SqliteGuidScalarMapper : ScalarMapper +{ + protected override Guid Map(object value) + => Guid.Parse($"{value}"); +} + +public class SqliteNullableGuidScalarMapper : ScalarMapper +{ + protected override Guid? Map(object? value) + { + if (value is null || value == DBNull.Value) + { + return default(Guid?); + } + + return Guid.TryParse($"{value}", out Guid result) + ? result + : default(Guid?); + } +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoGuidMapper.cs b/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoGuidMapper.cs new file mode 100644 index 0000000000..f7b2836f1a --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Mappers/SqlitePocoGuidMapper.cs @@ -0,0 +1,33 @@ +using NPoco; + +namespace Umbraco.Cms.Persistence.Sqlite.Mappers; + +public class SqlitePocoGuidMapper : DefaultMapper +{ + public override Func GetFromDbConverter(Type destType, Type sourceType) + { + if (destType == typeof(Guid)) + { + return (value) => + { + var result = Guid.Parse($"{value}"); + return result; + }; + } + + if (destType == typeof(Guid?)) + { + return (value) => + { + if (Guid.TryParse($"{value}", out Guid result)) + { + return result; + } + + return default(Guid?); + }; + } + + return base.GetFromDbConverter(destType, sourceType); + } +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteBulkSqlInsertProvider.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteBulkSqlInsertProvider.cs new file mode 100644 index 0000000000..895ee21ef6 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteBulkSqlInsertProvider.cs @@ -0,0 +1,55 @@ +using NPoco; +using Umbraco.Cms.Infrastructure.Persistence; + +namespace Umbraco.Cms.Persistence.Sqlite.Services; + +/// +/// Implements for SQLite. +/// +public class SqliteBulkSqlInsertProvider : IBulkSqlInsertProvider +{ + public string ProviderName => Constants.ProviderName; + + public int BulkInsertRecords(IUmbracoDatabase database, IEnumerable records) + { + var recordsA = records.ToArray(); + if (recordsA.Length == 0) return 0; + + var pocoData = database.PocoDataFactory.ForType(typeof(T)); + if (pocoData == null) throw new InvalidOperationException("Could not find PocoData for " + typeof(T)); + + return BulkInsertRecordsSqlite(database, pocoData, recordsA); + } + + /// + /// Bulk-insert records using SqlServer BulkCopy method. + /// + /// The type of the records. + /// The database. + /// The PocoData object corresponding to the record's type. + /// The records. + /// The number of records that were inserted. + private int BulkInsertRecordsSqlite(IUmbracoDatabase database, PocoData pocoData, IEnumerable records) + { + var count = 0; + var inTrans = database.InTransaction; + + if (!inTrans) + { + database.BeginTransaction(); + } + + foreach (var record in records) + { + database.Insert(record); + count++; + } + + if (!inTrans) + { + database.CompleteTransaction(); + } + + return count; + } +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseCreator.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseCreator.cs new file mode 100644 index 0000000000..43980b3b77 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseCreator.cs @@ -0,0 +1,48 @@ +using System.Diagnostics; +using Microsoft.Data.Sqlite; +using Umbraco.Cms.Infrastructure.Persistence; + +namespace Umbraco.Cms.Persistence.Sqlite.Services; + +/// +/// Implements for SQLite. +/// +public class SqliteDatabaseCreator : IDatabaseCreator +{ + /// + public string ProviderName => Constants.ProviderName; + + /// + /// Creates a SQLite database file. + /// + /// + /// + /// With journal_mode = wal we have snapshot isolation. + /// + /// + /// Concurrent read/write can take occur, committing a write transaction will have no impact + /// on open read transactions as they see only committed data from the point in time that they began reading. + /// + /// + /// A write transaction still requires exclusive access to database files so concurrent writes are not possible. + /// + /// + /// Read more Isolation in SQLite
+ /// Read more Write-Ahead Logging + ///
+ ///
+ public void Create(string connectionString) + { + using var connection = new SqliteConnection(connectionString); + connection.Open(); + + using SqliteCommand command = connection.CreateCommand(); + command.CommandText = "PRAGMA journal_mode = wal;"; + command.ExecuteNonQuery(); + + command.CommandText = "PRAGMA journal_mode"; + var mode = command.ExecuteScalar(); + + Debug.Assert(mode as string == "wal", "incorrect journal_mode"); + } +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseProviderMetadata.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseProviderMetadata.cs new file mode 100644 index 0000000000..07e6db8b8f --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDatabaseProviderMetadata.cs @@ -0,0 +1,72 @@ +using System.Runtime.Serialization; +using Microsoft.Data.Sqlite; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Infrastructure.Persistence; + +namespace Umbraco.Cms.Persistence.Sqlite.Services; + +[DataContract] +public class SqliteDatabaseProviderMetadata : IDatabaseProviderMetadata +{ + /// + public Guid Id => new ("530386a2-b219-4d5f-b68c-b965e14c9ac9"); + + /// + public int SortOrder => -1; + + /// + public string DisplayName => "SQLite"; + + /// + public string DefaultDatabaseName => Core.Constants.System.UmbracoDefaultDatabaseName; + + /// + public string ProviderName => Constants.ProviderName; + + /// + public bool SupportsQuickInstall => true; + + /// + public bool IsAvailable => true; + + /// + public bool RequiresServer => false; + + /// + public string ServerPlaceholder => null; + + /// + public bool RequiresCredentials => false; + + /// + public bool SupportsIntegratedAuthentication => false; + + /// + public bool RequiresConnectionTest => false; + + /// + /// + /// + /// Required to ensure database creator is used regardless of configured InstallMissingDatabase value. + /// + /// + /// Ensures database setup with journal_mode = wal; + /// + /// + public bool ForceCreateDatabase => true; + + /// + public string GenerateConnectionString(DatabaseModel databaseModel) + { + var builder = new SqliteConnectionStringBuilder + { + DataSource = $"{ConnectionStrings.DataDirectoryPlaceholder}/{databaseModel.DatabaseName}.sqlite.db", + ForeignKeys = true, + Pooling = true, + Cache = SqliteCacheMode.Shared, + }; + + return builder.ConnectionString; + } +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDistributedLockingMechanism.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDistributedLockingMechanism.cs new file mode 100644 index 0000000000..c8c34603ab --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDistributedLockingMechanism.cs @@ -0,0 +1,157 @@ +using System.Data; +using System.Data.Common; +using Microsoft.Data.SqlClient; +using Microsoft.Data.Sqlite; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DistributedLocking; +using Umbraco.Cms.Core.DistributedLocking.Exceptions; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Scoping; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Persistence.Sqlite.Services; + +public class SqliteDistributedLockingMechanism : IDistributedLockingMechanism +{ + private readonly ILogger _logger; + private readonly Lazy _scopeAccessor; + private readonly IOptionsMonitor _connectionStrings; + private readonly IOptionsMonitor _globalSettings; + + public SqliteDistributedLockingMechanism( + ILogger logger, + Lazy scopeAccessor, + IOptionsMonitor globalSettings, + IOptionsMonitor connectionStrings) + { + _logger = logger; + _scopeAccessor = scopeAccessor; + _connectionStrings = connectionStrings; + _globalSettings = globalSettings; + } + + /// + public bool Enabled => _connectionStrings.CurrentValue.IsConnectionStringConfigured() && + _connectionStrings.CurrentValue.ProviderName == Constants.ProviderName; + + // With journal_mode=wal we can always read a snapshot. + public IDistributedLock ReadLock(int lockId, TimeSpan? obtainLockTimeout = null) + { + obtainLockTimeout ??= _globalSettings.CurrentValue.DistributedLockingReadLockDefaultTimeout; + return new SqliteDistributedLock(this, lockId, DistributedLockType.ReadLock, obtainLockTimeout.Value); + } + + // With journal_mode=wal only a single write transaction can exist at a time. + public IDistributedLock WriteLock(int lockId, TimeSpan? obtainLockTimeout = null) + { + obtainLockTimeout ??= _globalSettings.CurrentValue.DistributedLockingWriteLockDefaultTimeout; + return new SqliteDistributedLock(this, lockId, DistributedLockType.WriteLock, obtainLockTimeout.Value); + } + + private class SqliteDistributedLock : IDistributedLock + { + private readonly SqliteDistributedLockingMechanism _parent; + private readonly TimeSpan _timeout; + + public SqliteDistributedLock( + SqliteDistributedLockingMechanism parent, + int lockId, + DistributedLockType lockType, + TimeSpan timeout) + { + _parent = parent; + _timeout = timeout; + LockId = lockId; + LockType = lockType; + + _parent._logger.LogDebug("Requesting {lockType} for id {id}", LockType, LockId); + + try + { + switch (lockType) + { + case DistributedLockType.ReadLock: + ObtainReadLock(); + break; + case DistributedLockType.WriteLock: + ObtainWriteLock(); + break; + default: + throw new ArgumentOutOfRangeException(nameof(lockType), lockType, @"Unsupported lockType"); + } + } + catch (SqlException ex) when (ex.Number == 1222) + { + if (LockType == DistributedLockType.ReadLock) + { + throw new DistributedReadLockTimeoutException(LockId); + } + + throw new DistributedWriteLockTimeoutException(LockId); + } + + _parent._logger.LogDebug("Acquired {lockType} for id {id}", LockType, LockId); + } + + public int LockId { get; } + + public DistributedLockType LockType { get; } + + public void Dispose() + { + // Mostly no op, cleaned up by completing transaction in scope. + _parent._logger.LogDebug("Dropped {lockType} for id {id}", LockType, LockId); + } + + public override string ToString() + => $"SqliteDistributedLock({LockId})"; + + // Can always obtain a read lock (snapshot isolation in wal mode) + // Mostly no-op just check that we didn't end up ReadUncommitted for real. + private void ObtainReadLock() + { + IUmbracoDatabase db = _parent._scopeAccessor.Value.AmbientScope.Database; + + if (!db.InTransaction) + { + throw new InvalidOperationException("SqliteDistributedLockingMechanism requires a transaction to function."); + } + } + + // Only one writer is possible at a time + // lock occurs for entire database as opposed to row/table. + private void ObtainWriteLock() + { + IUmbracoDatabase db = _parent._scopeAccessor.Value.AmbientScope.Database; + + if (!db.InTransaction) + { + throw new InvalidOperationException("SqliteDistributedLockingMechanism requires a transaction to function."); + } + + var query = @$"UPDATE umbracoLock SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id = {LockId}"; + + DbCommand command = db.CreateCommand(db.Connection, CommandType.Text, query); + + // imagine there is an existing writer, whilst elapsed time is < command timeout sqlite will busy loop + command.CommandTimeout = _timeout.Seconds; + + try + { + var i = command.ExecuteNonQuery(); + + if (i == 0) + { + // ensure we are actually locking! + throw new ArgumentException($"LockObject with id={LockId} does not exist."); + } + } + catch (SqliteException ex) when (ex.IsBusyOrLocked()) + { + throw new DistributedWriteLockTimeoutException(LockId); + } + } + } +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteExceptionExtensions.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteExceptionExtensions.cs new file mode 100644 index 0000000000..4076718266 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteExceptionExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.Data.Sqlite; + +namespace Umbraco.Cms.Persistence.Sqlite.Services; + +public static class SqliteExceptionExtensions +{ + public static bool IsBusyOrLocked(this SqliteException ex) => + ex.SqliteErrorCode + is SQLitePCL.raw.SQLITE_BUSY + or SQLitePCL.raw.SQLITE_LOCKED + or SQLitePCL.raw.SQLITE_LOCKED_SHAREDCACHE; +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqlitePreferDeferredTransactionsConnection.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqlitePreferDeferredTransactionsConnection.cs new file mode 100644 index 0000000000..e4cee692a2 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqlitePreferDeferredTransactionsConnection.cs @@ -0,0 +1,124 @@ +using System.Data; +using System.Data.Common; +using Microsoft.Data.Sqlite; + +namespace Umbraco.Cms.Persistence.Sqlite.Services; + +public class SqlitePreferDeferredTransactionsConnection : DbConnection +{ + private readonly SqliteConnection _inner; + + public SqlitePreferDeferredTransactionsConnection(SqliteConnection inner) + { + _inner = inner; + } + + protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) + => _inner.BeginTransaction(isolationLevel, deferred: true); // <-- The important bit + + public override void ChangeDatabase(string databaseName) + => _inner.ChangeDatabase(databaseName); + + public override void Close() + => _inner.Close(); + + public override void Open() + => _inner.Open(); + + public override string Database + => _inner.Database; + + public override ConnectionState State + => _inner.State; + + public override string DataSource + => _inner.DataSource; + + public override string ServerVersion + => _inner.ServerVersion; + + protected override DbCommand CreateDbCommand() + => new CommandWrapper(_inner.CreateCommand()); + + public override string ConnectionString + { + get => _inner.ConnectionString; + set => _inner.ConnectionString = value; + } + + private class CommandWrapper : DbCommand + { + private readonly DbCommand _inner; + + public CommandWrapper(DbCommand inner) + { + _inner = inner; + } + + public override void Cancel() + => _inner.Cancel(); + + public override int ExecuteNonQuery() + => _inner.ExecuteNonQuery(); + + public override object? ExecuteScalar() + => _inner.ExecuteScalar(); + + public override void Prepare() + => _inner.Prepare(); + + public override string CommandText + { + get => _inner.CommandText; + set => _inner.CommandText = value; + } + + public override int CommandTimeout + { + get => _inner.CommandTimeout; + set => _inner.CommandTimeout = value; + } + + public override CommandType CommandType + { + get => _inner.CommandType; + set => _inner.CommandType = value; + } + + public override UpdateRowSource UpdatedRowSource + { + get => _inner.UpdatedRowSource; + set => _inner.UpdatedRowSource = value; + } + + protected override DbConnection? DbConnection + { + get => _inner.Connection; + set + { + _inner.Connection = (value as SqlitePreferDeferredTransactionsConnection)?._inner; + } + } + + protected override DbParameterCollection DbParameterCollection + => _inner.Parameters; + + protected override DbTransaction? DbTransaction + { + get => _inner.Transaction; + set => _inner.Transaction = value; + } + + public override bool DesignTimeVisible + { + get => _inner.DesignTimeVisible; + set => _inner.DesignTimeVisible = value; + } + + protected override DbParameter CreateDbParameter() + => _inner.CreateParameter(); + + protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) + => _inner.ExecuteReader(behavior); + } +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSpecificMapperFactory.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSpecificMapperFactory.cs new file mode 100644 index 0000000000..cf1d707d69 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSpecificMapperFactory.cs @@ -0,0 +1,16 @@ +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Persistence.Sqlite.Mappers; + +namespace Umbraco.Cms.Persistence.Sqlite.Services; + +/// +/// Implements for SQLite. +/// +public class SqliteSpecificMapperFactory : IProviderSpecificMapperFactory +{ + /// + public string ProviderName => Constants.ProviderName; + + /// + public NPocoMapperCollection Mappers => new NPocoMapperCollection(() => new[] { new SqlitePocoGuidMapper() }); +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs new file mode 100644 index 0000000000..cc9d1ff279 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs @@ -0,0 +1,424 @@ +using System.Data; +using System.Data.Common; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; +using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; +using Umbraco.Cms.Persistence.Sqlite.Mappers; +using Umbraco.Extensions; +using ColumnInfo = Umbraco.Cms.Infrastructure.Persistence.SqlSyntax.ColumnInfo; + +namespace Umbraco.Cms.Persistence.Sqlite.Services; + +/// +/// Implements for SQLite. +/// +public class SqliteSyntaxProvider : SqlSyntaxProviderBase +{ + private readonly IOptions _globalSettings; + private readonly ILogger _log; + private readonly IDictionary _scalarMappers; + + public SqliteSyntaxProvider(IOptions globalSettings, ILogger log) + { + _globalSettings = globalSettings; + _log = log; + + _scalarMappers = new Dictionary + { + [typeof(Guid)] = new SqliteGuidScalarMapper(), + [typeof(Guid?)] = new SqliteNullableGuidScalarMapper(), + }; + } + + /// + public override string ProviderName => Constants.ProviderName; + + public override string StringColumnDefinition => "TEXT COLLATE NOCASE"; + + public override string StringLengthUnicodeColumnDefinitionFormat => "TEXT COLLATE NOCASE"; + + /// + public override IsolationLevel DefaultIsolationLevel + => IsolationLevel.Serializable; + + /// + public override string DbProvider => Constants.ProviderName; + + + /// + public override bool SupportsIdentityInsert() => false; + + /// + public override bool SupportsClustered() => false; + + + public override string GetIndexType(IndexTypes indexTypes) + { + switch (indexTypes) + { + case IndexTypes.UniqueNonClustered: + return "UNIQUE"; + default: + return string.Empty; + } + } + + public override List Format(IEnumerable foreignKeys) + { + return 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; + + var localColumn = GetQuotedColumnName(foreignKey.ForeignColumns.First()); + var remoteColumn = GetQuotedColumnName(foreignKey.PrimaryColumns.First()); + var remoteTable = GetQuotedTableName(foreignKey.PrimaryTable); + var onDelete = FormatCascade("DELETE", foreignKey.OnDelete); + var onUpdate = FormatCascade("UPDATE", foreignKey.OnUpdate); + + return + $"CONSTRAINT {constraintName} FOREIGN KEY ({localColumn}) REFERENCES {remoteTable} ({remoteColumn}) {onDelete} {onUpdate}"; + } + + /// + public override IEnumerable> GetDefinedIndexes(IDatabase db) + { + List items = db.Fetch( + @"SELECT + m.tbl_name AS tableName, + ilist.name AS indexName, + iinfo.name AS columnName, + ilist.[unique] AS isUnique + FROM + sqlite_master AS m, + pragma_index_list(m.name) AS ilist, + pragma_index_info(ilist.name) AS iinfo"); + + return items + .Where(x => !x.IndexName.StartsWith("sqlite_")) + .Select(item => + new Tuple(item.TableName, item.IndexName, item.ColumnName, item.IsUnique)) + .ToList(); + } + + + public override string ConvertIntegerToOrderableString => "substr('0000000000'||'{0}', -10, 10)"; + public override string ConvertDecimalToOrderableString => "substr('0000000000'||'{0}', -10, 10)"; + public override string ConvertDateToOrderableString => "{0}"; + + /// + public override string GetSpecialDbType(SpecialDbType dbType) => "TEXT COLLATE NOCASE"; + + /// + public override string GetSpecialDbType(SpecialDbType dbType, int customSize) => GetSpecialDbType(dbType); + + /// + public override bool TryGetDefaultConstraint(IDatabase db, string tableName, string columnName, + out string constraintName) + { + // TODO: SQLite + constraintName = string.Empty; + return false; + } + + public override string GetFieldNameForUpdate(Expression> fieldSelector, + string tableAlias = null) + { + var field = ExpressionHelper.FindProperty(fieldSelector).Item1 as PropertyInfo; + var fieldName = GetColumnName(field!); + + return GetQuotedColumnName(fieldName); + } + + private static string GetColumnName(PropertyInfo column) + { + ColumnAttribute? attr = column.FirstAttribute(); + return string.IsNullOrWhiteSpace(attr?.Name) ? column.Name : attr.Name; + } + + /// + protected override string FormatSystemMethods(SystemMethods systemMethod) + { + // TODO: SQLite + switch (systemMethod) + { + case SystemMethods.NewGuid: + return "NEWID()"; // No NEWID() in SQLite perhaps try RANDOM() + case SystemMethods.CurrentDateTime: + return "DATE()"; // No GETDATE() trying DATE() + } + + return null; + } + + /// + protected override string FormatIdentity(ColumnDefinition column) + { + /* NOTE: We need AUTOINCREMENT, adds overhead but makes magic ids not break everything. + * e.g. Cms.Core.Constants.Security.SuperUserId is -1 + * without the sqlite_sequence table we end up with the next user id = 0 + * but 0 is considered to not exist by our c# code and things explode */ + return column.IsIdentity ? "PRIMARY KEY AUTOINCREMENT" : string.Empty; + } + + public override string GetConcat(params string[] args) + { + return string.Join(" || ", args.AsEnumerable()); + } + + public override string GetColumn(DatabaseType dbType, string tableName, string columnName, string columnAlias, + string referenceName = null, bool forInsert = false) + { + if (forInsert) + { + return dbType.EscapeSqlIdentifier(columnName); + } + + return base.GetColumn(dbType, tableName, columnName, columnAlias, referenceName, forInsert); + } + + public override string FormatPrimaryKey(TableDefinition table) + { + ColumnDefinition? 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(Cms.Core.Constants.CharArrays.CommaSpace, StringSplitOptions.RemoveEmptyEntries) + .Select(GetQuotedColumnName)); + + // We can't name the PK if it's set as a column constraint so add an alternate at table level. + var constraintType = table.Columns.Any(x => x.IsIdentity) + ? "UNIQUE" + : "PRIMARY KEY"; + + return $"CONSTRAINT {constraintName} {constraintType} ({columns})"; + } + + + /// + public override Sql SelectTop(Sql sql, int top) + { + // SQLite uses LIMIT as opposed to TOP + // SELECT TOP 5 * FROM My_Table + // SELECT * FROM My_Table LIMIT 5; + + return sql.Append($"LIMIT {top}"); + } + + public virtual string Format(IEnumerable columns) + { + var sb = new StringBuilder(); + foreach (ColumnDefinition column in columns) + { + sb.AppendLine(", " + Format(column)); + } + + return sb.ToString().TrimStart(','); + } + + public override void HandleCreateTable(IDatabase database, TableDefinition tableDefinition, + bool skipKeysAndIndexes = false) + { + var columns = Format(tableDefinition.Columns); + var primaryKey = FormatPrimaryKey(tableDefinition); + List foreignKeys = Format(tableDefinition.ForeignKeys); + + var sb = new StringBuilder(); + sb.AppendLine($"CREATE TABLE {tableDefinition.Name}"); + sb.AppendLine("("); + sb.Append(columns); + + if (!string.IsNullOrEmpty(primaryKey) && !skipKeysAndIndexes) + { + sb.AppendLine($", {primaryKey}"); + } + + if (!skipKeysAndIndexes) + { + foreach (var foreignKey in foreignKeys) + { + sb.AppendLine($", {foreignKey}"); + } + } + + sb.AppendLine(")"); + + var createSql = sb.ToString(); + + _log.LogInformation("Create table:\n {Sql}", createSql); + database.Execute(new Sql(createSql)); + + if (skipKeysAndIndexes) + { + return; + } + + List indexSql = Format(tableDefinition.Indexes); + foreach (var sql in indexSql) + { + _log.LogInformation("Create Index:\n {Sql}", sql); + database.Execute(new Sql(sql)); + } + } + + public override IEnumerable GetTablesInSchema(IDatabase db) => + db.Fetch("select name from sqlite_master where type='table'") + .Where(x => !x.StartsWith("sqlite_")); + + public override IEnumerable GetColumnsInSchema(IDatabase db) + { + IEnumerable tables = GetTablesInSchema(db); + + db.OpenSharedConnection(); + foreach (var table in tables) + { + DbCommand? cmd = db.CreateCommand(db.Connection, CommandType.Text, $"PRAGMA table_info({table})"); + DbDataReader reader = cmd.ExecuteReader(); + + while (reader.Read()) + { + var ordinal = reader.GetInt32("cid"); + var columnName = reader.GetString("name"); + var type = reader.GetString("type"); + var notNull = reader.GetBoolean("notnull"); + yield return new ColumnInfo(table, columnName, ordinal, notNull, type); + } + } + } + + + /// + public override IEnumerable> GetConstraintsPerColumn(IDatabase db) + { + var items = db.Fetch("select * from sqlite_master where type = 'table'") + .Where(x => !x.Name.StartsWith("sqlite_")); + + List foundConstraints = new(); + foreach (SqliteMaster row in items) + { + var altPk = Regex.Match(row.Sql, @"CONSTRAINT (?PK_\w+)\s.*UNIQUE \(""(?.+?)""\)"); + if (altPk.Success) + { + var field = altPk.Groups["field"].Value; + var constraint = altPk.Groups["constraint"].Value; + foundConstraints.Add(new Constraint(row.Name, field, constraint)); + } + else + { + var identity = Regex.Match(row.Sql, @"""(?.+)"".*AUTOINCREMENT"); + if (identity.Success) + { + foundConstraints.Add(new Constraint(row.Name, identity.Groups["field"].Value, $"PK_{row.Name}")); + } + } + + var pk = Regex.Match(row.Sql, @"CONSTRAINT (?\w+)\s.*PRIMARY KEY \(""(?.+?)""\)"); + if (pk.Success) + { + var field = pk.Groups["field"].Value; + var constraint = pk.Groups["constraint"].Value; + foundConstraints.Add(new Constraint(row.Name, field, constraint)); + } + + var fkRegex = new Regex(@"CONSTRAINT (?\w+) FOREIGN KEY \(""(?.+?)""\) REFERENCES"); + var foreignKeys = fkRegex.Matches(row.Sql).Cast(); + { + foreach (var fk in foreignKeys) + { + var field = fk.Groups["field"].Value; + var constraint = fk.Groups["constraint"].Value; + foundConstraints.Add(new Constraint(row.Name, field, constraint)); + } + } + } + + // item.TableName, item.ColumnName, item.ConstraintName + return foundConstraints + .Select(x => Tuple.Create(x.TableName, x.ColumnName, x.ConstraintName)); + } + + public override Sql.SqlJoinClause LeftJoinWithNestedJoin( + Sql sql, + Func, + Sql> nestedJoin, + string? alias = null) + { + Type type = typeof(TDto); + + var tableName = GetQuotedTableName(type.GetTableName()); + var join = tableName; + string? quotedAlias = null; + + if (alias != null) + { + quotedAlias = GetQuotedTableName(alias); + join += " " + quotedAlias; + } + + var nestedSql = new Sql(sql.SqlContext); + nestedSql = nestedJoin(nestedSql); + + Sql.SqlJoinClause sqlJoin = sql.LeftJoin("(" + join); + sql.Append(nestedSql); + sql.Append($") {quotedAlias ?? tableName}"); + return sqlJoin; + } + + public override IDictionary ScalarMappers => _scalarMappers; + + private class Constraint + { + public string TableName { get; } + + public string ColumnName { get; } + + public string ConstraintName { get; } + + public Constraint(string tableName, string columnName, string constraintName) + { + TableName = tableName; + ColumnName = columnName; + ConstraintName = constraintName; + } + + public override string ToString() => ConstraintName; + } + + private class SqliteMaster + { + public string Type { get; set; } + public string Name { get; set; } + public string Sql { get; set; } + } + + private class IndexMeta + { + public string TableName { get; set; } + public string IndexName { get; set; } + public string ColumnName { get; set; } + public bool IsUnique { get; set; } + } +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteTransientErrorDetectionStrategy.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteTransientErrorDetectionStrategy.cs new file mode 100644 index 0000000000..54414b2121 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteTransientErrorDetectionStrategy.cs @@ -0,0 +1,17 @@ +using Microsoft.Data.Sqlite; +using Umbraco.Cms.Infrastructure.Persistence.FaultHandling; + +namespace Umbraco.Cms.Persistence.Sqlite.Services; + +public class SqliteTransientErrorDetectionStrategy : ITransientErrorDetectionStrategy +{ + public bool IsTransient(Exception ex) + { + if (ex is not SqliteException sqliteException) + { + return false; + } + + return sqliteException.IsTransient || sqliteException.IsBusyOrLocked(); + } +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/SqliteComposer.cs b/src/Umbraco.Cms.Persistence.Sqlite/SqliteComposer.cs new file mode 100644 index 0000000000..d638f713a2 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/SqliteComposer.cs @@ -0,0 +1,14 @@ +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.DependencyInjection; + +namespace Umbraco.Cms.Persistence.Sqlite; + +/// +/// Automatically adds SQLite support to Umbraco when this project is referenced. +/// +public class SqliteComposer : IComposer +{ + /// + public void Compose(IUmbracoBuilder builder) + => builder.AddUmbracoSqliteSupport(); +} diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Umbraco.Cms.Persistence.Sqlite.csproj b/src/Umbraco.Cms.Persistence.Sqlite/Umbraco.Cms.Persistence.Sqlite.csproj new file mode 100644 index 0000000000..3fce21af07 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/Umbraco.Cms.Persistence.Sqlite.csproj @@ -0,0 +1,21 @@ + + + + net6.0 + enable + enable + + Umbraco.Cms.Persistence.Sqlite + Umbraco.Cms.Persistence.Sqlite + Adds support for SQLite to Umbraco CMS. + + + + + + + + + + + diff --git a/src/Umbraco.Cms.Persistence.Sqlite/UmbracoBuilderExtensions.cs b/src/Umbraco.Cms.Persistence.Sqlite/UmbracoBuilderExtensions.cs new file mode 100644 index 0000000000..0945b71270 --- /dev/null +++ b/src/Umbraco.Cms.Persistence.Sqlite/UmbracoBuilderExtensions.cs @@ -0,0 +1,41 @@ +using System.Data.Common; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.DistributedLocking; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; +using Umbraco.Cms.Persistence.Sqlite.Interceptors; +using Umbraco.Cms.Persistence.Sqlite.Services; + +namespace Umbraco.Cms.Persistence.Sqlite; + +/// +/// SQLite support extensions for IUmbracoBuilder. +/// +public static class UmbracoBuilderExtensions +{ + /// + /// Add required services for SQLite support. + /// + public static IUmbracoBuilder AddUmbracoSqliteSupport(this IUmbracoBuilder builder) + { + // TryAddEnumerable takes both TService and TImplementation into consideration (unlike TryAddSingleton) + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); + + DbProviderFactories.UnregisterFactory(Constants.ProviderName); + DbProviderFactories.RegisterFactory(Constants.ProviderName, Microsoft.Data.Sqlite.SqliteFactory.Instance); + + return builder; + } +} diff --git a/src/Umbraco.Core/Configuration/ConfigConnectionString.cs b/src/Umbraco.Core/Configuration/ConfigConnectionString.cs deleted file mode 100644 index 116b96df9c..0000000000 --- a/src/Umbraco.Core/Configuration/ConfigConnectionString.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using System.Data.Common; - -namespace Umbraco.Cms.Core.Configuration -{ - public class ConfigConnectionString - { - public string Name { get; } - - public string ConnectionString { get; } - - public string ProviderName { get; } - - public ConfigConnectionString(string name, string connectionString, string providerName = null) - { - Name = name ?? throw new ArgumentNullException(nameof(name)); - ConnectionString = ParseConnectionString(connectionString, ref providerName); - ProviderName = providerName; - } - - private static string ParseConnectionString(string connectionString, ref string providerName) - { - if (string.IsNullOrEmpty(connectionString)) - { - return connectionString; - } - - var builder = new DbConnectionStringBuilder - { - ConnectionString = connectionString - }; - - // Replace data directory placeholder - const string attachDbFileNameKey = "AttachDbFileName"; - const string dataDirectoryPlaceholder = "|DataDirectory|"; - if (builder.TryGetValue(attachDbFileNameKey, out var attachDbFileNameValue) && - attachDbFileNameValue is string attachDbFileName && - attachDbFileName.Contains(dataDirectoryPlaceholder)) - { - var dataDirectory = AppDomain.CurrentDomain.GetData("DataDirectory")?.ToString(); - if (!string.IsNullOrEmpty(dataDirectory)) - { - builder[attachDbFileNameKey] = attachDbFileName.Replace(dataDirectoryPlaceholder, dataDirectory); - - // Mutate the existing connection string (note: the builder also lowercases the properties) - connectionString = builder.ToString(); - } - } - - // Also parse provider name now we already have a builder - if (string.IsNullOrEmpty(providerName)) - { - providerName = ParseProviderName(builder); - } - - return connectionString; - } - - /// - /// Parses the connection string to get the provider name. - /// - /// The connection string. - /// - /// The provider name or null is the connection string is empty. - /// - public static string ParseProviderName(string connectionString) - { - if (string.IsNullOrEmpty(connectionString)) - { - return null; - } - - var builder = new DbConnectionStringBuilder - { - ConnectionString = connectionString - }; - - return ParseProviderName(builder); - } - - private static string ParseProviderName(DbConnectionStringBuilder builder) - { - if ((builder.TryGetValue("Data Source", out var dataSource) || builder.TryGetValue("DataSource", out dataSource)) && - dataSource?.ToString().EndsWith(".sdf", StringComparison.OrdinalIgnoreCase) == true) - { - return Constants.DbProviderNames.SqlCe; - } - - return Constants.DbProviderNames.SqlServer; - } - } -} diff --git a/src/Umbraco.Core/Configuration/ConfigureConnectionStrings.cs b/src/Umbraco.Core/Configuration/ConfigureConnectionStrings.cs new file mode 100644 index 0000000000..174a65ac1e --- /dev/null +++ b/src/Umbraco.Core/Configuration/ConfigureConnectionStrings.cs @@ -0,0 +1,40 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Configuration; + +/// +/// Configures ConnectionStrings. +/// +public class ConfigureConnectionStrings : IConfigureNamedOptions +{ + private readonly IConfiguration _configuration; + + /// + /// Initializes a new instance of the class. + /// + public ConfigureConnectionStrings(IConfiguration configuration) => _configuration = configuration; + + /// + public void Configure(ConnectionStrings options) => Configure(Constants.System.UmbracoConnectionName, options); + + /// + public void Configure(string name, ConnectionStrings options) + { + if (name == Options.DefaultName) + { + name = Constants.System.UmbracoConnectionName; + } + + if (options.IsConnectionStringConfigured()) + { + return; + } + + options.Name = name; + options.ConnectionString = _configuration.GetConnectionString(name); + options.ProviderName = _configuration.GetConnectionString($"{name}{ConnectionStrings.ProviderNamePostfix}") ?? ConnectionStrings.DefaultProviderName; + } +} diff --git a/src/Umbraco.Core/Configuration/Models/ConnectionStrings.cs b/src/Umbraco.Core/Configuration/Models/ConnectionStrings.cs index c0421be7e3..0642a6171c 100644 --- a/src/Umbraco.Core/Configuration/Models/ConnectionStrings.cs +++ b/src/Umbraco.Core/Configuration/Models/ConnectionStrings.cs @@ -1,31 +1,34 @@ -// Copyright (c) Umbraco. -// See LICENSE for more details. +using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration.Models; + +[UmbracoOptions("ConnectionStrings")] +public class ConnectionStrings { - /// - /// Typed configuration options for connection strings. - /// - [UmbracoOptions("ConnectionStrings", BindNonPublicProperties = true)] - public class ConnectionStrings - { - // Backing field for UmbracoConnectionString to load from configuration value with key umbracoDbDSN. - // Attributes cannot be applied to map from keys that don't match, and have chosen to retain the key name - // used in configuration for older Umbraco versions. - // See: https://stackoverflow.com/a/54607296/489433 -#pragma warning disable SA1300 // Element should begin with upper-case letter -#pragma warning disable IDE1006 // Naming Styles - private string umbracoDbDSN -#pragma warning restore IDE1006 // Naming Styles -#pragma warning restore SA1300 // Element should begin with upper-case letter - { - get => UmbracoConnectionString?.ConnectionString; - set => UmbracoConnectionString = new ConfigConnectionString(Constants.System.UmbracoConnectionName, value); - } + private string _connectionString; - /// - /// Gets or sets a value for the Umbraco database connection string.. - /// - public ConfigConnectionString UmbracoConnectionString { get; set; } = new ConfigConnectionString(Constants.System.UmbracoConnectionName, null); + /// + /// The default provider name when not present in configuration. + /// + public const string DefaultProviderName = "Microsoft.Data.SqlClient"; + + /// + /// The DataDirectory placeholder. + /// + public const string DataDirectoryPlaceholder = "|DataDirectory|"; + + /// + /// The postfix used to identify a connection strings provider setting. + /// + public const string ProviderNamePostfix = "_ProviderName"; + + public string Name { get; set; } + + public string ConnectionString + { + get => _connectionString; + set => _connectionString = value.ReplaceDataDirectoryPlaceholder(); } + + public string ProviderName { get; set; } = DefaultProviderName; } diff --git a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs index 5e42d3b8be..c082e1e651 100644 --- a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs @@ -27,7 +27,8 @@ namespace Umbraco.Cms.Core.Configuration.Models internal const bool StaticInstallMissingDatabase = false; internal const bool StaticDisableElectionForSingleServer = false; internal const string StaticNoNodesViewPath = "~/umbraco/UmbracoWebsite/NoNodes.cshtml"; - internal const string StaticSqlWriteLockTimeOut = "00:00:05"; + internal const string StaticDistributedLockingReadLockDefaultTimeout = "00:01:00"; + internal const string StaticDistributedLockingWriteLockDefaultTimeout = "00:00:05"; internal const bool StaticSanitizeTinyMce = false; internal const int StaticMainDomReleaseSignalPollingInterval = 2000; @@ -201,12 +202,26 @@ namespace Umbraco.Cms.Core.Configuration.Models public bool SanitizeTinyMce { get; set; } = StaticSanitizeTinyMce; /// - /// An int value representing the time in milliseconds to lock the database for a write operation + /// Gets or sets a value representing the maximum time to wait whilst attempting to obtain a distributed read lock. /// /// - /// The default value is 5000 milliseconds. + /// The default value is 60 seconds. /// - [DefaultValue(StaticSqlWriteLockTimeOut)] - public TimeSpan SqlWriteLockTimeOut { get; set; } = TimeSpan.Parse(StaticSqlWriteLockTimeOut); + [DefaultValue(StaticDistributedLockingReadLockDefaultTimeout)] + public TimeSpan DistributedLockingReadLockDefaultTimeout { get; set; } = TimeSpan.Parse(StaticDistributedLockingReadLockDefaultTimeout); + + /// + /// Gets or sets a value representing the maximum time to wait whilst attempting to obtain a distributed write lock. + /// + /// + /// The default value is 5 seconds. + /// + [DefaultValue(StaticDistributedLockingWriteLockDefaultTimeout)] + public TimeSpan DistributedLockingWriteLockDefaultTimeout { get; set; } = TimeSpan.Parse(StaticDistributedLockingWriteLockDefaultTimeout); + + /// + /// Gets or sets a value representing the DistributedLockingMechanism to use. + /// + public string DistributedLockingMechanism { get; set; } = string.Empty; } } diff --git a/src/Umbraco.Core/Configuration/Models/Validation/GlobalSettingsValidator.cs b/src/Umbraco.Core/Configuration/Models/Validation/GlobalSettingsValidator.cs index 5a7a0ad2f5..abd8db19c7 100644 --- a/src/Umbraco.Core/Configuration/Models/Validation/GlobalSettingsValidator.cs +++ b/src/Umbraco.Core/Configuration/Models/Validation/GlobalSettingsValidator.cs @@ -20,7 +20,7 @@ namespace Umbraco.Cms.Core.Configuration.Models.Validation return ValidateOptionsResult.Fail(message); } - if (!ValidateSqlWriteLockTimeOutSetting(options.SqlWriteLockTimeOut, out var message2)) + if (!ValidateSqlWriteLockTimeOutSetting(options.DistributedLockingWriteLockDefaultTimeout, out var message2)) { return ValidateOptionsResult.Fail(message2); } @@ -37,7 +37,7 @@ namespace Umbraco.Cms.Core.Configuration.Models.Validation const int maximumTimeOut = 20000; if (configuredTimeOut.TotalMilliseconds < minimumTimeOut || configuredTimeOut.TotalMilliseconds > maximumTimeOut) // between 0.1 and 20 seconds { - message = $"The `{Constants.Configuration.ConfigGlobal}:{nameof(GlobalSettings.SqlWriteLockTimeOut)}` setting is not between the minimum of {minimumTimeOut} ms and maximum of {maximumTimeOut} ms"; + message = $"The `{Constants.Configuration.ConfigGlobal}:{nameof(GlobalSettings.DistributedLockingWriteLockDefaultTimeout)}` setting is not between the minimum of {minimumTimeOut} ms and maximum of {maximumTimeOut} ms"; return false; } diff --git a/src/Umbraco.Core/Constants-Configuration.cs b/src/Umbraco.Core/Constants-Configuration.cs index bdbd13b2a4..b3963d64ef 100644 --- a/src/Umbraco.Core/Constants-Configuration.cs +++ b/src/Umbraco.Core/Constants-Configuration.cs @@ -17,6 +17,7 @@ namespace Umbraco.Cms.Core public const string ConfigCustomErrorsPrefix = ConfigPrefix + "CustomErrors:"; public const string ConfigGlobalPrefix = ConfigPrefix + "Global:"; public const string ConfigGlobalId = ConfigGlobalPrefix + "Id"; + public const string ConfigGlobalDistributedLockingMechanism = ConfigGlobalPrefix + "DistributedLockingMechanism"; public const string ConfigHostingPrefix = ConfigPrefix + "Hosting:"; public const string ConfigModelsBuilderPrefix = ConfigPrefix + "ModelsBuilder:"; public const string ConfigSecurityPrefix = ConfigPrefix + "Security:"; diff --git a/src/Umbraco.Core/Constants-DatabaseProviders.cs b/src/Umbraco.Core/Constants-DatabaseProviders.cs deleted file mode 100644 index 1fd16133e5..0000000000 --- a/src/Umbraco.Core/Constants-DatabaseProviders.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Umbraco.Cms.Core -{ - public static partial class Constants - { - public static class DatabaseProviders - { - public const string SqlCe = "System.Data.SqlServerCe.4.0"; - public const string SqlServer = "Microsoft.Data.SqlClient"; - } - } -} diff --git a/src/Umbraco.Core/Constants-System.cs b/src/Umbraco.Core/Constants-System.cs index ddff380c08..eeea929662 100644 --- a/src/Umbraco.Core/Constants-System.cs +++ b/src/Umbraco.Core/Constants-System.cs @@ -59,7 +59,10 @@ public const string RecycleBinMediaPathPrefix = "-1,-21,"; public const int DefaultLabelDataTypeId = -92; - public const string UmbracoConnectionName = "umbracoDbDSN"; + + public const string UmbracoDefaultDatabaseName = "Umbraco"; + + public const string UmbracoConnectionName = "umbracoDbDSN"; } } } diff --git a/src/Umbraco.Core/DependencyInjection/IUmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/IUmbracoBuilder.cs index 738cfed26e..1fc06f4714 100644 --- a/src/Umbraco.Core/DependencyInjection/IUmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/IUmbracoBuilder.cs @@ -31,7 +31,7 @@ namespace Umbraco.Cms.Core.DependencyInjection IProfiler Profiler { get; } AppCaches AppCaches { get; } - TBuilder WithCollectionBuilder() where TBuilder : ICollectionBuilder, new(); + TBuilder WithCollectionBuilder() where TBuilder : ICollectionBuilder; void Build(); } } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs index 91e6f71415..8aed77c0d5 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs @@ -90,6 +90,8 @@ namespace Umbraco.Cms.Core.DependencyInjection .AddUmbracoOptions() .AddUmbracoOptions(); + builder.Services.AddSingleton, ConfigureConnectionStrings>(); + builder.Services.Configure(options => options.MergeReplacements(builder.Config)); return builder; diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 26646d2a4f..bc099517ad 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -103,7 +103,7 @@ namespace Umbraco.Cms.Core.DependencyInjection /// The type of the collection builder. /// The collection builder. public TBuilder WithCollectionBuilder() - where TBuilder : ICollectionBuilder, new() + where TBuilder : ICollectionBuilder { Type typeOfBuilder = typeof(TBuilder); @@ -112,7 +112,22 @@ namespace Umbraco.Cms.Core.DependencyInjection return (TBuilder)o; } - var builder = new TBuilder(); + TBuilder builder; + + if (typeof(TBuilder).GetConstructor(Type.EmptyTypes) != null) + { + builder = Activator.CreateInstance(); + } + else if (typeof(TBuilder).GetConstructor(new[] { typeof(IUmbracoBuilder) }) != null) + { + // Handle those collection builders which need a reference to umbraco builder i.e. DistributedLockingCollectionBuilder. + builder = (TBuilder)Activator.CreateInstance(typeof(TBuilder), this); + } + else + { + throw new InvalidOperationException("A CollectionBuilder must have either a parameterless constructor or a constructor whose only parameter is of type IUmbracoBuilder"); + } + _builders[typeOfBuilder] = builder; return builder; } diff --git a/src/Umbraco.Core/DistributedLocking/DistributedLockType.cs b/src/Umbraco.Core/DistributedLocking/DistributedLockType.cs new file mode 100644 index 0000000000..01acd02c10 --- /dev/null +++ b/src/Umbraco.Core/DistributedLocking/DistributedLockType.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.Core.DistributedLocking; + +/// +/// Represents the type of distributed lock. +/// +public enum DistributedLockType +{ + ReadLock, + WriteLock +} diff --git a/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedLockingException.cs b/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedLockingException.cs new file mode 100644 index 0000000000..2f27929a6c --- /dev/null +++ b/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedLockingException.cs @@ -0,0 +1,26 @@ +using System; + +namespace Umbraco.Cms.Core.DistributedLocking.Exceptions; + +/// +/// Base class for all DistributedLockingExceptions. +/// +public class DistributedLockingException : ApplicationException +{ + /// + /// Initializes a new instance of the class. + /// + public DistributedLockingException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + // ReSharper disable once UnusedMember.Global + public DistributedLockingException(string message, Exception innerException) + : base(message, innerException) + { + } +} diff --git a/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedLockingTimeoutException.cs b/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedLockingTimeoutException.cs new file mode 100644 index 0000000000..9d65023790 --- /dev/null +++ b/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedLockingTimeoutException.cs @@ -0,0 +1,15 @@ +namespace Umbraco.Cms.Core.DistributedLocking.Exceptions; + +/// +/// Base class for all DistributedLocking timeout related exceptions. +/// +public abstract class DistributedLockingTimeoutException : DistributedLockingException +{ + /// + /// Initializes a new instance of the class. + /// + protected DistributedLockingTimeoutException(int lockId, bool isWrite) + : base($"Failed to acquire {(isWrite ? "write" : "read")} lock for id: {lockId}.") + { + } +} diff --git a/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedReadLockTimeoutException.cs b/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedReadLockTimeoutException.cs new file mode 100644 index 0000000000..4d37238c0d --- /dev/null +++ b/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedReadLockTimeoutException.cs @@ -0,0 +1,15 @@ +namespace Umbraco.Cms.Core.DistributedLocking.Exceptions; + +/// +/// Exception thrown when a read lock could not be obtained in a timely manner. +/// +public class DistributedReadLockTimeoutException : DistributedLockingTimeoutException +{ + /// + /// Initializes a new instance of the class. + /// + public DistributedReadLockTimeoutException(int lockId) + : base(lockId, false) + { + } +} diff --git a/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedWriteLockTimeoutException.cs b/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedWriteLockTimeoutException.cs new file mode 100644 index 0000000000..abf84470e0 --- /dev/null +++ b/src/Umbraco.Core/DistributedLocking/Exceptions/DistributedWriteLockTimeoutException.cs @@ -0,0 +1,15 @@ +namespace Umbraco.Cms.Core.DistributedLocking.Exceptions; + +/// +/// Exception thrown when a write lock could not be obtained in a timely manner. +/// +public class DistributedWriteLockTimeoutException : DistributedLockingTimeoutException +{ + /// + /// Initializes a new instance of the class. + /// + public DistributedWriteLockTimeoutException(int lockId) + : base(lockId, true) + { + } +} diff --git a/src/Umbraco.Core/DistributedLocking/IDistributedLock.cs b/src/Umbraco.Core/DistributedLocking/IDistributedLock.cs new file mode 100644 index 0000000000..202bb594bc --- /dev/null +++ b/src/Umbraco.Core/DistributedLocking/IDistributedLock.cs @@ -0,0 +1,19 @@ +using System; + +namespace Umbraco.Cms.Core.DistributedLocking; + +/// +/// Interface representing a DistributedLock. +/// +public interface IDistributedLock : IDisposable +{ + /// + /// Gets the LockId. + /// + int LockId { get; } + + /// + /// Gets the DistributedLockType. + /// + DistributedLockType LockType { get; } +} diff --git a/src/Umbraco.Core/DistributedLocking/IDistributedLockingMechanism.cs b/src/Umbraco.Core/DistributedLocking/IDistributedLockingMechanism.cs new file mode 100644 index 0000000000..5df8a23650 --- /dev/null +++ b/src/Umbraco.Core/DistributedLocking/IDistributedLockingMechanism.cs @@ -0,0 +1,51 @@ +using System; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DistributedLocking.Exceptions; + +namespace Umbraco.Cms.Core.DistributedLocking; + +/// +/// Represents a class responsible for managing distributed locks. +/// +/// +/// In general the rules for distributed locks are as follows. +/// +/// +/// Cannot obtain a write lock if a read lock exists for same lock id (except during an upgrade from reader -> writer) +/// +/// +/// Cannot obtain a write lock if a write lock exists for same lock id. +/// +/// +/// Cannot obtain a read lock if a write lock exists for same lock id. +/// +/// +/// Can obtain a read lock if a read lock exists for same lock id. +/// +/// +/// +public interface IDistributedLockingMechanism +{ + /// + /// Gets a value indicating whether this distributed locking mechanism can be used. + /// + bool Enabled { get; } + + /// + /// Obtains a distributed read lock. + /// + /// + /// When timeout is null, implementations should use . + /// + /// Failed to obtain distributed read lock in time. + IDistributedLock ReadLock(int lockId, TimeSpan? obtainLockTimeout = null); + + /// + /// Obtains a distributed read lock. + /// + /// + /// When timeout is null, implementations should use . + /// + /// Failed to obtain distributed write lock in time. + IDistributedLock WriteLock(int lockId, TimeSpan? obtainLockTimeout = null); +} diff --git a/src/Umbraco.Core/DistributedLocking/IDistributedLockingMechanismFactory.cs b/src/Umbraco.Core/DistributedLocking/IDistributedLockingMechanismFactory.cs new file mode 100644 index 0000000000..1bd1cfe206 --- /dev/null +++ b/src/Umbraco.Core/DistributedLocking/IDistributedLockingMechanismFactory.cs @@ -0,0 +1,9 @@ +namespace Umbraco.Cms.Core.DistributedLocking; + +/// +/// Picks an appropriate IDistributedLockingMechanism when multiple are registered +/// +public interface IDistributedLockingMechanismFactory +{ + IDistributedLockingMechanism DistributedLockingMechanism { get; } +} diff --git a/src/Umbraco.Core/Extensions/ConfigConnectionStringExtensions.cs b/src/Umbraco.Core/Extensions/ConfigConnectionStringExtensions.cs deleted file mode 100644 index 329c9e8202..0000000000 --- a/src/Umbraco.Core/Extensions/ConfigConnectionStringExtensions.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Umbraco. -// See LICENSE for more details. - -using Umbraco.Cms.Core.Configuration; - -namespace Umbraco.Extensions -{ - public static class ConfigConnectionStringExtensions - { - public static bool IsConnectionStringConfigured(this ConfigConnectionString databaseSettings) - => databaseSettings != null && - !string.IsNullOrWhiteSpace(databaseSettings.ConnectionString) && - !string.IsNullOrWhiteSpace(databaseSettings.ProviderName); - } -} diff --git a/src/Umbraco.Core/Extensions/ConnectionStringExtensions.cs b/src/Umbraco.Core/Extensions/ConnectionStringExtensions.cs new file mode 100644 index 0000000000..6aa17055f9 --- /dev/null +++ b/src/Umbraco.Core/Extensions/ConnectionStringExtensions.cs @@ -0,0 +1,35 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System; +using Microsoft.Extensions.Configuration; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; + +namespace Umbraco.Extensions +{ + public static class ConnectionStringExtensions + { + public static bool IsConnectionStringConfigured(this ConnectionStrings connectionString) + => connectionString != null && + !string.IsNullOrWhiteSpace(connectionString.ConnectionString) && + !string.IsNullOrWhiteSpace(connectionString.ProviderName); + + /// + /// Gets a connection string from configuration with placeholders replaced. + /// + public static string GetUmbracoConnectionString( + this IConfiguration configuration, + string connectionStringName = Constants.System.UmbracoConnectionName) => + configuration.GetConnectionString(connectionStringName).ReplaceDataDirectoryPlaceholder(); + + /// + /// Replaces instances of the |DataDirectory| placeholder in a string with the value of AppDomain DataDirectory. + /// + public static string ReplaceDataDirectoryPlaceholder(this string input) + { + var dataDirectory = AppDomain.CurrentDomain.GetData("DataDirectory")?.ToString(); + return input?.Replace(ConnectionStrings.DataDirectoryPlaceholder, dataDirectory); + } + } +} diff --git a/src/Umbraco.Core/Install/Models/DatabaseModel.cs b/src/Umbraco.Core/Install/Models/DatabaseModel.cs index b87941e590..d7f5bc9d03 100644 --- a/src/Umbraco.Core/Install/Models/DatabaseModel.cs +++ b/src/Umbraco.Core/Install/Models/DatabaseModel.cs @@ -1,3 +1,4 @@ +using System; using System.Runtime.Serialization; namespace Umbraco.Cms.Core.Install.Models @@ -5,8 +6,11 @@ namespace Umbraco.Cms.Core.Install.Models [DataContract(Name = "database", Namespace = "")] public class DatabaseModel { - [DataMember(Name = "dbType")] - public DatabaseType DatabaseType { get; set; } = DatabaseType.SqlServer; + [DataMember(Name = "databaseProviderMetadataId")] + public Guid DatabaseProviderMetadataId { get; set; } + + [DataMember(Name = "providerName")] + public string ProviderName { get; set; } [DataMember(Name = "server")] public string Server { get; set; } diff --git a/src/Umbraco.Core/Install/Models/DatabaseType.cs b/src/Umbraco.Core/Install/Models/DatabaseType.cs deleted file mode 100644 index bc0616620f..0000000000 --- a/src/Umbraco.Core/Install/Models/DatabaseType.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Umbraco.Cms.Core.Install.Models -{ - public enum DatabaseType - { - SqlLocalDb, - SqlCe, - SqlServer, - SqlAzure, - Custom - } -} diff --git a/src/Umbraco.Core/Persistence/Constants-DbProviderNames.cs b/src/Umbraco.Core/Persistence/Constants-DbProviderNames.cs deleted file mode 100644 index bd95776dea..0000000000 --- a/src/Umbraco.Core/Persistence/Constants-DbProviderNames.cs +++ /dev/null @@ -1,12 +0,0 @@ - // ReSharper disable once CheckNamespace -namespace Umbraco.Cms.Core -{ - static partial class Constants - { - public static class DbProviderNames - { - public const string SqlServer = "Microsoft.Data.SqlClient"; - public const string SqlCe = "System.Data.SqlServerCe.4.0"; - } - } -} diff --git a/src/Umbraco.Infrastructure/Configuration/JsonConfigManipulator.cs b/src/Umbraco.Infrastructure/Configuration/JsonConfigManipulator.cs index d72f17d559..8a38ab5cff 100644 --- a/src/Umbraco.Infrastructure/Configuration/JsonConfigManipulator.cs +++ b/src/Umbraco.Infrastructure/Configuration/JsonConfigManipulator.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Web.Common.DependencyInjection; namespace Umbraco.Cms.Core.Configuration @@ -182,8 +183,10 @@ namespace Umbraco.Cms.Core.Configuration writer.WriteStartObject(); writer.WritePropertyName("ConnectionStrings"); writer.WriteStartObject(); - writer.WritePropertyName(Cms.Core.Constants.System.UmbracoConnectionName); + writer.WritePropertyName(Constants.System.UmbracoConnectionName); writer.WriteValue(connectionString); + writer.WritePropertyName($"{Constants.System.UmbracoConnectionName}{ConnectionStrings.ProviderNamePostfix}"); + writer.WriteValue(providerName); writer.WriteEndObject(); writer.WriteEndObject(); diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 361cd1b4ad..17ca813b3f 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -9,6 +9,7 @@ using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.DistributedLocking; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Handlers; using Umbraco.Cms.Core.HealthChecks.NotificationMethods; @@ -36,6 +37,7 @@ using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Core.Templates; using Umbraco.Cms.Core.Trees; using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Infrastructure.DistributedLocking; using Umbraco.Cms.Infrastructure.Examine; using Umbraco.Cms.Infrastructure.HealthChecks; using Umbraco.Cms.Infrastructure.HostedServices; @@ -68,6 +70,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection .AddMainDom() .AddLogging(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(factory => factory.GetRequiredService().SqlContext); builder.NPocoMappers().Add(); diff --git a/src/Umbraco.Infrastructure/DistributedLocking/DefaultDistributedLockingMechanismFactory.cs b/src/Umbraco.Infrastructure/DistributedLocking/DefaultDistributedLockingMechanismFactory.cs new file mode 100644 index 0000000000..401e399441 --- /dev/null +++ b/src/Umbraco.Infrastructure/DistributedLocking/DefaultDistributedLockingMechanismFactory.cs @@ -0,0 +1,64 @@ +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; + +namespace Umbraco.Cms.Infrastructure.DistributedLocking; + +public class DefaultDistributedLockingMechanismFactory : IDistributedLockingMechanismFactory +{ + private object _lock = new(); + private bool _initialized; + private IDistributedLockingMechanism _distributedLockingMechanism; + + private readonly IOptionsMonitor _globalSettings; + private readonly IEnumerable _distributedLockingMechanisms; + + public DefaultDistributedLockingMechanismFactory( + IOptionsMonitor globalSettings, + IEnumerable distributedLockingMechanisms) + { + _globalSettings = globalSettings; + _distributedLockingMechanisms = distributedLockingMechanisms; + } + + public IDistributedLockingMechanism DistributedLockingMechanism + { + get + { + EnsureInitialized(); + + return _distributedLockingMechanism; + } + } + + private void EnsureInitialized() + => LazyInitializer.EnsureInitialized(ref _distributedLockingMechanism, ref _initialized, ref _lock, Initialize); + + private IDistributedLockingMechanism Initialize() + { + var configured = _globalSettings.CurrentValue.DistributedLockingMechanism; + + if (!string.IsNullOrEmpty(configured)) + { + IDistributedLockingMechanism value = _distributedLockingMechanisms + .FirstOrDefault(x => x.GetType().FullName?.EndsWith(configured) ?? false); + + if (value == null) + { + throw new InvalidOperationException($"Couldn't find DistributedLockingMechanism specified by global config: {configured}"); + } + } + + IDistributedLockingMechanism defaultMechanism = _distributedLockingMechanisms.FirstOrDefault(x => x.Enabled); + if (defaultMechanism != null) + { + return defaultMechanism; + } + + throw new InvalidOperationException($"Couldn't find an appropriate default distributed locking mechanism."); + } +} diff --git a/src/Umbraco.Infrastructure/Install/InstallHelper.cs b/src/Umbraco.Infrastructure/Install/InstallHelper.cs index cd7dfcbf0c..671dc85c4f 100644 --- a/src/Umbraco.Infrastructure/Install/InstallHelper.cs +++ b/src/Umbraco.Infrastructure/Install/InstallHelper.cs @@ -95,9 +95,10 @@ namespace Umbraco.Cms.Infrastructure.Install /// /// true if this is a brand new install; otherwise, false. /// - private bool IsBrandNewInstall => _connectionStrings.CurrentValue.UmbracoConnectionString?.IsConnectionStringConfigured() != true || - _databaseBuilder.IsDatabaseConfigured == false || - _databaseBuilder.CanConnectToDatabase == false || - _databaseBuilder.IsUmbracoInstalled() == false; + private bool IsBrandNewInstall => + _connectionStrings.Get(Constants.System.UmbracoConnectionName).IsConnectionStringConfigured() == false || + _databaseBuilder.IsDatabaseConfigured == false || + _databaseBuilder.CanConnectToDatabase == false || + _databaseBuilder.IsUmbracoInstalled() == false; } } diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs index a25151bda2..c2a4ad8e80 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs @@ -1,6 +1,6 @@ using System; using System.Collections.Generic; -using System.Runtime.InteropServices; +using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -20,116 +20,55 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps { private readonly DatabaseBuilder _databaseBuilder; private readonly ILogger _logger; + private readonly IEnumerable _databaseProviderMetadata; private readonly IOptionsMonitor _connectionStrings; - public DatabaseConfigureStep(DatabaseBuilder databaseBuilder, IOptionsMonitor connectionStrings, ILogger logger) + public DatabaseConfigureStep( + DatabaseBuilder databaseBuilder, + IOptionsMonitor connectionStrings, + ILogger logger, + IEnumerable databaseProviderMetadata) { _databaseBuilder = databaseBuilder; _connectionStrings = connectionStrings; _logger = logger; + _databaseProviderMetadata = databaseProviderMetadata; } - public override Task ExecuteAsync(DatabaseModel database) + public override Task ExecuteAsync(DatabaseModel databaseSettings) { - //if the database model is null then we will apply the defaults - if (database == null) - { - database = new DatabaseModel(); - - if (IsLocalDbAvailable()) - { - database.DatabaseType = DatabaseType.SqlLocalDb; - } - else if (IsSqlCeAvailable()) - { - database.DatabaseType = DatabaseType.SqlCe; - } - } - - if (_databaseBuilder.CanConnect(database.DatabaseType.ToString(), database.ConnectionString, database.Server, database.DatabaseName, database.Login, database.Password, database.IntegratedAuth) == false) + if (!_databaseBuilder.ConfigureDatabaseConnection(databaseSettings, isTrialRun: false)) { throw new InstallException("Could not connect to the database"); } - ConfigureConnection(database); - return Task.FromResult(null); } - private void ConfigureConnection(DatabaseModel database) - { - if (database.ConnectionString.IsNullOrWhiteSpace() == false) - { - _databaseBuilder.ConfigureDatabaseConnection(database.ConnectionString); - } - else if (database.DatabaseType == DatabaseType.SqlLocalDb) - { - _databaseBuilder.ConfigureSqlLocalDbDatabaseConnection(); - } - else if (database.DatabaseType == DatabaseType.SqlCe) - { - _databaseBuilder.ConfigureEmbeddedDatabaseConnection(); - } - else if (database.IntegratedAuth) - { - _databaseBuilder.ConfigureIntegratedSecurityDatabaseConnection(database.Server, database.DatabaseName); - } - else - { - var password = database.Password.Replace("'", "''"); - password = string.Format("'{0}'", password); - - _databaseBuilder.ConfigureDatabaseConnection(database.Server, database.DatabaseName, database.Login, password, database.DatabaseType.ToString()); - } - } - public override object ViewModel { get { - var databases = new List() - { - new { name = "Microsoft SQL Server", id = DatabaseType.SqlServer.ToString() }, - new { name = "Microsoft SQL Azure", id = DatabaseType.SqlAzure.ToString() }, - new { name = "Custom connection string", id = DatabaseType.Custom.ToString() }, - }; - - if (IsSqlCeAvailable()) - { - databases.Insert(0, new { name = "Microsoft SQL Server Compact (SQL CE)", id = DatabaseType.SqlCe.ToString() }); - } - - if (IsLocalDbAvailable()) - { - // Ensure this is always inserted as first when available - databases.Insert(0, new { name = "Microsoft SQL Server Express (LocalDB)", id = DatabaseType.SqlLocalDb.ToString() }); - } + var options = _databaseProviderMetadata + .Where(x => x.IsAvailable) + .OrderBy(x => x.SortOrder) + .ToList(); return new { - databases + databases = options }; } } - public static bool IsLocalDbAvailable() => new LocalDb().IsAvailable; - - public static bool IsSqlCeAvailable() => - // NOTE: Type.GetType will only return types that are currently loaded into the appdomain. In this case - // that is ok because we know if this is availalbe we will have manually loaded it into the appdomain. - // Else we'd have to use Assembly.LoadFrom and need to know the DLL location here which we don't need to do. - RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && - !(Type.GetType("Umbraco.Cms.Persistence.SqlCe.SqlCeSyntaxProvider, Umbraco.Persistence.SqlCe") is null); - public override string View => ShouldDisplayView() ? base.View : ""; - 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.CurrentValue.UmbracoConnectionString; + var databaseSettings = _connectionStrings.Get(Core.Constants.System.UmbracoConnectionName); if (databaseSettings.IsConnectionStringConfigured()) { diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseUpgradeStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseUpgradeStep.cs index 929b448286..336e386a96 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseUpgradeStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseUpgradeStep.cs @@ -77,7 +77,7 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps return false; } - var databaseSettings = _connectionStrings.CurrentValue.UmbracoConnectionString; + var databaseSettings = _connectionStrings.Get(Core.Constants.System.UmbracoConnectionName); if (databaseSettings.IsConnectionStringConfigured()) { diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs index 4ef8fa4e28..0a52a531c1 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Collections.Specialized; +using System.Linq; using System.Net.Http; using System.Text; using System.Threading.Tasks; @@ -37,6 +39,7 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps private readonly ICookieManager _cookieManager; private readonly IBackOfficeUserManager _userManager; private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator; + private readonly IEnumerable _databaseProviderMetadata; public NewInstallStep( IUserService userService, @@ -47,7 +50,8 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps IOptionsMonitor connectionStrings, ICookieManager cookieManager, IBackOfficeUserManager userManager, - IDbProviderFactoryCreator dbProviderFactoryCreator) + IDbProviderFactoryCreator dbProviderFactoryCreator, + IEnumerable databaseProviderMetadata) { _userService = userService ?? throw new ArgumentNullException(nameof(userService)); _databaseBuilder = databaseBuilder ?? throw new ArgumentNullException(nameof(databaseBuilder)); @@ -58,6 +62,7 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps _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) @@ -113,11 +118,22 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps { 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(), - quickInstallAvailable = DatabaseConfigureStep.IsSqlCeAvailable() || DatabaseConfigureStep.IsLocalDbAvailable(), + quickInstallSettings, customInstallAvailable = !GetInstallState().HasFlag(InstallState.ConnectionStringConfigured) }; } @@ -139,10 +155,11 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps { 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 databaseSettings = _connectionStrings.CurrentValue.UmbracoConnectionString; + var databaseSettings = _connectionStrings.Get(Constants.System.UmbracoConnectionName); var hasConnString = databaseSettings != null && _databaseBuilder.IsDatabaseConfigured; if (hasConnString) diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableOfDtoBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableOfDtoBuilder.cs index ae2098b75a..c3dc8b83ae 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableOfDtoBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableOfDtoBuilder.cs @@ -29,15 +29,8 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table var syntax = _context.SqlContext.SqlSyntax; var tableDefinition = DefinitionFactory.GetTableDefinition(TypeOfDto, syntax); - ExecuteSql(syntax.Format(tableDefinition)); - if (WithoutKeysAndIndexes) - return; - - ExecuteSql(syntax.FormatPrimaryKey(tableDefinition)); - foreach (var sql in syntax.Format(tableDefinition.ForeignKeys)) - ExecuteSql(sql); - foreach (var sql in syntax.Format(tableDefinition.Indexes)) - ExecuteSql(sql); + syntax.HandleCreateTable(_context.Database, tableDefinition, WithoutKeysAndIndexes); + _context.BuildingExpression = false; } private void ExecuteSql(string sql) diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs index 1cef9e4851..0f4c385c61 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs @@ -6,6 +6,21 @@ using Umbraco.Extensions; 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 { private readonly IMigrationContext _context; diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs index 0aa2cdda57..224af5d2d0 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs @@ -1,9 +1,14 @@ using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Install; +using Umbraco.Cms.Core.Install.Models; using Umbraco.Cms.Core.Migrations; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; @@ -32,6 +37,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install private readonly IOptionsMonitor _connectionStrings; private readonly IMigrationPlanExecutor _migrationPlanExecutor; private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory; + private readonly IEnumerable _databaseProviderMetadata; private DatabaseSchemaResult _databaseSchemaValidationResult; @@ -50,7 +56,8 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install IOptionsMonitor globalSettings, IOptionsMonitor connectionStrings, IMigrationPlanExecutor migrationPlanExecutor, - DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory) + DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, + IEnumerable databaseProviderMetadata) { _scopeProvider = scopeProvider; _scopeAccessor = scopeAccessor; @@ -64,6 +71,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install _connectionStrings = connectionStrings; _migrationPlanExecutor = migrationPlanExecutor; _databaseSchemaCreatorFactory = databaseSchemaCreatorFactory; + _databaseProviderMetadata = databaseProviderMetadata; } #region Status @@ -83,32 +91,9 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install /// /// Verifies whether a it is possible to connect to a database. /// - public bool CanConnect(string databaseType, string connectionString, string server, string database, string login, string password, bool integratedAuth) + public bool CanConnect(string connectionString, string providerName) { - // we do not test SqlCE or LocalDB connections - if (databaseType.InvariantContains("SqlCe") || databaseType.InvariantContains("SqlLocalDb")) - return true; - - string providerName; - - if (string.IsNullOrWhiteSpace(connectionString) == false) - { - providerName = ConfigConnectionString.ParseProviderName(connectionString); - } - else if (integratedAuth) - { - // has to be Sql Server - providerName = Constants.DbProviderNames.SqlServer; - connectionString = GetIntegratedSecurityDatabaseConnectionString(server, database); - } - else - { - connectionString = GetDatabaseConnectionString( - server, database, login, password, - databaseType, out providerName); - } - - var factory = _dbProviderFactoryCreator.CreateFactory(providerName); + DbProviderFactory factory = _dbProviderFactoryCreator.CreateFactory(providerName); return DbConnectionExtensions.IsConnectionAvailable(connectionString, factory); } @@ -147,65 +132,60 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install #region Configure Connection String - public const string EmbeddedDatabaseConnectionString = @"Data Source=|DataDirectory|\Umbraco.sdf;Flush Interval=1"; - - /// - /// Configures a connection string for the embedded database. - /// - public void ConfigureEmbeddedDatabaseConnection() + public bool ConfigureDatabaseConnection(DatabaseModel databaseSettings, bool isTrialRun) { - const string connectionString = EmbeddedDatabaseConnectionString; - const string providerName = Constants.DbProviderNames.SqlCe; + IDatabaseProviderMetadata providerMeta; - _configManipulator.SaveConnectionString(connectionString, providerName); - Configure(connectionString, providerName, true); + // 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); + + databaseSettings = new DatabaseModel + { + DatabaseName = providerMeta?.DefaultDatabaseName, + }; + } + else + { + providerMeta = _databaseProviderMetadata + .FirstOrDefault(x => x.Id == databaseSettings.DatabaseProviderMetadataId); + } + + if (providerMeta == null) + { + throw new InstallException("Unable to determine database provider configuration."); + } + + var connectionString = providerMeta.GenerateConnectionString(databaseSettings); + var providerName = databaseSettings.ProviderName ?? providerMeta.ProviderName; + + if (providerMeta.RequiresConnectionTest && !CanConnect(connectionString, providerName)) + { + return false; + } + + if (!isTrialRun) + { + _configManipulator.SaveConnectionString(connectionString, providerName); + Configure(connectionString, providerName, _globalSettings.CurrentValue.InstallMissingDatabase || providerMeta.ForceCreateDatabase); + } + + return true; } - public const string LocalDbConnectionString = @"Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True"; - - public void ConfigureSqlLocalDbDatabaseConnection() - { - string connectionString = LocalDbConnectionString; - const string providerName = Constants.DbProviderNames.SqlServer; - - _configManipulator.SaveConnectionString(connectionString, providerName); - Configure(connectionString, providerName, true); - } - - /// - /// Configures a connection string that has been entered manually. - /// - /// A connection string. - /// Has to be SQL Server - public void ConfigureDatabaseConnection(string connectionString) - { - _configManipulator.SaveConnectionString(connectionString, null); - Configure(connectionString, null, _globalSettings.CurrentValue.InstallMissingDatabase); - } - - /// - /// Configures a connection string from the installer. - /// - /// The name or address of the database server. - /// The name of the database. - /// The user name. - /// The user password. - /// The name of the provider (Sql, Sql Azure, Sql Ce). - public void ConfigureDatabaseConnection(string server, string databaseName, string user, string password, string databaseProvider) - { - var connectionString = GetDatabaseConnectionString(server, databaseName, user, password, databaseProvider, out var providerName); - - _configManipulator.SaveConnectionString(connectionString, providerName); - Configure(connectionString, providerName, _globalSettings.CurrentValue.InstallMissingDatabase); - } private void Configure(string connectionString, string providerName, bool installMissingDatabase) { // Update existing connection string - var umbracoConnectionString = new ConfigConnectionString(Constants.System.UmbracoConnectionName, connectionString, providerName); - _connectionStrings.CurrentValue.UmbracoConnectionString = umbracoConnectionString; + var umbracoConnectionString = _connectionStrings.Get(Core.Constants.System.UmbracoConnectionName); + umbracoConnectionString.ConnectionString = connectionString; + umbracoConnectionString.ProviderName = providerName; - _databaseFactory.Configure(umbracoConnectionString.ConnectionString, umbracoConnectionString.ProviderName); + _databaseFactory.Configure(umbracoConnectionString); if (installMissingDatabase) { @@ -213,102 +193,6 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install } } - /// - /// Gets a connection string from the installer. - /// - /// The name or address of the database server. - /// The name of the database. - /// The user name. - /// The user password. - /// The name of the provider (Sql, Sql Azure, Sql Ce). - /// - /// A connection string. - public static string GetDatabaseConnectionString(string server, string databaseName, string user, string password, string databaseProvider, out string providerName) - { - providerName = Constants.DbProviderNames.SqlServer; - - if (databaseProvider.InvariantContains("Azure")) - return GetAzureConnectionString(server, databaseName, user, password); - - return $"server={server};database={databaseName};user id={user};password={password}"; - } - - /// - /// Configures a connection string using Microsoft SQL Server integrated security. - /// - /// The name or address of the database server. - /// The name of the database - public void ConfigureIntegratedSecurityDatabaseConnection(string server, string databaseName) - { - var connectionString = GetIntegratedSecurityDatabaseConnectionString(server, databaseName); - const string providerName = Constants.DbProviderNames.SqlServer; - - _configManipulator.SaveConnectionString(connectionString, providerName); - _databaseFactory.Configure(connectionString, providerName); - } - - /// - /// Gets a connection string using Microsoft SQL Server integrated security. - /// - /// The name or address of the database server. - /// The name of the database - /// A connection string. - public static string GetIntegratedSecurityDatabaseConnectionString(string server, string databaseName) - { - return $"Server={server};Database={databaseName};Integrated Security=true"; - } - - /// - /// Gets an Azure connection string. - /// - /// The name or address of the database server. - /// The name of the database. - /// The user name. - /// The user password. - /// A connection string. - public static string GetAzureConnectionString(string server, string databaseName, string user, string password) - { - if (server.Contains(".") && ServerStartsWithTcp(server) == false) - server = $"tcp:{server}"; - - if (server.Contains(".") == false && ServerStartsWithTcp(server)) - { - string serverName = server.Contains(",") - ? server.Substring(0, server.IndexOf(",", StringComparison.Ordinal)) - : server; - - var portAddition = string.Empty; - - if (server.Contains(",")) - portAddition = server.Substring(server.IndexOf(",", StringComparison.Ordinal)); - - server = $"{serverName}.database.windows.net{portAddition}"; - } - - if (ServerStartsWithTcp(server) == false) - server = $"tcp:{server}.database.windows.net"; - - if (server.Contains(",") == false) - server = $"{server},1433"; - - if (user.Contains("@") == false) - { - var userDomain = server; - - if (ServerStartsWithTcp(server)) - userDomain = userDomain.Substring(userDomain.IndexOf(":", StringComparison.Ordinal) + 1); - - if (userDomain.Contains(".")) - userDomain = userDomain.Substring(0, userDomain.IndexOf(".", StringComparison.Ordinal)); - - user = $"{user}@{userDomain}"; - } - - return $"Server={server};Database={databaseName};User ID={user};Password={password}"; - } - - private static bool ServerStartsWithTcp(string server) => server.InvariantStartsWith("tcp:"); - #endregion #region Database Schema diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs index b19802996b..434ed58f12 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs @@ -81,7 +81,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.LogViewerQuery)) CreateLogViewerQueryData(); - _logger.LogInformation("Done creating table {TableName} data.", tableName); + _logger.LogInformation("Completed creating data in {TableName}", tableName); } private void CreateNodeData() diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs index 52c86f9ccf..888f412070 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs @@ -89,8 +89,12 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install private readonly ILoggerFactory _loggerFactory; private readonly IUmbracoVersion _umbracoVersion; - public DatabaseSchemaCreator(IUmbracoDatabase database, ILogger logger, - ILoggerFactory loggerFactory, IUmbracoVersion umbracoVersion, IEventAggregator eventAggregator) + public DatabaseSchemaCreator( + IUmbracoDatabase database, + ILogger logger, + ILoggerFactory loggerFactory, + IUmbracoVersion umbracoVersion, + IEventAggregator eventAggregator) { _database = database ?? throw new ArgumentNullException(nameof(database)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -153,8 +157,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install if (creatingNotification.Cancel == false) { - var dataCreation = new DatabaseDataCreator(_database, - _loggerFactory.CreateLogger(), _umbracoVersion); + var dataCreation = new DatabaseDataCreator(_database, _loggerFactory.CreateLogger(), _umbracoVersion); foreach (Type table in OrderedTables) { CreateTable(false, table, dataCreation); @@ -448,12 +451,6 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install TableDefinition tableDefinition = DefinitionFactory.GetTableDefinition(modelType, SqlSyntax); var tableName = tableDefinition.Name; - - var createSql = SqlSyntax.Format(tableDefinition); - var createPrimaryKeySql = SqlSyntax.FormatPrimaryKey(tableDefinition); - List foreignSql = SqlSyntax.Format(tableDefinition.ForeignKeys); - List indexSql = SqlSyntax.Format(tableDefinition.Indexes); - var tableExist = TableExists(tableName); if (overwrite && tableExist) { @@ -471,18 +468,11 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install } //Execute the Create Table sql - _database.Execute(new Sql(createSql)); - _logger.LogInformation("Create Table {TableName}: \n {Sql}", tableName, createSql); - - //If any statements exists for the primary key execute them here - if (string.IsNullOrEmpty(createPrimaryKeySql) == false) - { - _database.Execute(new Sql(createPrimaryKeySql)); - _logger.LogInformation("Create Primary Key:\n {Sql}", createPrimaryKeySql); - } + 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 ")); } @@ -496,20 +486,6 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install _database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(tableName)} OFF;")); } - //Loop through index statements and execute sql - foreach (var sql in indexSql) - { - _database.Execute(new Sql(sql)); - _logger.LogInformation("Create Index:\n {Sql}", sql); - } - - //Loop through foreignkey statements and execute sql - foreach (var sql in foreignSql) - { - _database.Execute(new Sql(sql)); - _logger.LogInformation("Create Foreign Key:\n {Sql}", sql); - } - if (overwrite) { _logger.LogInformation("Table {TableName} was recreated", tableName); diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationBase_Extra.cs b/src/Umbraco.Infrastructure/Migrations/MigrationBase_Extra.cs index b3ad73811e..d20b0cc1eb 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationBase_Extra.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationBase_Extra.cs @@ -86,18 +86,8 @@ namespace Umbraco.Cms.Infrastructure.Migrations protected void ReplaceColumn(string tableName, string currentName, string newName) { - if (DatabaseType.IsSqlCe()) - { - AddColumn(tableName, newName, out var sqls); - Execute.Sql($"UPDATE {SqlSyntax.GetQuotedTableName(tableName)} SET {SqlSyntax.GetQuotedColumnName(newName)}={SqlSyntax.GetQuotedColumnName(currentName)}").Do(); - foreach (var sql in sqls) Execute.Sql(sql).Do(); - Delete.Column(currentName).FromTable(tableName).Do(); - } - else - { - Execute.Sql(SqlSyntax.FormatColumnRename(tableName, currentName, newName)).Do(); - AlterColumn(tableName, newName); - } + Execute.Sql(SqlSyntax.FormatColumnRename(tableName, currentName, newName)).Do(); + AlterColumn(tableName, newName); } protected bool TableExists(string tableName) diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationExpressionBase.cs b/src/Umbraco.Infrastructure/Migrations/MigrationExpressionBase.cs index cb9fab0160..9e36ec3ecd 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationExpressionBase.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationExpressionBase.cs @@ -120,7 +120,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations protected void AppendStatementSeparator(StringBuilder stmtBuilder) { stmtBuilder.AppendLine(";"); - if (DatabaseType.IsSqlServerOrCe()) + if (DatabaseType.IsSqlServer()) stmtBuilder.AppendLine("GO"); } 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 632b6f8a6c..fa88f17422 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 @@ -18,24 +18,12 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 AddColumn("id", out var sqls); - if (Database.DatabaseType.IsSqlCe()) - { - // SQLCE does not support UPDATE...FROM - var versions = Database.Fetch($@"SELECT v.versionId, v.id -FROM cmsContentVersion v -JOIN umbracoNode n on v.contentId=n.id -WHERE n.nodeObjectType='{Cms.Core.Constants.ObjectTypes.Media}'"); - foreach (var t in versions) - Execute.Sql($"UPDATE {Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion} SET id={t.id} WHERE versionId='{t.versionId}'").Do(); - } - else - { - Database.Execute($@"UPDATE {Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion} SET id=v.id + Database.Execute($@"UPDATE {Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion} SET id=v.id FROM {Cms.Core.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}'"); - } + foreach (var sql in sqls) Execute.Sql(sql).Do(); 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 8dbc7d59b3..f0fbb63729 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 @@ -76,19 +76,9 @@ HAVING COUNT(v2.id) <> 1").Any()) { Alter.Table(PreTables.PropertyData).AddColumn("versionId2").AsInt32().Nullable().Do(); - if (Database.DatabaseType.IsSqlCe()) - { - // SQLCE does not support UPDATE...FROM - var versions = Database.Fetch($"SELECT id, versionId FROM {PreTables.ContentVersion}"); - foreach (var t in versions) - Database.Execute($"UPDATE {PreTables.PropertyData} SET versionId2=@v2 WHERE versionId=@v1", new { v1 = t.versionId, v2 = t.id }); - } - else - { - Database.Execute($@"UPDATE {PreTables.PropertyData} SET versionId2={PreTables.ContentVersion}.id + 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"); @@ -181,40 +171,16 @@ INNER JOIN {PreTables.PropertyData} ON {PreTables.ContentVersion}.versionId = {P ReplaceColumn(PreTables.ContentVersion, "ContentId", "nodeId"); // populate contentVersion text, current and userId columns for documents - if (Database.DatabaseType.IsSqlCe()) - { - // SQLCE does not support UPDATE...FROM - var documents = Database.Fetch($"SELECT versionId, text, published, newest, documentUser FROM {PreTables.Document}"); - foreach (var t in documents) - Database.Execute($@"UPDATE {PreTables.ContentVersion} SET text=@text, {SqlSyntax.GetQuotedColumnName("current")}=@current, userId=@userId WHERE versionId=@versionId", - new { text = t.text, current = t.newest && !t.published, userId = t.documentUser, versionId = t.versionId }); - } - else - { - Database.Execute($@"UPDATE {PreTables.ContentVersion} SET text=d.text, {SqlSyntax.GetQuotedColumnName("current")}=(d.newest & ~d.published), userId=d.documentUser + 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 - if (Database.DatabaseType.IsSqlCe()) - { - // SQLCE does not support UPDATE...FROM - var otherContent = Database.Fetch($@"SELECT cver.versionId, n.text + 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 WHERE cver.versionId NOT IN (SELECT versionId FROM {SqlSyntax.GetQuotedTableName(PreTables.Document)})"); - foreach (var t in otherContent) - Database.Execute($@"UPDATE {PreTables.ContentVersion} SET text=@text, {SqlSyntax.GetQuotedColumnName("current")}=1, userId=0 WHERE versionId=@versionId", - new { text = t.text, versionId = t.versionId }); - } - else - { - 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 -WHERE cver.versionId NOT IN (SELECT versionId FROM {SqlSyntax.GetQuotedTableName(PreTables.Document)})"); - } // create table Create.Table(withoutKeysAndIndexes: true).Do(); 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 a63cb7c1e5..10db1964e0 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 @@ -17,22 +17,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_15_0 protected override void Migrate() { - // allow null for the `data` field - if (DatabaseType.IsSqlCe()) - { - // SQLCE does not support altering NTEXT, so we have to jump through some hoops to do it - // All column ordering must remain the same as what is defined in the DTO so we need to create a temp table, - // drop orig and then re-create/copy. - Create.Table(withoutKeysAndIndexes: true).Do(); - Execute.Sql($"INSERT INTO [{TempTableName}] SELECT nodeId, published, data, rv FROM [{Constants.DatabaseSchema.Tables.NodeData}]").Do(); - Delete.Table(Constants.DatabaseSchema.Tables.NodeData).Do(); - Create.Table().Do(); - Execute.Sql($"INSERT INTO [{Constants.DatabaseSchema.Tables.NodeData}] SELECT nodeId, published, data, rv, NULL FROM [{TempTableName}]").Do(); - } - else - { - AlterColumn(Constants.DatabaseSchema.Tables.NodeData, "data"); - } + AlterColumn(Constants.DatabaseSchema.Tables.NodeData, "data"); var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); AddColumnIfNotExists(columns, "dataRaw"); 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 868343374d..496e12a1fa 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 @@ -10,10 +10,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_15_0 protected override void Migrate() { - if (DatabaseType.IsSqlCe()) - { - Database.Execute(Sql("ALTER TABLE [cmsPropertyTypeGroup] ALTER COLUMN [id] IDENTITY (56,1)")); - } + // NOOP - was sql ce only } } } 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 b050f90d82..44034c5e45 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 @@ -109,22 +109,6 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 { string columnSpecification; - // If using SQL CE, we don't have access to COUNT (DISTINCT *) or CONCAT, so will need to do this by querying all records. - if (DatabaseType.IsSqlCe()) - { - columnSpecification = columns.Length == 1 - ? StringConvertedAndQuotedColumnName(columns[0]) - : $"{string.Join(" + ", columns.Select(x => StringConvertedAndQuotedColumnName(x)))}"; - - var allRecordsQuery = Database.SqlContext.Sql() - .Select(columnSpecification) - .From(); - - var allRecords = Database.Fetch(allRecordsQuery); - - return allRecords.Distinct().Count(); - } - columnSpecification = columns.Length == 1 ? QuoteColumnName(columns[0]) : $"CONCAT({string.Join(",", columns.Select(QuoteColumnName))})"; 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 4c7104e762..6b74c49f67 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 @@ -35,22 +35,8 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_3_0 //special trick to add the column without constraints and return the sql to add them later AddColumn("userOrMemberKey", out var sqls); - - if (DatabaseType.IsSqlCe()) - { - var userIds = Database.Fetch(Sql().Select("userId").From()); - - foreach (int userId in userIds) - { - Execute.Sql($"UPDATE {ExternalLoginDto.TableName} SET userOrMemberKey = '{userId.ToGuid()}' WHERE userId = {userId}").Do(); - } - } - else - { - //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(); - - } + //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(); diff --git a/src/Umbraco.Infrastructure/Persistence/BasicBulkSqlInsertProvider.cs b/src/Umbraco.Infrastructure/Persistence/BasicBulkSqlInsertProvider.cs deleted file mode 100644 index a5524b44de..0000000000 --- a/src/Umbraco.Infrastructure/Persistence/BasicBulkSqlInsertProvider.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Infrastructure.Persistence -{ - /// - /// A provider that just generates insert commands - /// - public class BasicBulkSqlInsertProvider : IBulkSqlInsertProvider - { - public string ProviderName => Cms.Core.Constants.DatabaseProviders.SqlServer; - - public int BulkInsertRecords(IUmbracoDatabase database, IEnumerable records) - { - if (!records.Any()) return 0; - - return BulkInsertRecordsWithCommands(database, records.ToArray()); - } - - /// - /// Bulk-insert records using commands. - /// - /// The type of the records. - /// The database. - /// The records. - /// The number of records that were inserted. - internal static int BulkInsertRecordsWithCommands(IUmbracoDatabase database, T[] records) - { - foreach (var command in database.GenerateBulkInsertCommands(records)) - command.ExecuteNonQuery(); - - return records.Length; // what else? - } - } -} diff --git a/src/Umbraco.Infrastructure/Persistence/CustomConnectionStringDatabaseProviderMetadata.cs b/src/Umbraco.Infrastructure/Persistence/CustomConnectionStringDatabaseProviderMetadata.cs new file mode 100644 index 0000000000..c9984d69ea --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/CustomConnectionStringDatabaseProviderMetadata.cs @@ -0,0 +1,55 @@ +using System; +using System.Runtime.Serialization; +using Umbraco.Cms.Core.Install.Models; + +namespace Umbraco.Cms.Infrastructure.Persistence; + +/// +/// Provider metadata for custom connection string setup. +/// +[DataContract] +public class CustomConnectionStringDatabaseProviderMetadata : IDatabaseProviderMetadata +{ + /// + public Guid Id => new("42c0eafd-1650-4bdb-8cf6-d226e8941698"); + + /// + public int SortOrder => int.MaxValue; + + /// + public string DisplayName => "Custom"; + + /// + public string DefaultDatabaseName => string.Empty; + + /// + public string ProviderName => null; + + /// + public bool SupportsQuickInstall => false; + + /// + public bool IsAvailable => true; + + /// + public bool RequiresServer => false; + + /// + public string ServerPlaceholder => null; + + /// + public bool RequiresCredentials => false; + + /// + public bool SupportsIntegratedAuthentication => false; + + /// + public bool RequiresConnectionTest => true; + + /// + public bool ForceCreateDatabase => false; + + /// + public string GenerateConnectionString(DatabaseModel databaseModel) + => databaseModel.ConnectionString; +} diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs index 34ad767b04..05e1c3722f 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs @@ -9,7 +9,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions { - internal static class DefinitionFactory + public static class DefinitionFactory { public static TableDefinition GetTableDefinition(Type modelType, ISqlSyntaxProvider sqlSyntax) { diff --git a/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs b/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs index e54c1f5fbc..6cdd319ef8 100644 --- a/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs +++ b/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs @@ -10,6 +10,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence public class DbProviderFactoryCreator : IDbProviderFactoryCreator { private readonly Func _getFactory; + private readonly IEnumerable _providerSpecificInterceptors; private readonly IDictionary _databaseCreators; private readonly IDictionary _syntaxProviders; private readonly IDictionary _bulkSqlInsertProviders; @@ -20,9 +21,12 @@ namespace Umbraco.Cms.Infrastructure.Persistence IEnumerable syntaxProviders, IEnumerable bulkSqlInsertProviders, IEnumerable databaseCreators, - IEnumerable providerSpecificMapperFactories) + 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); @@ -50,10 +54,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence public IBulkSqlInsertProvider CreateBulkSqlInsertProvider(string providerName) { - if (!_bulkSqlInsertProviders.TryGetValue(providerName, out var result)) { - return new BasicBulkSqlInsertProvider(); + throw new InvalidOperationException($"Unknown provider name \"{providerName}\""); } return result; @@ -76,5 +79,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence return new NPocoMapperCollection(() => Enumerable.Empty()); } + + public IEnumerable GetProviderSpecificInterceptors(string providerName) + => _providerSpecificInterceptors.Where(x => x.ProviderName == providerName); } } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryDbConnection.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryDbConnection.cs index cfd078b81c..956137fbcc 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryDbConnection.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryDbConnection.cs @@ -5,7 +5,7 @@ using Transaction = System.Transactions.Transaction; namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling { - class RetryDbConnection : DbConnection + public class RetryDbConnection : DbConnection { private DbConnection _inner; private readonly RetryPolicy _conRetryPolicy; diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryPolicyFactory.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryPolicyFactory.cs index a88ae39982..6a4e257af7 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryPolicyFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryPolicyFactory.cs @@ -2,6 +2,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling { + // TODO: These should move to Persistence.SqlServer + /// /// Provides a factory class for instantiating application-specific retry policies. /// diff --git a/src/Umbraco.Infrastructure/Persistence/IDatabaseProviderMetadata.cs b/src/Umbraco.Infrastructure/Persistence/IDatabaseProviderMetadata.cs new file mode 100644 index 0000000000..41baa9c4f8 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/IDatabaseProviderMetadata.cs @@ -0,0 +1,90 @@ +using System; +using System.Runtime.Serialization; +using Umbraco.Cms.Core.Install.Models; + +namespace Umbraco.Cms.Infrastructure.Persistence; + +public interface IDatabaseProviderMetadata +{ + /// + /// 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. + /// + [DataMember(Name = "sortOrder")] + int SortOrder { get; } + + /// + /// Gets a friendly name to describe the provider. + /// + [DataMember(Name = "displayName")] + string DisplayName { get; } + + /// + /// Gets the default database name for the provider. + /// + [DataMember(Name = "defaultDatabaseName")] + string DefaultDatabaseName { get; } + + /// + /// Gets the database factory provider name. + /// + [DataMember(Name = "providerName")] + string ProviderName { get; } + + /// + /// 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. + /// + [DataMember(Name = "isAvailable")] + bool IsAvailable { get; } + + /// + /// 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. + /// + [DataMember(Name = "serverPlaceholder")] + string ServerPlaceholder { get; } + + /// + /// 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). + /// + [DataMember(Name = "supportsIntegratedAuthentication")] + bool SupportsIntegratedAuthentication { get; } + + /// + /// 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 + /// + public bool ForceCreateDatabase { get; } + + /// + /// 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 6a38dc3c06..d1ebcb8caa 100644 --- a/src/Umbraco.Infrastructure/Persistence/IDbProviderFactoryCreator.cs +++ b/src/Umbraco.Infrastructure/Persistence/IDbProviderFactoryCreator.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Data.Common; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; @@ -11,5 +12,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence IBulkSqlInsertProvider CreateBulkSqlInsertProvider(string providerName); void CreateDatabase(string providerName, string connectionString); NPocoMapperCollection ProviderSpecificMappers(string providerName); + IEnumerable GetProviderSpecificInterceptors(string providerName); } } diff --git a/src/Umbraco.Infrastructure/Persistence/IProviderSpecificInterceptor.cs b/src/Umbraco.Infrastructure/Persistence/IProviderSpecificInterceptor.cs new file mode 100644 index 0000000000..736ba80854 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/IProviderSpecificInterceptor.cs @@ -0,0 +1,28 @@ +using NPoco; + +namespace Umbraco.Cms.Infrastructure.Persistence; + +public interface IProviderSpecificInterceptor : IInterceptor +{ + string ProviderName { get; } +} + +public interface IProviderSpecificExecutingInterceptor : IProviderSpecificInterceptor, IExecutingInterceptor +{ +} + +public interface IProviderSpecificConnectionInterceptor : IProviderSpecificInterceptor, IConnectionInterceptor +{ +} + +public interface IProviderSpecificExceptionInterceptor : IProviderSpecificInterceptor, IExceptionInterceptor +{ +} + +public interface IProviderSpecificDataInterceptor : IProviderSpecificInterceptor, IDataInterceptor +{ +} + +public interface IProviderSpecificTransactionInterceptor: IProviderSpecificInterceptor, ITransactionInterceptor +{ +} diff --git a/src/Umbraco.Infrastructure/Persistence/IScalarMapper.cs b/src/Umbraco.Infrastructure/Persistence/IScalarMapper.cs new file mode 100644 index 0000000000..29f1128a44 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/IScalarMapper.cs @@ -0,0 +1,13 @@ +namespace Umbraco.Cms.Infrastructure.Persistence; + +/// +/// Provides a mapping function for +/// +public interface IScalarMapper +{ + /// + /// Performs a mapping operation for a scalar value. + /// + object Map(object value); +} + diff --git a/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabaseFactory.cs b/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabaseFactory.cs index 5af76d7220..2a171a021e 100644 --- a/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabaseFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabaseFactory.cs @@ -1,4 +1,5 @@ using System; +using Umbraco.Cms.Core.Configuration.Models; namespace Umbraco.Cms.Infrastructure.Persistence { @@ -51,7 +52,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence /// /// Configures the database factory. /// - void Configure(string connectionString, string providerName); + void Configure(ConnectionStrings umbracoConnectionString); /// /// Gets the . diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseTypeExtensions.cs b/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseTypeExtensions.cs index d692656f0f..b349824591 100644 --- a/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseTypeExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseTypeExtensions.cs @@ -1,35 +1,19 @@ -using NPoco; +using System; +using NPoco; namespace Umbraco.Cms.Infrastructure.Persistence { internal static class NPocoDatabaseTypeExtensions { - public static bool IsSqlServer(this DatabaseType databaseType) - { + [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. - return databaseType is NPoco.DatabaseTypes.SqlServerDatabaseType; - } + databaseType is NPoco.DatabaseTypes.SqlServerDatabaseType; - public static bool IsSqlServer2008OrLater(this DatabaseType databaseType) - { - return databaseType is NPoco.DatabaseTypes.SqlServer2008DatabaseType; - } - - public static bool IsSqlServer2012OrLater(this DatabaseType databaseType) - { - return databaseType is NPoco.DatabaseTypes.SqlServer2012DatabaseType; - } - - public static bool IsSqlCe(this DatabaseType databaseType) - { - return databaseType is NPoco.DatabaseTypes.SqlServerCEDatabaseType; - } - - public static bool IsSqlServerOrCe(this DatabaseType databaseType) - { - return databaseType.IsSqlServer() || databaseType.IsSqlCe(); - } + [Obsolete("Usage of this method indicates a code smell.")] + public static bool IsSqlite(this DatabaseType databaseType) + => databaseType is NPoco.DatabaseTypes.SQLiteDatabaseType; } } diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs b/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs index 6b7c34dc15..d87d138507 100644 --- a/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs @@ -431,20 +431,11 @@ namespace Umbraco.Extensions /// 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) - { - 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 nestedSql = new Sql(sql.SqlContext); - nestedSql = nestedJoin(nestedSql); - - var sqlJoin = sql.LeftJoin(join); - sql.Append(nestedSql); - return sqlJoin; - } + public static Sql.SqlJoinClause LeftJoin( + this Sql sql, + Func, Sql> nestedJoin, + string alias = null) => + sql.SqlContext.SqlSyntax.LeftJoinWithNestedJoin(sql, nestedJoin, alias); /// /// Appends a RIGHT JOIN clause to the Sql statement. @@ -922,6 +913,15 @@ namespace Umbraco.Extensions return string.Join(", ", sql.GetColumns(columnExpressions: fields, withAlias: false)); } + /// + /// 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)); + } + /// /// Gets fields for a Dto. /// @@ -954,7 +954,8 @@ namespace Umbraco.Extensions var type = typeof(TDto); var tableName = type.GetTableName(); - sql.Append($"DELETE {sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName)}"); + // FROM optional SQL server, but not elsewhere. + sql.Append($"DELETE FROM {sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName)}"); return sql; } @@ -1023,7 +1024,7 @@ namespace Umbraco.Extensions public SqlUpd Set(Expression> fieldSelector, object value) { - var fieldName = _sqlContext.SqlSyntax.GetFieldName(fieldSelector); + var fieldName = _sqlContext.SqlSyntax.GetFieldNameForUpdate(fieldSelector); _setExpressions.Add(new Tuple(fieldName, value)); return this; } @@ -1040,62 +1041,14 @@ namespace Umbraco.Extensions /// /// The Sql statement. /// The Sql statement. - /// NOTE: This method will not work for all queries, only simple ones! + /// + /// NOTE: This method will not work for all queries, only simple ones! + /// public static Sql ForUpdate(this Sql sql) - { - // go find the first FROM clause, and append the lock hint - Sql s = sql; - var updated = false; + => sql.SqlContext.SqlSyntax.InsertForUpdateHint(sql); - while (s != null) - { - var sqlText = SqlInspector.GetSqlText(s); - if (sqlText.StartsWith("FROM ", StringComparison.OrdinalIgnoreCase)) - { - SqlInspector.SetSqlText(s, sqlText + " WITH (UPDLOCK)"); - updated = true; - break; - } - - s = SqlInspector.GetSqlRhs(sql); - } - - if (updated) - SqlInspector.Reset(sql); - - return sql; - } - - #endregion - - #region Sql Inspection - - private static SqlInspectionUtilities _sqlInspector; - - private static SqlInspectionUtilities SqlInspector => _sqlInspector ?? (_sqlInspector = new SqlInspectionUtilities()); - - private class SqlInspectionUtilities - { - private readonly Func _getSqlText; - private readonly Action _setSqlText; - private readonly Func _getSqlRhs; - private readonly Action _setSqlFinal; - - public SqlInspectionUtilities() - { - (_getSqlText, _setSqlText) = ReflectionUtilities.EmitFieldGetterAndSetter("_sql"); - _getSqlRhs = ReflectionUtilities.EmitFieldGetter("_rhs"); - _setSqlFinal = ReflectionUtilities.EmitFieldSetter("_sqlFinal"); - } - - public string GetSqlText(Sql sql) => _getSqlText(sql); - - public void SetSqlText(Sql sql, string sqlText) => _setSqlText(sql, sqlText); - - public Sql GetSqlRhs(Sql sql) => _getSqlRhs(sql); - - public void Reset(Sql sql) => _setSqlFinal(sql, null); - } + public static Sql AppendForUpdateHint(this Sql sql) + => sql.SqlContext.SqlSyntax.AppendForUpdateHint(sql); #endregion @@ -1120,7 +1073,7 @@ namespace Umbraco.Extensions #region Utilities - private static string[] GetColumns(this Sql sql, string tableAlias = null, string referenceName = null, Expression>[] columnExpressions = null, bool withAlias = true) + 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; @@ -1160,24 +1113,11 @@ namespace Umbraco.Extensions } return queryColumns - .Select(x => GetColumn(sql.SqlContext.DatabaseType, tableName, x.Value.ColumnName, GetAlias(x.Value), referenceName)) + .Select(x => sql.SqlContext.SqlSyntax.GetColumn(sql.SqlContext.DatabaseType, tableName, x.Value.ColumnName, GetAlias(x.Value), referenceName, forInsert: forInsert)) .ToArray(); } - private static string GetColumn(DatabaseType dbType, string tableName, string columnName, string columnAlias, string referenceName = null) - { - tableName = dbType.EscapeTableName(tableName); - columnName = dbType.EscapeSqlIdentifier(columnName); - var column = tableName + "." + columnName; - if (columnAlias == null) return column; - - referenceName = referenceName == null ? string.Empty : referenceName + "__"; - columnAlias = dbType.EscapeSqlIdentifier(referenceName + columnAlias); - column += " AS " + columnAlias; - return column; - } - - private static string GetTableName(this Type type) + 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 diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs index 56fd356011..bbbabe5578 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs @@ -92,8 +92,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement var sqlClause = Sql() .SelectAll() - .From() - .RightJoin() + .From() + .LeftJoin() .On(left => left.Id, right => right.PropertyTypeGroupId) .InnerJoin() .On(left => left.DataTypeId, right => right.NodeId); diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index 5ffdf4bf10..67e35bfec5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -757,7 +757,7 @@ AND umbracoNode.id <> @id", //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().Columns(x => x.VersionId, x => x.Name, x => x.UpdateUserId, x => x.UpdateDate, x => x.LanguageId); + 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() @@ -768,7 +768,7 @@ AND umbracoNode.id <> @id", Database.Execute(sqlInsert); //insert rows into the documentCultureVariation table - cols = Sql().Columns(x => x.NodeId, x => x.Edited, x => x.Published, x => x.Name, x => x.Available, x => x.LanguageId); + 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 @@ -856,7 +856,7 @@ AND umbracoNode.id <> @id", .WhereNull(x => x.Id, "xtags") // ie, not exists .Where(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1)); - var cols = Sql().Columns(x => x.Text, x => x.Group, x => x.LanguageId); + var cols = Sql().ColumnsForInsert(x => x.Text, x => x.Group, x => x.LanguageId); var sqlInsertTags = Sql($"INSERT INTO {TagDto.TableName} ({cols})").Append(sqlSelectTagsToInsert); Database.Execute(sqlInsertTags); @@ -884,7 +884,7 @@ AND umbracoNode.id <> @id", .Where(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1)) .WhereIn(x => x.PropertyTypeId, propertyTypeIds); - var relationColumnsToInsert = Sql().Columns(x => x.NodeId, x => x.PropertyTypeId, x => x.TagId); + var relationColumnsToInsert = Sql().ColumnsForInsert(x => x.NodeId, x => x.PropertyTypeId, x => x.TagId); var sqlInsertRelations = Sql($"INSERT INTO {TagRelationshipDto.TableName} ({relationColumnsToInsert})").Append(sqlSelectRelationsToInsert); Database.Execute(sqlInsertRelations); @@ -972,7 +972,7 @@ AND umbracoNode.id <> @id", //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().Columns(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 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(); diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs index 438d5c1a1a..9e81af61e7 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs @@ -136,7 +136,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement .Delete() .Where(x => x.Id == id); - _umbracoDatabase.Delete(query); + _umbracoDatabase.Execute(query); } public bool SavePackage(PackageDefinition definition) @@ -167,10 +167,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement }; // Set the ids, we have to save in database first to get the Id - definition.PackageId = dto.PackageId; - var result = _umbracoDatabase.Insert(dto); - var decimalResult = result.SafeCast(); - definition.Id = decimal.ToInt32(decimalResult); + _umbracoDatabase.Insert(dto); + definition.Id = dto.Id; } // Save snapshot locally, we do this to the updated packagePath diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs index 965c85b6e6..5f562c1c12 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs @@ -356,7 +356,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement .On((node, dcv, lang) => node.NodeId == dcv.NodeId && lang.Id == dcv.LanguageId, aliasRight: "dcv") // for selected nodes - .WhereIn(x => x.NodeId, ids); + .WhereIn(x => x.NodeId, ids) + .OrderBy(x => x.Id); } // gets the full sql for a given object type and a given unique id diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs index 955cbf5d5d..8e8640c5e5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs @@ -296,7 +296,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement => forUpdate ? Sql() .Select(r => r.Select(x => x.ExternalLoginDto)) .From() - .Append(" WITH (UPDLOCK)") // ensure these table values are locked for updates, the ForUpdate ext method does not work here + .AppendForUpdateHint() // ensure these table values are locked for updates, the ForUpdate ext method does not work here .InnerJoin() .On(x => x.ExternalLoginId, x => x.Id) : Sql() diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs index 733a85b55b..e16f850a17 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs @@ -365,8 +365,11 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement public void DeleteByParent(int parentId, params string[] relationTypeAliases) { - if (Database.DatabaseType.IsSqlCe()) + // HACK: SQLite - hard to replace this without provider specific repositories/another ORM. + if (Database.DatabaseType.IsSqlite()) { + var query = Sql().Append(@"delete from umbracoRelation"); + var subQuery = Sql().Select(x => x.Id) .From() .InnerJoin().On(x => x.RelationType, x => x.Id) @@ -377,8 +380,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement subQuery.WhereIn(x => x.Alias, relationTypeAliases); } - Database.Execute(Sql().Delete().WhereIn(x => x.Id, subQuery)); + var fullQuery = query.WhereIn(x => x.Id, subQuery); + Database.Execute(fullQuery); } else { diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs index 81e6a82f89..781b9f3cab 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs @@ -210,7 +210,16 @@ WHERE r.tagId IS NULL"; sql.Append(" UNION "); } - sql.Append("SELECT N'"); + // 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)); diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs index 0d33105c4b..d92f0903f1 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs @@ -76,28 +76,10 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // Gets the descendants of the parent node - Sql subQuery; - - if (_scopeAccessor.AmbientScope.Database.DatabaseType.IsSqlCe()) - { - // SqlCE do not support nested selects that returns a scalar. So we need to do this in multiple queries - - var pathForLike = _scopeAccessor.AmbientScope.Database.ExecuteScalar(subsubQuery); - - subQuery = _scopeAccessor.AmbientScope.Database.SqlContext.Sql() - .Select(x => x.NodeId) - .From() - .WhereLike(x => x.Path, pathForLike); - } - else - { - subQuery = _scopeAccessor.AmbientScope.Database.SqlContext.Sql() - .Select(x => x.NodeId) - .From() - .WhereLike(x => x.Path, subsubQuery); - } - - + 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().Select( diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs index a9cba6b2ae..86ead6512e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs @@ -225,10 +225,10 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0 // that query is going to run a *lot*, make it a template var t = SqlContext.Templates.Get("Umbraco.Core.UserRepository.ValidateLoginSession", s => s .Select() - .SelectTop(1) .From() .Where(x => x.SessionId == SqlTemplate.Arg("sessionId")) - .ForUpdate()); + .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. var sql = t.Sql(sessionId); diff --git a/src/Umbraco.Infrastructure/Persistence/ScalarMapper.cs b/src/Umbraco.Infrastructure/Persistence/ScalarMapper.cs new file mode 100644 index 0000000000..9b6eada924 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/ScalarMapper.cs @@ -0,0 +1,14 @@ +using System; + +namespace Umbraco.Cms.Infrastructure.Persistence; + +public abstract class ScalarMapper : IScalarMapper +{ + /// + /// 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/SqlServerDbProviderFactoryCreator.cs b/src/Umbraco.Infrastructure/Persistence/SqlServerDbProviderFactoryCreator.cs deleted file mode 100644 index c16e5a75ab..0000000000 --- a/src/Umbraco.Infrastructure/Persistence/SqlServerDbProviderFactoryCreator.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Data.Common; -using System.Linq; -using Microsoft.Extensions.Options; -using NPoco; -using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; - -namespace Umbraco.Cms.Infrastructure.Persistence -{ - [Obsolete("This is only used for integration tests and should be moved into a test project.")] - public class SqlServerDbProviderFactoryCreator : IDbProviderFactoryCreator - { - private readonly Func _getFactory; - private readonly IOptions _globalSettings; - - public SqlServerDbProviderFactoryCreator(Func getFactory, IOptions globalSettings) - { - _getFactory = getFactory; - _globalSettings = globalSettings; - } - - public DbProviderFactory CreateFactory(string providerName) - { - if (string.IsNullOrEmpty(providerName)) return null; - - return _getFactory(providerName); - } - - // gets the sql syntax provider that corresponds, from attribute - public ISqlSyntaxProvider GetSqlSyntaxProvider(string providerName) - => providerName switch - { - Cms.Core.Constants.DbProviderNames.SqlCe => throw new NotSupportedException("SqlCe is not supported"), - Cms.Core.Constants.DbProviderNames.SqlServer => new SqlServerSyntaxProvider(_globalSettings), - _ => throw new InvalidOperationException($"Unknown provider name \"{providerName}\""), - }; - - public IBulkSqlInsertProvider CreateBulkSqlInsertProvider(string providerName) - => providerName switch - { - Cms.Core.Constants.DbProviderNames.SqlCe => throw new NotSupportedException("SqlCe is not supported"), - Cms.Core.Constants.DbProviderNames.SqlServer => new SqlServerBulkSqlInsertProvider(), - _ => new BasicBulkSqlInsertProvider(), - }; - - public void CreateDatabase(string providerName, string connectionString) - => throw new NotSupportedException("Embedded databases are not supported"); // TODO But LocalDB is? - - public NPocoMapperCollection ProviderSpecificMappers(string providerName) - => new NPocoMapperCollection(() => Enumerable.Empty()); - } -} diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ColumnInfo.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ColumnInfo.cs index 46d21e5894..afe4a896ac 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ColumnInfo.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ColumnInfo.cs @@ -21,6 +21,15 @@ 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 string TableName { get; set; } public string ColumnName { get; set; } public int Ordinal { get; set; } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs index b9890d85d6..960c085eda 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs @@ -1,12 +1,12 @@ using System; using System.Collections.Generic; using System.Data; +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; -using Umbraco.Cms.Infrastructure.Persistence.Querying; namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax { @@ -15,6 +15,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax /// public interface ISqlSyntaxProvider { + DatabaseType GetUpdatedDatabaseType(DatabaseType current, string connectionString); + string ProviderName { get; } string EscapeString(string val); @@ -24,6 +26,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax 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); @@ -64,6 +68,10 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax 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. /// @@ -126,16 +134,33 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax /// The constraint name. /// A value indicating whether a default constraint was found. /// - /// Some database engines (e.g. SqlCe) may not have names for default constraints, + /// 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, out string constraintName); - void ReadLock(IDatabase db, TimeSpan timeout, int lockId); - void WriteLock(IDatabase db, TimeSpan timeout, int lockId); - void ReadLock(IDatabase db, params int[] lockIds); - void WriteLock(IDatabase db, params int[] lockIds); + 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 { get; } } } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index 5dbf97021d..a6671034d1 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Data; using System.Globalization; using System.Linq; +using System.Linq.Expressions; using System.Text; using System.Text.RegularExpressions; using NPoco; @@ -60,7 +61,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax public string GetWildcardPlaceholder() => "%"; public string StringLengthNonUnicodeColumnDefinitionFormat { get; } = "VARCHAR({0})"; - public string StringLengthUnicodeColumnDefinitionFormat { get; } = "NVARCHAR({0})"; + public virtual string StringLengthUnicodeColumnDefinitionFormat { get; } = "NVARCHAR({0})"; public string DecimalColumnDefinitionFormat { get; } = "DECIMAL({0},{1})"; public string DefaultValueFormat { get; } = "DEFAULT ({0})"; @@ -69,7 +70,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax public int DefaultDecimalScale { get; } = 9; //Set by Constructor - public string StringColumnDefinition { get; } + public virtual string StringColumnDefinition { get; } public string StringLengthColumnDefinitionFormat { get; } public string AutoIncrementDefinition { get; protected set; } = "AUTOINCREMENT"; @@ -137,6 +138,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax return dbTypeMap.Create(); } + public virtual DatabaseType GetUpdatedDatabaseType(DatabaseType current, string connectionString) => current; + public abstract string ProviderName { get; } public virtual string EscapeString(string val) @@ -216,6 +219,22 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax return "NVARCHAR"; } + public virtual string GetSpecialDbType(SpecialDbType dbType, int customSize) => $"{GetSpecialDbType(dbType)}({customSize})"; + + 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 abstract IsolationLevel DefaultIsolationLevel { get; } public abstract string DbProvider { get; } @@ -243,16 +262,18 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax public abstract bool TryGetDefaultConstraint(IDatabase db, string tableName, string columnName, out string constraintName); - public abstract void ReadLock(IDatabase db, params int[] lockIds); - public abstract void WriteLock(IDatabase db, params int[] lockIds); - public abstract void ReadLock(IDatabase db, TimeSpan timeout, int lockId); + public virtual string GetFieldNameForUpdate(Expression> fieldSelector, string tableAlias = null) => this.GetFieldName(fieldSelector, tableAlias); - public abstract void WriteLock(IDatabase db, TimeSpan timeout, int lockId); + public virtual Sql InsertForUpdateHint(Sql sql) => sql; - public virtual bool DoesTableExist(IDatabase db, string tableName) - { - return false; - } + public virtual Sql AppendForUpdateHint(Sql sql) => sql; + + public abstract Sql.SqlJoinClause LeftJoinWithNestedJoin(Sql sql, Func, Sql> nestedJoin, string alias = null); + + + public virtual IDictionary ScalarMappers => null; + + public virtual bool DoesTableExist(IDatabase db, string tableName) => GetTablesInSchema(db).Contains(tableName); public virtual bool SupportsClustered() { @@ -479,7 +500,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax { if (column.Size != default) { - return $"{GetSpecialDbType(column.CustomDbType.Value)}({column.Size})"; + return GetSpecialDbType(column.CustomDbType.Value, column.Size); } return GetSpecialDbType(column.CustomDbType.Value); @@ -554,6 +575,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax public abstract Sql SelectTop(Sql sql, int top); + public abstract void HandleCreateTable(IDatabase database, TableDefinition tableDefinition, bool skipKeysAndIndexes = false); + public virtual string DeleteDefaultConstraint => throw new NotSupportedException("Default constraints are not supported"); public virtual string CreateTable => "CREATE TABLE {0} ({1})"; diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs index 9a28686ced..6c816f7c92 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs @@ -27,7 +27,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax /// /// 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: SQLCE and MSSQL + /// needs to be used to work for all servers /// /// /// diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntaxExtensions.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntaxExtensions.cs index 93978cee8b..53cccd1fb9 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntaxExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntaxExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq.Expressions; using System.Reflection; using NPoco; @@ -12,15 +12,6 @@ namespace Umbraco.Extensions /// public static class SqlSyntaxExtensions { - private 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... - var attr = type.FirstAttribute(); - return string.IsNullOrWhiteSpace(attr?.Value) ? string.Empty : attr.Value; - } - private static string GetColumnName(this PropertyInfo column) { var attr = column.FirstAttribute(); diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs index 80970ec637..1b237adcf5 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs @@ -27,8 +27,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence private readonly ILogger _logger; private readonly IBulkSqlInsertProvider _bulkSqlInsertProvider; private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory; - private readonly RetryPolicy _connectionRetryPolicy; - private readonly RetryPolicy _commandRetryPolicy; private readonly IEnumerable _mapperCollection; private readonly Guid _instanceGuid = Guid.NewGuid(); private List _commands; @@ -49,8 +47,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence ILogger logger, IBulkSqlInsertProvider bulkSqlInsertProvider, DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, - RetryPolicy connectionRetryPolicy = null, - RetryPolicy commandRetryPolicy = null, IEnumerable mapperCollection = null) : base(connectionString, sqlContext.DatabaseType, provider, sqlContext.SqlSyntax.DefaultIsolationLevel) { @@ -58,8 +54,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence _logger = logger; _bulkSqlInsertProvider = bulkSqlInsertProvider; _databaseSchemaCreatorFactory = databaseSchemaCreatorFactory; - _connectionRetryPolicy = connectionRetryPolicy; - _commandRetryPolicy = commandRetryPolicy; _mapperCollection = mapperCollection; Init(); @@ -99,27 +93,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence /// public ISqlContext SqlContext { get; } - #region Temp - - // work around NPoco issue https://github.com/schotime/NPoco/issues/517 while we wait for the fix - public override DbCommand CreateCommand(DbConnection connection, CommandType commandType, string sql, params object[] args) - { - var command = base.CreateCommand(connection, commandType, sql, args); - - if (!DatabaseType.IsSqlCe()) - return command; - - foreach (DbParameter parameter in command.Parameters) - { - if (parameter.Value == DBNull.Value) - parameter.DbType = DbType.String; - } - - return command; - } - - #endregion - #region Testing, Debugging and Troubleshooting private bool _enableCount; @@ -218,12 +191,14 @@ namespace Umbraco.Cms.Infrastructure.Persistence #region OnSomething - // TODO: has new interceptors to replace OnSomething? - protected override DbConnection OnConnectionOpened(DbConnection connection) { - if (connection == null) throw new ArgumentNullException(nameof(connection)); + if (connection == null) + { + throw new ArgumentNullException(nameof(connection)); + } + // TODO: this should probably move to a SQL Server ProviderSpecificInterceptor. #if DEBUG_DATABASES // determines the database connection SPID for debugging if (DatabaseType.IsSqlServer()) @@ -239,15 +214,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence // includes SqlCE _spid = 0; } + #endif - - // wrap the connection with a profiling connection that tracks timings - connection = new StackExchange.Profiling.Data.ProfiledDbConnection(connection, MiniProfiler.Current); - - // wrap the connection with a retrying connection - if (_connectionRetryPolicy != null || _commandRetryPolicy != null) - connection = new RetryDbConnection(connection, _connectionRetryPolicy, _commandRetryPolicy); - return connection; } @@ -354,5 +322,33 @@ namespace Umbraco.Cms.Infrastructure.Persistence public DbType DbType { get; } public int Size { get; } } + + /// + public new T ExecuteScalar(string sql, params object[] args) + => ExecuteScalar(new Sql(sql, args)); + + /// + public new T ExecuteScalar(Sql sql) + => ExecuteScalar(sql.SQL, CommandType.Text, sql.Arguments); + + /// + /// + /// 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); + } + + 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/UmbracoDatabaseFactory.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs index 6093c06a97..24bf94ed04 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs @@ -43,17 +43,16 @@ namespace Umbraco.Cms.Infrastructure.Persistence private DatabaseFactory _npocoDatabaseFactory; private IPocoDataFactory _pocoDataFactory; - private string _providerName; private DatabaseType _databaseType; private ISqlSyntaxProvider _sqlSyntax; private IBulkSqlInsertProvider _bulkSqlInsertProvider; - private RetryPolicy _connectionRetryPolicy; - private RetryPolicy _commandRetryPolicy; private NPoco.MapperCollection _pocoMappers; private SqlContext _sqlContext; private bool _upgrading; private bool _initialized; + private ConnectionStrings _umbracoConnectionString; + private DbProviderFactory _dbProviderFactory = null; private DbProviderFactory DbProviderFactory @@ -62,9 +61,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence { if (_dbProviderFactory == null) { - _dbProviderFactory = string.IsNullOrWhiteSpace(_providerName) + _dbProviderFactory = string.IsNullOrWhiteSpace(ProviderName) ? null - : _dbProviderFactoryCreator.CreateFactory(_providerName); + : _dbProviderFactoryCreator.CreateFactory(ProviderName); } return _dbProviderFactory; @@ -74,45 +73,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence #region Constructors - /// - /// Initializes a new instance of the . - /// - /// Used by the other ctor and in tests. - internal UmbracoDatabaseFactory( - ILogger logger, - ILoggerFactory loggerFactory, - IOptions globalSettings, - IMapperCollection mappers, - IDbProviderFactoryCreator dbProviderFactoryCreator, - DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, - NPocoMapperCollection npocoMappers, - string connectionString) - { - _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; - if (connectionString is null) - { - logger.LogDebug("Missing connection string, defer configuration."); - return; // not configured - } - - var configConnectionString = new ConfigConnectionString("Custom", connectionString); - // could as well be - // so need to test the values too - if (configConnectionString.IsConnectionStringConfigured() == false) - { - logger.LogDebug("Empty connection string or provider name, defer configuration."); - return; // not configured - } - - Configure(configConnectionString.ConnectionString, configConnectionString.ProviderName); - } /// /// Initializes a new instance of the . @@ -126,10 +87,24 @@ namespace Umbraco.Cms.Infrastructure.Persistence IMapperCollection mappers, IDbProviderFactoryCreator dbProviderFactoryCreator, DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, - NPocoMapperCollection npocoMappers): - this(logger, loggerFactory, globalSettings, mappers, dbProviderFactoryCreator, databaseSchemaCreatorFactory, npocoMappers, connectionStrings?.CurrentValue?.UmbracoConnectionString?.ConnectionString) + NPocoMapperCollection npocoMappers) { + _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()) + { + logger.LogDebug("Missing connection string, defer configuration."); + return; // not configured + } + + Configure(umbracoConnectionString); } #endregion @@ -141,7 +116,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence { lock (_lock) { - return !ConnectionString.IsNullOrWhiteSpace() && !_providerName.IsNullOrWhiteSpace(); + return !ConnectionString.IsNullOrWhiteSpace() && !ProviderName.IsNullOrWhiteSpace(); } } } @@ -150,52 +125,17 @@ namespace Umbraco.Cms.Infrastructure.Persistence public bool Initialized => Volatile.Read(ref _initialized); /// - public string ConnectionString { get; private set; } + public string ConnectionString => _umbracoConnectionString?.ConnectionString; /// - public string ProviderName => _providerName; + public string ProviderName => _umbracoConnectionString?.ProviderName; /// public bool CanConnect => // actually tries to connect to the database (regardless of configured/initialized) - !ConnectionString.IsNullOrWhiteSpace() && !_providerName.IsNullOrWhiteSpace() && + !ConnectionString.IsNullOrWhiteSpace() && !ProviderName.IsNullOrWhiteSpace() && DbConnectionExtensions.IsConnectionAvailable(ConnectionString, DbProviderFactory); - private void UpdateSqlServerDatabaseType() - { - // replace NPoco database type by a more efficient one - var setting = _globalSettings.Value.DatabaseFactoryServerVersion; - var fromSettings = false; - - if (setting.IsNullOrWhiteSpace() || !setting.StartsWith("SqlServer.") - || !Enum.TryParse(setting.Substring("SqlServer.".Length), out var versionName, true)) - { - versionName = ((SqlServerSyntaxProvider) _sqlSyntax).GetSetVersion(ConnectionString, _providerName, _logger).ProductVersionName; - } - else - { - fromSettings = true; - } - - switch (versionName) - { - case SqlServerSyntaxProvider.VersionName.V2008: - _databaseType = DatabaseType.SqlServer2008; - break; - case SqlServerSyntaxProvider.VersionName.V2012: - case SqlServerSyntaxProvider.VersionName.V2014: - case SqlServerSyntaxProvider.VersionName.V2016: - case SqlServerSyntaxProvider.VersionName.V2017: - case SqlServerSyntaxProvider.VersionName.V2019: - _databaseType = DatabaseType.SqlServer2012; - break; - // else leave unchanged - } - - _logger.LogDebug("SqlServer {SqlServerVersion}, DatabaseType is {DatabaseType} ({Source}).", - versionName, _databaseType, fromSettings ? "settings" : "detected"); - } - /// public ISqlContext SqlContext { @@ -224,18 +164,21 @@ namespace Umbraco.Cms.Infrastructure.Persistence public void ConfigureForUpgrade() => _upgrading = true; /// - public void Configure(string connectionString, string providerName) + public void Configure(ConnectionStrings umbracoConnectionString) { - if (connectionString.IsNullOrWhiteSpace()) throw new ArgumentNullException(nameof(connectionString)); - if (providerName.IsNullOrWhiteSpace()) throw new ArgumentNullException(nameof(providerName)); + if (umbracoConnectionString is null) + { + throw new ArgumentNullException(nameof(umbracoConnectionString)); + } lock (_lock) { if (Volatile.Read(ref _initialized)) + { throw new InvalidOperationException("Already initialized."); + } - ConnectionString = connectionString; - _providerName = providerName; + _umbracoConnectionString = umbracoConnectionString; } // rest to be lazy-initialized @@ -252,37 +195,31 @@ namespace Umbraco.Cms.Infrastructure.Persistence throw new InvalidOperationException("The factory has not been configured with a proper connection string."); } - if (_providerName.IsNullOrWhiteSpace()) + if (ProviderName.IsNullOrWhiteSpace()) { throw new InvalidOperationException("The factory has not been configured with a proper provider name."); } if (DbProviderFactory == null) { - throw new Exception($"Can't find a provider factory for provider name \"{_providerName}\"."); + throw new Exception($"Can't find a provider factory for provider name \"{ProviderName}\"."); } - _connectionRetryPolicy = RetryPolicyFactory.GetDefaultSqlConnectionRetryPolicyByConnectionString(ConnectionString); - _commandRetryPolicy = RetryPolicyFactory.GetDefaultSqlCommandRetryPolicyByConnectionString(ConnectionString); - - _databaseType = DatabaseType.Resolve(DbProviderFactory.GetType().Name, _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}\"."); + throw new Exception($"Can't find an NPoco database type for provider name \"{ProviderName}\"."); } - _sqlSyntax = _dbProviderFactoryCreator.GetSqlSyntaxProvider(_providerName); + _sqlSyntax = _dbProviderFactoryCreator.GetSqlSyntaxProvider(ProviderName); if (_sqlSyntax == null) { - throw new Exception($"Can't find a sql syntax provider for provider name \"{_providerName}\"."); + throw new Exception($"Can't find a sql syntax provider for provider name \"{ProviderName}\"."); } - _bulkSqlInsertProvider = _dbProviderFactoryCreator.CreateBulkSqlInsertProvider(_providerName); + _bulkSqlInsertProvider = _dbProviderFactoryCreator.CreateBulkSqlInsertProvider(ProviderName); - if (_databaseType.IsSqlServer()) - { - UpdateSqlServerDatabaseType(); - } + _databaseType = _sqlSyntax.GetUpdatedDatabaseType(_databaseType, ConnectionString); // 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 @@ -290,16 +227,23 @@ namespace Umbraco.Cms.Infrastructure.Persistence // add all registered mappers for NPoco _pocoMappers.AddRange(_npocoMappers); - _pocoMappers.AddRange(_dbProviderFactoryCreator.ProviderSpecificMappers(_providerName)); + _pocoMappers.AddRange(_dbProviderFactoryCreator.ProviderSpecificMappers(ProviderName)); var factory = new FluentPocoDataFactory(GetPocoDataFactoryResolver, _pocoMappers); _pocoDataFactory = factory; var config = new FluentConfig(xmappers => factory); // create the database factory - _npocoDatabaseFactory = DatabaseFactory.Config(x => x - .UsingDatabase(CreateDatabaseInstance) // creating UmbracoDatabase instances - .WithFluentConfig(config)); // with proper configuration + _npocoDatabaseFactory = DatabaseFactory.Config(cfg => + { + cfg.UsingDatabase(CreateDatabaseInstance) // creating UmbracoDatabase instances + .WithFluentConfig(config); // with proper configuration + + foreach (IProviderSpecificInterceptor interceptor in _dbProviderFactoryCreator.GetProviderSpecificInterceptors(ProviderName)) + { + cfg.WithInterceptor(interceptor); + } + }); if (_npocoDatabaseFactory == null) { @@ -323,8 +267,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence private InitializedPocoDataBuilder GetPocoDataFactoryResolver(Type type, IPocoDataFactory factory) => new UmbracoPocoDataBuilder(type, _pocoMappers, _upgrading).Init(); - - // method used by NPoco's UmbracoDatabaseFactory to actually create the database instance private UmbracoDatabase CreateDatabaseInstance() => new UmbracoDatabase( @@ -334,10 +276,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence _loggerFactory.CreateLogger(), _bulkSqlInsertProvider, _databaseSchemaCreatorFactory, - _connectionRetryPolicy, - _commandRetryPolicy, - _pocoMappers - ); + _pocoMappers); protected override void DisposeResources() { diff --git a/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs b/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs index c81041849a..1a7f0d3936 100644 --- a/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs +++ b/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs @@ -311,7 +311,6 @@ namespace Umbraco.Cms.Infrastructure.Runtime } private bool CanAutoInstallMissingDatabase(IUmbracoDatabaseFactory databaseFactory) - => databaseFactory.ProviderName == Constants.DatabaseProviders.SqlCe || - databaseFactory.ConnectionString?.InvariantContains("(localdb)") == true; + => databaseFactory.ConnectionString?.InvariantContains("(localdb)") == true; } } diff --git a/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs b/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs index 4fb471cbd9..7488da6a55 100644 --- a/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs @@ -4,7 +4,6 @@ using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NPoco; @@ -14,13 +13,12 @@ using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Mappers; -using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; using MapperCollection = Umbraco.Cms.Infrastructure.Persistence.Mappers.MapperCollection; namespace Umbraco.Cms.Infrastructure.Runtime { - internal class SqlMainDomLock : IMainDomLock + public class SqlMainDomLock : IMainDomLock { private readonly string _lockId; private const string MainDomKeyPrefix = "Umbraco.Core.Runtime.SqlMainDom"; @@ -29,7 +27,6 @@ namespace Umbraco.Cms.Infrastructure.Runtime private readonly IOptions _globalSettings; private readonly IUmbracoDatabase _db; private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); - private SqlServerSyntaxProvider _sqlServerSyntax; private bool _mainDomChanging = false; private readonly UmbracoDatabaseFactory _dbFactory; private bool _errorDuringAcquiring; @@ -50,17 +47,16 @@ namespace Umbraco.Cms.Infrastructure.Runtime _lockId = Guid.NewGuid().ToString(); _logger = loggerFactory.CreateLogger(); _globalSettings = globalSettings; - _sqlServerSyntax = new SqlServerSyntaxProvider(_globalSettings); _dbFactory = new UmbracoDatabaseFactory( loggerFactory.CreateLogger(), loggerFactory, _globalSettings, + connectionStrings, new MapperCollection(() => Enumerable.Empty()), dbProviderFactoryCreator, databaseSchemaCreatorFactory, - npocoMappers, - connectionStrings.CurrentValue.UmbracoConnectionString.ConnectionString); + npocoMappers); MainDomKey = MainDomKeyPrefix + "-" + mainDomKeyGenerator.GenerateKey(); } @@ -73,13 +69,6 @@ namespace Umbraco.Cms.Infrastructure.Runtime return true; } - if (!(_dbFactory.SqlContext.SqlSyntax is SqlServerSyntaxProvider sqlServerSyntaxProvider)) - { - throw new NotSupportedException("SqlMainDomLock is only supported for Sql Server"); - } - - _sqlServerSyntax = sqlServerSyntaxProvider; - _logger.LogDebug("Acquiring lock..."); var tempId = Guid.NewGuid().ToString(); @@ -99,25 +88,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime return true; } - db.BeginTransaction(IsolationLevel.ReadCommitted); - - try - { - // wait to get a write lock - _sqlServerSyntax.WriteLock(db, TimeSpan.FromMilliseconds(millisecondsTimeout), Cms.Core.Constants.Locks.MainDom); - } - catch(SqlException ex) - { - if (IsLockTimeoutException(ex)) - { - _logger.LogError(ex, "Sql timeout occurred, could not acquire MainDom."); - _errorDuringAcquiring = true; - return false; - } - - // unexpected (will be caught below) - throw; - } + db.BeginTransaction(IsolationLevel.Serializable); var result = InsertLockRecord(tempId, db); //we change the row to a random Id to signal other MainDom to shutdown if (result == RecordPersistenceType.Insert) @@ -228,9 +199,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime InsertLockRecord(_lockId, db); } - db.BeginTransaction(IsolationLevel.ReadCommitted); - // get a read lock - _sqlServerSyntax.ReadLock(db, Cms.Core.Constants.Locks.MainDom); + db.BeginTransaction(IsolationLevel.Serializable); if (!IsMainDomValue(_lockId, db)) { @@ -317,12 +286,11 @@ namespace Umbraco.Cms.Infrastructure.Runtime try { - transaction = db.GetTransaction(IsolationLevel.ReadCommitted); - // get a read lock - _sqlServerSyntax.ReadLock(db, Cms.Core.Constants.Locks.MainDom); + transaction = db.GetTransaction(IsolationLevel.Serializable); - // the row - var mainDomRows = db.Fetch("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); + // the row + var mainDomRows = db.Fetch("SELECT * FROM umbracoKeyValue WHERE [key] = @key", + new {key = MainDomKey}); if (mainDomRows.Count == 0 || mainDomRows[0].Value == updatedTempId) { @@ -331,8 +299,6 @@ namespace Umbraco.Cms.Infrastructure.Runtime // which indicates that we // can acquire it and it has shutdown. - _sqlServerSyntax.WriteLock(db, Cms.Core.Constants.Locks.MainDom); - // so now we update the row with our appdomain id InsertLockRecord(_lockId, db); _logger.LogDebug("Acquired with ID {LockId}", _lockId); @@ -350,12 +316,6 @@ namespace Umbraco.Cms.Infrastructure.Runtime } catch (Exception ex) { - if (IsLockTimeoutException(ex as SqlException)) - { - _logger.LogError(ex, "Sql timeout occurred, waiting for existing MainDom is canceled."); - _errorDuringAcquiring = true; - return false; - } // unexpected _logger.LogError(ex, "Unexpected error, waiting for existing MainDom is canceled."); _errorDuringAcquiring = true; @@ -388,9 +348,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime try { - transaction = db.GetTransaction(IsolationLevel.ReadCommitted); - - _sqlServerSyntax.WriteLock(db, Cms.Core.Constants.Locks.MainDom); + transaction = db.GetTransaction(IsolationLevel.Serializable); // so now we update the row with our appdomain id InsertLockRecord(_lockId, db); @@ -399,13 +357,6 @@ namespace Umbraco.Cms.Infrastructure.Runtime } catch (Exception ex) { - if (IsLockTimeoutException(ex as SqlException)) - { - // something is wrong, we cannot acquire, not much we can do - _logger.LogError(ex, "Sql timeout occurred, could not forcibly acquire MainDom."); - _errorDuringAcquiring = true; - return false; - } _logger.LogError(ex, "Unexpected error, could not forcibly acquire MainDom."); _errorDuringAcquiring = true; return false; @@ -443,13 +394,6 @@ namespace Umbraco.Cms.Infrastructure.Runtime } - /// - /// Checks if the exception is an SQL timeout - /// - /// - /// - private bool IsLockTimeoutException(SqlException sqlException) => sqlException?.Number == 1222; - #region IDisposable Support private bool _disposedValue = false; // To detect redundant calls @@ -474,10 +418,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime try { db = _dbFactory.CreateDatabase(); - db.BeginTransaction(IsolationLevel.ReadCommitted); - - // get a write lock - _sqlServerSyntax.WriteLock(db, Cms.Core.Constants.Locks.MainDom); + db.BeginTransaction(IsolationLevel.Serializable); // When we are disposed, it means we have released the MainDom lock // and called all MainDom release callbacks, in this case diff --git a/src/Umbraco.Infrastructure/Scoping/Scope.cs b/src/Umbraco.Infrastructure/Scoping/Scope.cs index 1c1bd636b9..83cc8a1949 100644 --- a/src/Umbraco.Infrastructure/Scoping/Scope.cs +++ b/src/Umbraco.Infrastructure/Scoping/Scope.cs @@ -7,6 +7,7 @@ using System.Threading; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DistributedLocking; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Infrastructure.Persistence; @@ -44,7 +45,7 @@ namespace Umbraco.Cms.Core.Scoping private IsolatedCaches _isolatedCaches; private IScopedNotificationPublisher _notificationPublisher; - private StackQueue<(LockType lockType, TimeSpan timeout, Guid instanceId, int lockId)> _queuedLocks; + private StackQueue<(DistributedLockType lockType, TimeSpan timeout, Guid instanceId, int lockId)> _queuedLocks; // This is all used to safely track read/write locks at given Scope levels so that // when we dispose we can verify that everything has been cleaned up correctly. @@ -52,6 +53,7 @@ namespace Umbraco.Cms.Core.Scoping private Dictionary> _readLocksDictionary; private HashSet _writeLocks; private Dictionary> _writeLocksDictionary; + private Queue _acquiredLocks; // initializes a new scope private Scope( @@ -153,6 +155,8 @@ namespace Umbraco.Cms.Core.Scoping } else { + _acquiredLocks = new Queue(); + // the FS scope cannot be "on demand" like the rest, because we would need to hook into // every scoped FS to trigger the creation of shadow FS "on demand", and that would be // pretty pointless since if scopeFileSystems is true, we *know* we want to shadow @@ -179,7 +183,7 @@ namespace Umbraco.Cms.Core.Scoping bool callContext = false, bool autoComplete = false) : this(scopeProvider, coreDebugSettings, eventAggregator, logger, fileSystems, null, - scopeContext, detachable, isolationLevel, repositoryCacheMode, + scopeContext, detachable, isolationLevel, repositoryCacheMode, scopedNotificationPublisher, scopeFileSystems, callContext, autoComplete) { } @@ -204,9 +208,6 @@ namespace Umbraco.Cms.Core.Scoping { } - internal Dictionary> ReadLocks => _readLocksDictionary; - - internal Dictionary> WriteLocks => _writeLocksDictionary; // a value indicating whether to force call-context public bool CallContext @@ -277,25 +278,6 @@ namespace Umbraco.Cms.Core.Scoping } } - public IUmbracoDatabase DatabaseOrNull - { - get - { - EnsureNotDisposed(); - if (ParentScope == null) - { - if (_database != null) - { - EnsureDbLocks(); - } - - return _database; - } - - return ParentScope.DatabaseOrNull; - } - } - // true if Umbraco.CoreDebugSettings.LogUncompletedScope appSetting is set to "true" private bool LogUncompletedScopes => _coreDebugSettings.LogIncompletedScopes; @@ -466,6 +448,11 @@ namespace Umbraco.Cms.Core.Scoping ClearLocks(InstanceId); if (ParentScope is null) { + while (!_acquiredLocks?.IsCollectionEmpty() ?? false) + { + _acquiredLocks.Dequeue().Dispose(); + } + // We're the parent scope, make sure that locks of all scopes has been cleared // Since we're only reading we don't have to be in a lock if (_readLocksDictionary?.Count > 0 || _writeLocksDictionary?.Count > 0) @@ -505,24 +492,24 @@ namespace Umbraco.Cms.Core.Scoping _disposed = true; } - public void EagerReadLock(params int[] lockIds) => EagerReadLockInner(Database, InstanceId, null, lockIds); + public void EagerReadLock(params int[] lockIds) => EagerReadLockInner(InstanceId, null, lockIds); /// public void ReadLock(params int[] lockIds) => LazyReadLockInner(InstanceId, lockIds); public void EagerReadLock(TimeSpan timeout, int lockId) => - EagerReadLockInner(Database, InstanceId, timeout, lockId); + EagerReadLockInner(InstanceId, timeout, lockId); /// public void ReadLock(TimeSpan timeout, int lockId) => LazyReadLockInner(InstanceId, timeout, lockId); - public void EagerWriteLock(params int[] lockIds) => EagerWriteLockInner(Database, InstanceId, null, lockIds); + public void EagerWriteLock(params int[] lockIds) => EagerWriteLockInner(InstanceId, null, lockIds); /// public void WriteLock(params int[] lockIds) => LazyWriteLockInner(InstanceId, lockIds); public void EagerWriteLock(TimeSpan timeout, int lockId) => - EagerWriteLockInner(Database, InstanceId, timeout, lockId); + EagerWriteLockInner(InstanceId, timeout, lockId); /// public void WriteLock(TimeSpan timeout, int lockId) => LazyWriteLockInner(InstanceId, timeout, lockId); @@ -598,7 +585,7 @@ namespace Umbraco.Cms.Core.Scoping { if (_queuedLocks?.Count > 0) { - LockType currentType = LockType.ReadLock; + DistributedLockType currentType = DistributedLockType.ReadLock; TimeSpan currentTimeout = TimeSpan.Zero; Guid currentInstanceId = InstanceId; var collectedIds = new HashSet(); @@ -606,7 +593,7 @@ namespace Umbraco.Cms.Core.Scoping var i = 0; while (_queuedLocks.Count > 0) { - (LockType lockType, TimeSpan timeout, Guid instanceId, var lockId) = _queuedLocks.Dequeue(); + (DistributedLockType lockType, TimeSpan timeout, Guid instanceId, var lockId) = _queuedLocks.Dequeue(); if (i == 0) { @@ -621,13 +608,13 @@ namespace Umbraco.Cms.Core.Scoping // process the lock ids collected switch (currentType) { - case LockType.ReadLock: - EagerReadLockInner(_database, currentInstanceId, + case DistributedLockType.ReadLock: + EagerReadLockInner(currentInstanceId, currentTimeout == TimeSpan.Zero ? null : currentTimeout, collectedIds.ToArray()); break; - case LockType.WriteLock: - EagerWriteLockInner(_database, currentInstanceId, + case DistributedLockType.WriteLock: + EagerWriteLockInner(currentInstanceId, currentTimeout == TimeSpan.Zero ? null : currentTimeout, collectedIds.ToArray()); break; @@ -647,12 +634,12 @@ namespace Umbraco.Cms.Core.Scoping // process the remaining switch (currentType) { - case LockType.ReadLock: - EagerReadLockInner(_database, currentInstanceId, + case DistributedLockType.ReadLock: + EagerReadLockInner(currentInstanceId, currentTimeout == TimeSpan.Zero ? null : currentTimeout, collectedIds.ToArray()); break; - case LockType.WriteLock: - EagerWriteLockInner(_database, currentInstanceId, + case DistributedLockType.WriteLock: + EagerWriteLockInner(currentInstanceId, currentTimeout == TimeSpan.Zero ? null : currentTimeout, collectedIds.ToArray()); break; } @@ -922,7 +909,7 @@ namespace Umbraco.Cms.Core.Scoping { // It's safe to assume that the locks on the top of the stack belong to this instance, // since any child scopes that might have added locks to the stack must be disposed before we try and dispose this instance. - (LockType lockType, TimeSpan timeout, Guid instanceId, int lockId) top = + (DistributedLockType lockType, TimeSpan timeout, Guid instanceId, int lockId) top = _queuedLocks.PeekStack(); if (top.instanceId == instanceId) { @@ -945,7 +932,7 @@ namespace Umbraco.Cms.Core.Scoping } else { - LazyLockInner(LockType.ReadLock, instanceId, lockIds); + LazyLockInner(DistributedLockType.ReadLock, instanceId, lockIds); } } @@ -957,7 +944,7 @@ namespace Umbraco.Cms.Core.Scoping } else { - LazyLockInner(LockType.ReadLock, instanceId, timeout, lockId); + LazyLockInner(DistributedLockType.ReadLock, instanceId, timeout, lockId); } } @@ -969,7 +956,7 @@ namespace Umbraco.Cms.Core.Scoping } else { - LazyLockInner(LockType.WriteLock, instanceId, lockIds); + LazyLockInner(DistributedLockType.WriteLock, instanceId, lockIds); } } @@ -981,17 +968,17 @@ namespace Umbraco.Cms.Core.Scoping } else { - LazyLockInner(LockType.WriteLock, instanceId, timeout, lockId); + LazyLockInner(DistributedLockType.WriteLock, instanceId, timeout, lockId); } } - private void LazyLockInner(LockType lockType, Guid instanceId, params int[] lockIds) + private void LazyLockInner(DistributedLockType lockType, Guid instanceId, params int[] lockIds) { lock (_lockQueueLocker) { if (_queuedLocks == null) { - _queuedLocks = new StackQueue<(LockType, TimeSpan, Guid, int)>(); + _queuedLocks = new StackQueue<(DistributedLockType, TimeSpan, Guid, int)>(); } foreach (var lockId in lockIds) @@ -1001,15 +988,16 @@ namespace Umbraco.Cms.Core.Scoping } } - private void LazyLockInner(LockType lockType, Guid instanceId, TimeSpan timeout, int lockId) + private void LazyLockInner(DistributedLockType lockType, Guid instanceId, TimeSpan timeout, int lockId) { lock (_lockQueueLocker) { if (_queuedLocks == null) { - _queuedLocks = new StackQueue<(LockType, TimeSpan, Guid, int)>(); + _queuedLocks = new StackQueue<(DistributedLockType, TimeSpan, Guid, int)>(); } + _queuedLocks.Enqueue((lockType, timeout, instanceId, lockId)); } } @@ -1020,18 +1008,31 @@ namespace Umbraco.Cms.Core.Scoping /// Instance ID of the requesting scope. /// Optional database timeout in milliseconds. /// Array of lock object identifiers. - private void EagerReadLockInner(IUmbracoDatabase db, Guid instanceId, TimeSpan? timeout, params int[] lockIds) + private void EagerReadLockInner(Guid instanceId, TimeSpan? timeout, params int[] lockIds) { if (ParentScope is not null) { // If we have a parent we delegate lock creation to parent. - ParentScope.EagerReadLockInner(db, instanceId, timeout, lockIds); + ParentScope.EagerReadLockInner(instanceId, timeout, lockIds); } else { - // We are the outermost scope, handle the lock request. - LockInner(db, instanceId, ref _readLocksDictionary, ref _readLocks, ObtainReadLock, - ObtainTimeoutReadLock, timeout, lockIds); + lock (_dictionaryLocker) + { + foreach (var lockId in lockIds) + { + IncrementLock(lockId, instanceId, ref _readLocksDictionary); + + // We are the outermost scope, handle the lock request. + LockInner( + instanceId, + ref _readLocksDictionary, + ref _readLocks, + ObtainReadLock, + timeout, + lockId); + } + } } } @@ -1041,18 +1042,31 @@ namespace Umbraco.Cms.Core.Scoping /// Instance ID of the requesting scope. /// Optional database timeout in milliseconds. /// Array of lock object identifiers. - private void EagerWriteLockInner(IUmbracoDatabase db, Guid instanceId, TimeSpan? timeout, params int[] lockIds) + private void EagerWriteLockInner(Guid instanceId, TimeSpan? timeout, params int[] lockIds) { if (ParentScope is not null) { // If we have a parent we delegate lock creation to parent. - ParentScope.EagerWriteLockInner(db, instanceId, timeout, lockIds); + ParentScope.EagerWriteLockInner(instanceId, timeout, lockIds); } else { - // We are the outermost scope, handle the lock request. - LockInner(db, instanceId, ref _writeLocksDictionary, ref _writeLocks, ObtainWriteLock, - ObtainTimeoutWriteLock, timeout, lockIds); + lock (_dictionaryLocker) + { + foreach (var lockId in lockIds) + { + IncrementLock(lockId, instanceId, ref _writeLocksDictionary); + + // We are the outermost scope, handle the lock request. + LockInner( + instanceId, + ref _writeLocksDictionary, + ref _writeLocks, + ObtainWriteLock, + timeout, + lockId); + } + } } } @@ -1062,90 +1076,56 @@ namespace Umbraco.Cms.Core.Scoping /// Instance ID of the scope requesting the lock. /// Reference to the applicable locks dictionary (ReadLocks or WriteLocks). /// Reference to the applicable locks hashset (_readLocks or _writeLocks). - /// Delegate used to request the lock from the database without a timeout. - /// Delegate used to request the lock from the database with a timeout. + /// Delegate used to request the lock from the locking mechanism. /// Optional timeout parameter to specify a timeout. - /// Lock identifiers to lock on. - private void LockInner(IUmbracoDatabase db, Guid instanceId, ref Dictionary> locks, + /// Lock identifier. + private void LockInner( + Guid instanceId, + ref Dictionary> locks, ref HashSet locksSet, - Action obtainLock, Action obtainLockTimeout, + Action obtainLock, TimeSpan? timeout, - params int[] lockIds) + int lockId) { - lock (_dictionaryLocker) + locksSet ??= new HashSet(); + + // Only acquire the lock if we haven't done so yet. + if (locksSet.Contains(lockId)) { - locksSet ??= new HashSet(); - foreach (var lockId in lockIds) - { - // Only acquire the lock if we haven't done so yet. - if (!locksSet.Contains(lockId)) - { - IncrementLock(lockId, instanceId, ref locks); - locksSet.Add(lockId); - try - { - if (timeout is null) - { - // We just want an ordinary lock. - obtainLock(db, lockId); - } - else - { - // We want a lock with a custom timeout - obtainLockTimeout(db, lockId, timeout.Value); - } - } - catch - { - // Something went wrong and we didn't get the lock - // Since we at this point have determined that we haven't got any lock with an ID of LockID, it's safe to completely remove it instead of decrementing. - locks[instanceId].Remove(lockId); - // It needs to be removed from the HashSet as well, because that's how we determine to acquire a lock. - locksSet.Remove(lockId); - throw; - } - } - else - { - // We already have a lock, but need to update the dictionary for debugging purposes. - IncrementLock(lockId, instanceId, ref locks); - } - } + return; + } + + locksSet.Add(lockId); + try + { + obtainLock(lockId, timeout); + } + catch + { + // Something went wrong and we didn't get the lock + // Since we at this point have determined that we haven't got any lock with an ID of LockID, it's safe to completely remove it instead of decrementing. + locks[instanceId].Remove(lockId); + + // It needs to be removed from the HashSet as well, because that's how we determine to acquire a lock. + locksSet.Remove(lockId); + throw; } } - /// - /// Obtains an ordinary read lock. - /// - /// Lock object identifier to lock. - private void ObtainReadLock(IUmbracoDatabase db, int lockId) => SqlContext.SqlSyntax.ReadLock(db, lockId); - /// /// Obtains a read lock with a custom timeout. /// /// Lock object identifier to lock. /// TimeSpan specifying the timout period. - private void ObtainTimeoutReadLock(IUmbracoDatabase db, int lockId, TimeSpan timeout) => - SqlContext.SqlSyntax.ReadLock(db, timeout, lockId); - - /// - /// Obtains an ordinary write lock. - /// - /// Lock object identifier to lock. - private void ObtainWriteLock(IUmbracoDatabase db, int lockId) => SqlContext.SqlSyntax.WriteLock(db, lockId); + private void ObtainReadLock(int lockId, TimeSpan? timeout) + => _acquiredLocks.Enqueue(_scopeProvider.DistributedLockingMechanismFactory.DistributedLockingMechanism.ReadLock(lockId, timeout)); /// /// Obtains a write lock with a custom timeout. /// /// Lock object identifier to lock. /// TimeSpan specifying the timout period. - private void ObtainTimeoutWriteLock(IUmbracoDatabase db, int lockId, TimeSpan timeout) => - SqlContext.SqlSyntax.WriteLock(db, timeout, lockId); - - private enum LockType - { - ReadLock, - WriteLock - } + private void ObtainWriteLock(int lockId, TimeSpan? timeout) + => _acquiredLocks.Enqueue(_scopeProvider.DistributedLockingMechanismFactory.DistributedLockingMechanism.WriteLock(lockId, timeout)); } } diff --git a/src/Umbraco.Infrastructure/Scoping/ScopeProvider.cs b/src/Umbraco.Infrastructure/Scoping/ScopeProvider.cs index 0f5fba4d4a..c27fdb9489 100644 --- a/src/Umbraco.Infrastructure/Scoping/ScopeProvider.cs +++ b/src/Umbraco.Infrastructure/Scoping/ScopeProvider.cs @@ -11,6 +11,7 @@ using Umbraco.Extensions; using System.Collections.Generic; using System.Collections.Concurrent; using System.Threading; +using Umbraco.Cms.Core.DistributedLocking; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; @@ -38,6 +39,7 @@ namespace Umbraco.Cms.Core.Scoping private readonly IEventAggregator _eventAggregator; public ScopeProvider( + IDistributedLockingMechanismFactory distributedLockingMechanismFactory, IUmbracoDatabaseFactory databaseFactory, FileSystems fileSystems, IOptionsMonitor coreDebugSettings, @@ -45,6 +47,7 @@ namespace Umbraco.Cms.Core.Scoping IRequestCache requestCache, IEventAggregator eventAggregator) { + DistributedLockingMechanismFactory = distributedLockingMechanismFactory; DatabaseFactory = databaseFactory; _fileSystems = fileSystems; _coreDebugSettings = coreDebugSettings.CurrentValue; @@ -58,6 +61,8 @@ namespace Umbraco.Cms.Core.Scoping coreDebugSettings.OnChange(x => _coreDebugSettings = x); } + public IDistributedLockingMechanismFactory DistributedLockingMechanismFactory { get; } + public IUmbracoDatabaseFactory DatabaseFactory { get; } public ISqlContext SqlContext => DatabaseFactory.SqlContext; diff --git a/src/Umbraco.Persistence.SqlCe/SqlCeBulkSqlInsertProvider.cs b/src/Umbraco.Persistence.SqlCe/SqlCeBulkSqlInsertProvider.cs deleted file mode 100644 index e6ed41548d..0000000000 --- a/src/Umbraco.Persistence.SqlCe/SqlCeBulkSqlInsertProvider.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Data.SqlServerCe; -using System.Data.SqlTypes; -using System.Linq; -using NPoco; -using Umbraco.Cms.Infrastructure.Persistence; -using Umbraco.Extensions; -using Constants = Umbraco.Cms.Core.Constants; - -namespace Umbraco.Cms.Persistence.SqlCe -{ - public class SqlCeBulkSqlInsertProvider : IBulkSqlInsertProvider - { - public string ProviderName => Constants.DatabaseProviders.SqlCe; - - public int BulkInsertRecords(IUmbracoDatabase database, IEnumerable records) - { - if (!records.Any()) return 0; - - var pocoData = database.PocoDataFactory.ForType(typeof(T)); - if (pocoData == null) throw new InvalidOperationException("Could not find PocoData for " + typeof(T)); - - return BulkInsertRecordsSqlCe(database, pocoData, records.ToArray()); - - } - - /// - /// Bulk-insert records using SqlCE TableDirect method. - /// - /// The type of the records. - /// The database. - /// The PocoData object corresponding to the record's type. - /// The records. - /// The number of records that were inserted. - private static int BulkInsertRecordsSqlCe(IUmbracoDatabase database, PocoData pocoData, IEnumerable records) - { - var columns = pocoData.Columns.ToArray(); - - // create command against the original database.Connection - using (var command = database.CreateCommand(database.Connection, CommandType.TableDirect, string.Empty)) - { - command.CommandText = pocoData.TableInfo.TableName; - command.CommandType = CommandType.TableDirect; // TODO: why repeat? - // TODO: not supporting transactions? - //cmd.Transaction = GetTypedTransaction(db.Connection.); - - var count = 0; - var tCommand = NPocoDatabaseExtensions.GetTypedCommand(command); // execute on the real command - - // seems to cause problems, I think this is primarily used for retrieval, not inserting. - // see: https://msdn.microsoft.com/en-us/library/system.data.sqlserverce.sqlcecommand.indexname%28v=vs.100%29.aspx?f=255&MSPPError=-2147217396 - //tCommand.IndexName = pd.TableInfo.PrimaryKey; - - using (var resultSet = tCommand.ExecuteResultSet(ResultSetOptions.Updatable)) - { - var updatableRecord = resultSet.CreateRecord(); - foreach (var record in records) - { - for (var i = 0; i < columns.Length; i++) - { - // skip the index if this shouldn't be included (i.e. PK) - if (NPocoDatabaseExtensions.IncludeColumn(pocoData, columns[i])) - { - var val = columns[i].Value.GetValue(record); - - if (val is byte[]) - { - var bytes = val as byte[]; - updatableRecord.SetSqlBinary(i, new SqlBinary(bytes)); - } - else - { - updatableRecord.SetValue(i, val); - } - - updatableRecord.SetValue(i, val); - } - } - resultSet.Insert(updatableRecord); - count++; - } - } - - return count; - } - } - } -} diff --git a/src/Umbraco.Persistence.SqlCe/SqlCeDatabaseCreator.cs b/src/Umbraco.Persistence.SqlCe/SqlCeDatabaseCreator.cs deleted file mode 100644 index fd360be13a..0000000000 --- a/src/Umbraco.Persistence.SqlCe/SqlCeDatabaseCreator.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Umbraco.Cms.Infrastructure.Persistence; -using Constants = Umbraco.Cms.Core.Constants; - -namespace Umbraco.Cms.Persistence.SqlCe -{ - public class SqlCeDatabaseCreator : IDatabaseCreator - { - public string ProviderName => Constants.DatabaseProviders.SqlCe; - - public void Create(string connectionString) - { - using var engine = new System.Data.SqlServerCe.SqlCeEngine(connectionString); - engine.CreateDatabase(); - } - } -} diff --git a/src/Umbraco.Persistence.SqlCe/SqlCeImageMapper.cs b/src/Umbraco.Persistence.SqlCe/SqlCeImageMapper.cs deleted file mode 100644 index 8349056440..0000000000 --- a/src/Umbraco.Persistence.SqlCe/SqlCeImageMapper.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using System.Data; -using System.Data.Common; -using System.Data.SqlServerCe; -using System.Linq; -using System.Reflection; -using NPoco; -using Umbraco.Cms.Infrastructure.Persistence; - -namespace Umbraco.Cms.Persistence.SqlCe -{ - /// - /// Custom NPoco mapper for SqlCe - /// - /// - /// Work arounds to handle special columns - /// - public class SqlCeImageMapper : DefaultMapper - { - //private readonly IUmbracoDatabaseFactory _dbFactory; - - //public SqlCeImageMapper(IUmbracoDatabaseFactory dbFactory) => _dbFactory = dbFactory; - - public override Func GetToDbConverter(Type destType, MemberInfo sourceMemberInfo) - { - if (sourceMemberInfo.GetMemberInfoType() == typeof(byte[])) - { - return x => - { - //PocoData pd = _dbFactory.SqlContext.PocoDataFactory.ForType(sourceMemberInfo.DeclaringType); - //if (pd == null) - //{ - // return null; - //} - - //PocoColumn col = pd.AllColumns.FirstOrDefault(x => x.MemberInfoData.MemberInfo == sourceMemberInfo); - //if (col == null) - //{ - // return null; - //} - - return new SqlCeParameter - { - SqlDbType = SqlDbType.Image, - Value = x ?? Array.Empty() - }; - }; - } - return base.GetToDbConverter(destType, sourceMemberInfo); - } - - public override Func GetParameterConverter(DbCommand dbCommand, Type sourceType) - { - if (sourceType == typeof(byte[])) - { - return x => - { - var param = new SqlCeParameter - { - SqlDbType = SqlDbType.Image, - Value = x - }; - return param; - }; - - } - return base.GetParameterConverter(dbCommand, sourceType); - } - } -} diff --git a/src/Umbraco.Persistence.SqlCe/SqlCeSpecificMapperFactory.cs b/src/Umbraco.Persistence.SqlCe/SqlCeSpecificMapperFactory.cs deleted file mode 100644 index 3646218fce..0000000000 --- a/src/Umbraco.Persistence.SqlCe/SqlCeSpecificMapperFactory.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Umbraco.Cms.Core; -using Umbraco.Cms.Infrastructure.Persistence; - -namespace Umbraco.Cms.Persistence.SqlCe -{ - public class SqlCeSpecificMapperFactory : IProviderSpecificMapperFactory - { - public string ProviderName => Constants.DatabaseProviders.SqlCe; - public NPocoMapperCollection Mappers => new NPocoMapperCollection(() => new[] {new SqlCeImageMapper()}); - } -} diff --git a/src/Umbraco.Persistence.SqlCe/SqlCeSyntaxProvider.cs b/src/Umbraco.Persistence.SqlCe/SqlCeSyntaxProvider.cs deleted file mode 100644 index 62aa933a04..0000000000 --- a/src/Umbraco.Persistence.SqlCe/SqlCeSyntaxProvider.cs +++ /dev/null @@ -1,322 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Linq; -using Microsoft.Extensions.Options; -using NPoco; -using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Infrastructure.Persistence; -using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -using ColumnInfo = Umbraco.Cms.Infrastructure.Persistence.SqlSyntax.ColumnInfo; -using Constants = Umbraco.Cms.Core.Constants; - -namespace Umbraco.Cms.Persistence.SqlCe -{ - /// - /// Represents an SqlSyntaxProvider for Sql Ce - /// - public class SqlCeSyntaxProvider : MicrosoftSqlSyntaxProviderBase - { - private readonly IOptions _globalSettings; - - public SqlCeSyntaxProvider(IOptions globalSettings) - { - _globalSettings = globalSettings; - BlobColumnDefinition = "IMAGE"; - // NOTE: if this column type is used in sqlce, it will prob result in errors since - // SQLCE cannot support this type correctly without 2x columns and a lot of work arounds. - // We don't use this natively within Umbraco but 3rd parties might with SQL server. - DateTimeOffsetColumnDefinition = "DATETIME"; - } - - public override string ProviderName => Constants.DatabaseProviders.SqlCe; - - public override Sql SelectTop(Sql sql, int top) - { - return new Sql(sql.SqlContext, sql.SQL.Insert(sql.SQL.IndexOf(' '), " TOP " + top), sql.Arguments); - } - - public override bool SupportsClustered() - { - return false; - } - - /// - /// SqlCe doesn't support the Truncate Table syntax, so we just have to do a DELETE FROM which is slower but we have no choice. - /// - public override string TruncateTable - { - get { return "DELETE FROM {0}"; } - } - - public override string GetIndexType(IndexTypes indexTypes) - { - string indexType; - //NOTE Sql Ce doesn't support clustered indexes - if (indexTypes == IndexTypes.Clustered) - { - indexType = "NONCLUSTERED"; - } - else - { - indexType = indexTypes == IndexTypes.NonClustered - ? "NONCLUSTERED" - : "UNIQUE NONCLUSTERED"; - } - return indexType; - } - - public override string GetConcat(params string[] args) - { - return "(" + string.Join("+", args) + ")"; - } - - public override System.Data.IsolationLevel DefaultIsolationLevel => System.Data.IsolationLevel.RepeatableRead; - public override string DbProvider => "SqlServerCE"; - - public override string FormatColumnRename(string tableName, string oldName, string newName) - { - //NOTE Sql CE doesn't support renaming a column, so a new column needs to be created, then copy data and finally remove old column - //This assumes that the new column has been created, and that the old column will be deleted after this statement has run. - //http://stackoverflow.com/questions/3967353/microsoft-sql-compact-edition-rename-column - - return string.Format("UPDATE {0} SET {1} = {2}", tableName, newName, oldName); - } - - public override string FormatTableRename(string oldName, string newName) - { - return string.Format(RenameTable, oldName, newName); - } - - public override string FormatPrimaryKey(TableDefinition table) - { - var columnDefinition = table.Columns.FirstOrDefault(x => x.IsPrimaryKey); - if (columnDefinition == null) - return string.Empty; - - string constraintName = string.IsNullOrEmpty(columnDefinition.PrimaryKeyName) - ? string.Format("PK_{0}", table.Name) - : columnDefinition.PrimaryKeyName; - - string columns = string.IsNullOrEmpty(columnDefinition.PrimaryKeyColumns) - ? GetQuotedColumnName(columnDefinition.Name) - : string.Join(", ", columnDefinition.PrimaryKeyColumns - .Split(Constants.CharArrays.CommaSpace, StringSplitOptions.RemoveEmptyEntries) - .Select(GetQuotedColumnName)); - - return string.Format(CreateConstraint, - GetQuotedTableName(table.Name), - GetQuotedName(constraintName), - "PRIMARY KEY", - columns); - } - - public override IEnumerable GetTablesInSchema(IDatabase db) - { - var items = db.Fetch("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES"); - return items.Select(x => x.TABLE_NAME).Cast().ToList(); - } - - public override IEnumerable GetColumnsInSchema(IDatabase db) - { - var items = db.Fetch("SELECT TABLE_NAME, COLUMN_NAME, ORDINAL_POSITION, COLUMN_DEFAULT, IS_NULLABLE, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS"); - return - items.Select( - item => - new ColumnInfo(item.TABLE_NAME, item.COLUMN_NAME, item.ORDINAL_POSITION, item.COLUMN_DEFAULT, - item.IS_NULLABLE, item.DATA_TYPE)).ToList(); - } - - /// - public override IEnumerable> GetConstraintsPerTable(IDatabase db) - { - var items = db.Fetch("SELECT TABLE_NAME, CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS"); - return items.Select(item => new Tuple(item.TABLE_NAME, item.CONSTRAINT_NAME)).ToList(); - } - - /// - public override IEnumerable> GetConstraintsPerColumn(IDatabase db) - { - var items = db.Fetch("SELECT TABLE_NAME, COLUMN_NAME, CONSTRAINT_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE"); - return items.Select(item => new Tuple(item.TABLE_NAME, item.COLUMN_NAME, item.CONSTRAINT_NAME)).ToList(); - } - - /// - public override IEnumerable> GetDefinedIndexes(IDatabase db) - { - var items = - db.Fetch( - @"SELECT TABLE_NAME, INDEX_NAME, COLUMN_NAME, [UNIQUE] FROM INFORMATION_SCHEMA.INDEXES -WHERE PRIMARY_KEY=0 -ORDER BY TABLE_NAME, INDEX_NAME"); - return - items.Select( - item => new Tuple(item.TABLE_NAME, item.INDEX_NAME, item.COLUMN_NAME, item.UNIQUE)); - } - - /// - public override bool TryGetDefaultConstraint(IDatabase db, string tableName, string columnName, out string constraintName) - { - // cannot return a true default constraint name (does not exist on SqlCe) - // but we won't really need it anyways - just check whether there is a constraint - constraintName = null; - var hasDefault = db.Fetch(@"select column_hasdefault from information_schema.columns -where table_name=@0 and column_name=@1", tableName, columnName).FirstOrDefault(); - return hasDefault; - } - - public override bool DoesTableExist(IDatabase db, string tableName) - { - var result = - db.ExecuteScalar("SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = @TableName", - new { TableName = tableName }); - - return result > 0; - } - - public override void WriteLock(IDatabase db, TimeSpan timeout, int lockId) - { - // soon as we get Database, a transaction is started - - if (db.Transaction.IsolationLevel < IsolationLevel.RepeatableRead) - throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required."); - - ObtainWriteLock(db, timeout, lockId); - } - - public override void WriteLock(IDatabase db, params int[] lockIds) - { - // soon as we get Database, a transaction is started - - if (db.Transaction.IsolationLevel < IsolationLevel.RepeatableRead) - throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required."); - - var timeout = _globalSettings.Value.SqlWriteLockTimeOut; - - foreach (var lockId in lockIds) - { - ObtainWriteLock(db, timeout, lockId); - } - } - - private static void ObtainWriteLock(IDatabase db, TimeSpan timeout, int lockId) - { - db.Execute(@"SET LOCK_TIMEOUT " + timeout.TotalMilliseconds + ";"); - var i = db.Execute(@"UPDATE umbracoLock SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id", new { id = lockId }); - if (i == 0) // ensure we are actually locking! - throw new ArgumentException($"LockObject with id={lockId} does not exist."); - } - - public override void ReadLock(IDatabase db, TimeSpan timeout, int lockId) - { - // soon as we get Database, a transaction is started - - if (db.Transaction.IsolationLevel < IsolationLevel.RepeatableRead) - throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required."); - - ObtainReadLock(db, timeout, lockId); - } - - public override void ReadLock(IDatabase db, params int[] lockIds) - { - // soon as we get Database, a transaction is started - - if (db.Transaction.IsolationLevel < IsolationLevel.RepeatableRead) - throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required."); - - foreach (var lockId in lockIds) - { - ObtainReadLock(db, null, lockId); - } - } - - private static void ObtainReadLock(IDatabase db, TimeSpan? timeout, int lockId) - { - if (timeout.HasValue) - { - db.Execute(@"SET LOCK_TIMEOUT " + timeout.Value.TotalMilliseconds + ";"); - } - - var i = db.ExecuteScalar("SELECT value FROM umbracoLock WHERE id=@id", new {id = lockId}); - - if (i == null) // ensure we are actually locking! - throw new ArgumentException($"LockObject with id={lockId} does not exist."); - } - - protected override string FormatIdentity(ColumnDefinition column) - { - return column.IsIdentity ? GetIdentityString(column) : string.Empty; - } - - private static string GetIdentityString(ColumnDefinition column) - { - if (column.Seeding != default(int)) - return string.Format("IDENTITY({0},1)", column.Seeding); - - return "IDENTITY(1,1)"; - } - - protected override string FormatSystemMethods(SystemMethods systemMethod) - { - switch (systemMethod) - { - case SystemMethods.NewGuid: - return "NEWID()"; - case SystemMethods.CurrentDateTime: - return "GETDATE()"; - //case SystemMethods.NewSequentialId: - // return "NEWSEQUENTIALID()"; - //case SystemMethods.CurrentUTCDateTime: - // return "GETUTCDATE()"; - } - - return null; - } - - public override string DeleteDefaultConstraint - { - get - { - return "ALTER TABLE {0} ALTER COLUMN {1} DROP DEFAULT"; - } - } - - public override string DropIndex { get { return "DROP INDEX {1}.{0}"; } } - public override string CreateIndex => "CREATE {0}{1}INDEX {2} ON {3} ({4})"; - public override string Format(IndexDefinition index) - { - var name = string.IsNullOrEmpty(index.Name) - ? $"IX_{index.TableName}_{index.ColumnName}" - : index.Name; - - var columns = index.Columns.Any() - ? string.Join(",", index.Columns.Select(x => GetQuotedColumnName(x.Name))) - : GetQuotedColumnName(index.ColumnName); - - - return string.Format(CreateIndex, GetIndexType(index.IndexType), " ", GetQuotedName(name), - GetQuotedTableName(index.TableName), columns); - } - - public override string GetSpecialDbType(SpecialDbType dbTypes) - { - // SqlCE does not have nvarchar(max) for now - if (dbTypes == SpecialDbType.NVARCHARMAX) - { - return "NTEXT"; - } - - return base.GetSpecialDbType(dbTypes); - } - public override SqlDbType GetSqlDbType(DbType dbType) - { - if (DbType.Binary == dbType) - { - return SqlDbType.Image; - } - return base.GetSqlDbType(dbType); - } - } -} diff --git a/src/Umbraco.Persistence.SqlCe/Umbraco.Persistence.SqlCe.csproj b/src/Umbraco.Persistence.SqlCe/Umbraco.Persistence.SqlCe.csproj deleted file mode 100644 index 86aaafa5ec..0000000000 --- a/src/Umbraco.Persistence.SqlCe/Umbraco.Persistence.SqlCe.csproj +++ /dev/null @@ -1,39 +0,0 @@ - - - - net472 - Umbraco.Cms.Persistence.SqlCe - - - - bin\Release\Umbraco.Persistence.SqlCe.xml - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - all - - - - - - - - - - - - - - - - diff --git a/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs b/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs index 8aa35c1608..9964195f34 100644 --- a/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs +++ b/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs @@ -364,7 +364,7 @@ AND cmsContentNu.nodeId IS NULL foreach (IProperty prop in content.Properties) { var pdatas = new List(); - foreach (IPropertyValue pvalue in prop.Values) + foreach (IPropertyValue pvalue in prop.Values.OrderBy(x => x.Culture)) { // sanitize - properties should be ok but ... never knows if (!prop.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment)) diff --git a/src/Umbraco.Web.BackOffice/Install/InstallApiController.cs b/src/Umbraco.Web.BackOffice/Install/InstallApiController.cs index eb6457074e..29c64174dd 100644 --- a/src/Umbraco.Web.BackOffice/Install/InstallApiController.cs +++ b/src/Umbraco.Web.BackOffice/Install/InstallApiController.cs @@ -61,12 +61,8 @@ namespace Umbraco.Cms.Web.BackOffice.Install internal InstallHelper InstallHelper { get; } - public bool PostValidateDatabaseConnection(DatabaseModel model) - { - var canConnect = _databaseBuilder.CanConnect(model.DatabaseType.ToString(), model.ConnectionString, - model.Server, model.DatabaseName, model.Login, model.Password, model.IntegratedAuth); - return canConnect; - } + public bool PostValidateDatabaseConnection(DatabaseModel databaseSettings) + => _databaseBuilder.ConfigureDatabaseConnection(databaseSettings, isTrialRun: true); /// /// Gets the install setup. diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index 25292a8993..9f999e7167 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -145,10 +145,8 @@ namespace Umbraco.Extensions builder.Services.AddUnique(); builder.Services.AddHostedService(factory => factory.GetRequiredService()); - // Add supported databases - builder.AddUmbracoSqlServerSupport(); - builder.AddUmbracoSqlCeSupport(); builder.Services.AddSingleton(); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); // Must be added here because DbProviderFactories is netstandard 2.1 so cannot exist in Infra for now builder.Services.AddSingleton(factory => new DbProviderFactoryCreator( @@ -156,7 +154,8 @@ namespace Umbraco.Extensions factory.GetServices(), factory.GetServices(), factory.GetServices(), - factory.GetServices() + factory.GetServices(), + factory.GetServices() )); builder.AddCoreInitialServices(); @@ -388,66 +387,6 @@ namespace Umbraco.Extensions return builder; } - /// - /// Adds SqlCe support for Umbraco - /// - private static IUmbracoBuilder AddUmbracoSqlCeSupport(this IUmbracoBuilder builder) - { - try - { - var binFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - if (binFolder != null) - { - var dllPath = Path.Combine(binFolder, "Umbraco.Persistence.SqlCe.dll"); - var umbSqlCeAssembly = Assembly.LoadFrom(dllPath); - - Type sqlCeSyntaxProviderType = umbSqlCeAssembly.GetType("Umbraco.Cms.Persistence.SqlCe.SqlCeSyntaxProvider"); - Type sqlCeBulkSqlInsertProviderType = umbSqlCeAssembly.GetType("Umbraco.Cms.Persistence.SqlCe.SqlCeBulkSqlInsertProvider"); - Type sqlCeDatabaseCreatorType = umbSqlCeAssembly.GetType("Umbraco.Cms.Persistence.SqlCe.SqlCeDatabaseCreator"); - Type sqlCeSpecificMapperFactory = umbSqlCeAssembly.GetType("Umbraco.Cms.Persistence.SqlCe.SqlCeSpecificMapperFactory"); - - if (!(sqlCeSyntaxProviderType is null - || sqlCeBulkSqlInsertProviderType is null - || sqlCeDatabaseCreatorType is null - || sqlCeSpecificMapperFactory is null)) - { - builder.Services.AddSingleton(typeof(ISqlSyntaxProvider), sqlCeSyntaxProviderType); - builder.Services.AddSingleton(typeof(IBulkSqlInsertProvider), sqlCeBulkSqlInsertProviderType); - builder.Services.AddSingleton(typeof(IDatabaseCreator), sqlCeDatabaseCreatorType); - builder.Services.AddSingleton(typeof(IProviderSpecificMapperFactory), sqlCeSpecificMapperFactory); - } - - var sqlCeAssembly = Assembly.LoadFrom(Path.Combine(binFolder, "System.Data.SqlServerCe.dll")); - - var sqlCe = sqlCeAssembly.GetType("System.Data.SqlServerCe.SqlCeProviderFactory"); - if (!(sqlCe is null)) - { - DbProviderFactories.RegisterFactory(Cms.Core.Constants.DbProviderNames.SqlCe, sqlCe); - } - } - } - catch - { - // Ignore if SqlCE is not available - } - - return builder; - } - - /// - /// Adds Sql Server support for Umbraco - /// - private static IUmbracoBuilder AddUmbracoSqlServerSupport(this IUmbracoBuilder builder) - { - DbProviderFactories.RegisterFactory(Cms.Core.Constants.DbProviderNames.SqlServer, SqlClientFactory.Instance); - - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - - return builder; - } - private static IProfiler GetWebProfiler(IConfiguration config) { var isDebug = config.GetValue($"{Cms.Core.Constants.Configuration.ConfigHosting}:Debug"); diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/database.controller.js b/src/Umbraco.Web.UI.Client/src/installer/steps/database.controller.js index d236d45568..33c0ffca12 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/steps/database.controller.js +++ b/src/Umbraco.Web.UI.Client/src/installer/steps/database.controller.js @@ -1,38 +1,57 @@ angular.module("umbraco.install").controller("Umbraco.Installer.DataBaseController", function($scope, $http, installerService){ - $scope.checking = false; - $scope.invalidDbDns = false; + $scope.checking = false; + $scope.invalidDbDns = false; - $scope.dbs = $scope.installer.current.model.databases; + $scope.dbs = $scope.installer.current.model.databases; + window.dbs = $scope.dbs; + + $scope.providerNames = _.chain(dbs) + .map('providerName') + .filter(x => x) + .uniq() + .value(); + + if (!$scope.selectedDbMeta) { + $scope.selectedDbMeta = $scope.dbs[0]; + } + + $scope.$watch('selectedDbMeta', function(newValue, oldValue) { + $scope.installer.current.model.integratedAuth = false; + $scope.installer.current.model.databaseProviderMetadataId = newValue.id; + $scope.installer.current.model.providerName = newValue.providerName; + $scope.installer.current.model.databaseName = newValue.defaultDatabaseName; + }); + + $scope.isCustom = function() { + return $scope.selectedDbMeta.displayName === 'Custom'; + } - if (angular.isUndefined(installerService.status.current.model.dbType) || installerService.status.current.model.dbType === null) { - installerService.status.current.model.dbType = $scope.dbs[0].id; - } $scope.validateAndForward = function() { if (!$scope.checking && this.myForm.$valid) { - $scope.checking = true; - $scope.invalidDbDns = false; + $scope.checking = true; + $scope.invalidDbDns = false; - var model = installerService.status.current.model; + var model = installerService.status.current.model; - $http.post( - Umbraco.Sys.ServerVariables.installApiBaseUrl + "PostValidateDatabaseConnection", - model).then(function(response) { + $http.post( + Umbraco.Sys.ServerVariables.installApiBaseUrl + "PostValidateDatabaseConnection", + model).then(function(response) { - if (response.data === true) { - installerService.forward(); - } - else { - $scope.invalidDbDns = true; - } + if (response.data === true) { + installerService.forward(); + } + else { + $scope.invalidDbDns = true; + } - $scope.checking = false; - }, function(){ - $scope.invalidDbDns = true; - $scope.checking = false; - }); - } - }; + $scope.checking = false; + }, function(){ + $scope.invalidDbDns = true; + $scope.checking = false; + }); + } + }; }); diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/database.html b/src/Umbraco.Web.UI.Client/src/installer/steps/database.html index cf367b2ff2..dc3b972718 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/steps/database.html +++ b/src/Umbraco.Web.UI.Client/src/installer/steps/database.html @@ -11,22 +11,14 @@
-
-

Great! No need to configure anything, you can simply click the continue button below to continue to the next step

-
- -
-

Great! No need to configure anything, you can simply click the continue button below to continue to the next step

-
- -
+
What is the exact connection string we should use?
@@ -39,20 +31,36 @@ Enter a valid database connection string.
+ +
+ +
+ +
+
-
+ + +
Where do we find your database? -
-
- -
- - Enter server domain or IP + +
+
+
+ +
+ + Enter server domain or IP +
@@ -71,49 +79,52 @@
-
- What credentials are used to access the database? -
-
- -
- - Enter the database user name +
+
+ What credentials are used to access the database? +
+
+ +
+ + Enter the database user name +
-
-
-
- -
- - Enter the database password +
+
+ +
+ + Enter the database password +
-
-
-
- +
+
+ +
-
+
+
- diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/user.controller.js b/src/Umbraco.Web.UI.Client/src/installer/steps/user.controller.js index 896a7a54f5..b03282c62e 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/steps/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/installer/steps/user.controller.js @@ -1,16 +1,16 @@ angular.module("umbraco.install").controller("Umbraco.Install.UserController", function($scope, installerService) { - + $scope.majorVersion = Umbraco.Sys.ServerVariables.application.version; $scope.passwordPattern = /.*/; $scope.installer.current.model.subscribeToNewsLetter = false; - + if ($scope.installer.current.model.minNonAlphaNumericLength > 0) { var exp = ""; for (var i = 0; i < $scope.installer.current.model.minNonAlphaNumericLength; i++) { exp += ".*[\\W].*"; } //replace duplicates - exp = exp.replace(".*.*", ".*"); + exp = exp.replace(".*.*", ".*"); $scope.passwordPattern = new RegExp(exp); } @@ -23,5 +23,5 @@ angular.module("umbraco.install").controller("Umbraco.Install.UserController", f installerService.forward(); } }; - + }); diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/user.html b/src/Umbraco.Web.UI.Client/src/installer/steps/user.html index e314a16319..44ee16c840 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/steps/user.html +++ b/src/Umbraco.Web.UI.Client/src/installer/steps/user.html @@ -1,73 +1,85 @@
-

Install Umbraco

- -

Enter your name, email and password to install Umbraco with its default settings, alternatively you can customize your installation

- -
- -
-
-
-
- -
- -
-
- -
- -
- - Your email will be used as your login -
-
- -
- -
- - - At least {{installer.current.model.minCharLength}} characters long - - - At least {{installer.current.model.minNonAlphaNumericLength}} symbol{{installer.current.model.minNonAlphaNumericLength > 1 ? 's' : ''}} - -
-
- - -
-
- -
-
- -
-
- - - - -
-
- -
+

Install Umbraco

+

Enter your name, email and password for this Umbraco installation.

+ +
+
+
+
+ +
+
+
+
+ +
+ + Your email will be used as your login +
+
+
+ +
+ + + At least {{installer.current.model.minCharLength}} characters long + + At least {{installer.current.model.minNonAlphaNumericLength}} symbol{{installer.current.model.minNonAlphaNumericLength > 1 ? 's' : ''}} + +
+
+
+
+ +
+
- - +
+
+
+ Database Configuration +
+
+
+
+ +
+
{{installer.current.model.quickInstallSettings.displayName}}
+
+
+
+ +
+
{{installer.current.model.quickInstallSettings.defaultDatabaseName}}
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+ + +
+
+
+
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/less/installer.less b/src/Umbraco.Web.UI.Client/src/less/installer.less index 9ce519186a..46c15fcc89 100644 --- a/src/Umbraco.Web.UI.Client/src/less/installer.less +++ b/src/Umbraco.Web.UI.Client/src/less/installer.less @@ -53,17 +53,17 @@ body { opacity: 0.8; z-index: 777; } - #installer { margin: auto; background: @white; width: 80%; max-width: 750px; - height: 600px; + height: 640px; text-align: left; padding: 30px; - overflow: hidden; z-index: 667; + border-radius: 6px; + box-shadow: 0 2px 4px rgba(0,0,0,0.16); } #overlay { @@ -159,6 +159,14 @@ input.ng-dirty.ng-invalid { } } +#installer .input-readonly-text { + padding: 4px 6px; +} + +#installer legend { + clear: both; +} + .absolute-center { margin: auto; position: absolute; diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 65b6a1e0da..861393c376 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -11,11 +11,12 @@ true - - - - - + + + + + + diff --git a/tests/Umbraco.Tests.Benchmarks/ModelToSqlExpressionHelperBenchmarks.cs b/tests/Umbraco.Tests.Benchmarks/ModelToSqlExpressionHelperBenchmarks.cs index a75e9db8bc..7fd640ebb6 100644 --- a/tests/Umbraco.Tests.Benchmarks/ModelToSqlExpressionHelperBenchmarks.cs +++ b/tests/Umbraco.Tests.Benchmarks/ModelToSqlExpressionHelperBenchmarks.cs @@ -9,6 +9,7 @@ using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; +using Umbraco.Cms.Persistence.SqlServer.Services; namespace Umbraco.Tests.Benchmarks { diff --git a/tests/Umbraco.Tests.Benchmarks/SqlTemplatesBenchmark.cs b/tests/Umbraco.Tests.Benchmarks/SqlTemplatesBenchmark.cs index bd0c426fec..629da3cb25 100644 --- a/tests/Umbraco.Tests.Benchmarks/SqlTemplatesBenchmark.cs +++ b/tests/Umbraco.Tests.Benchmarks/SqlTemplatesBenchmark.cs @@ -7,6 +7,7 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; +using Umbraco.Cms.Persistence.SqlServer.Services; using Umbraco.Extensions; namespace Umbraco.Tests.Benchmarks diff --git a/tests/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj b/tests/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj index 52ba3c6a37..ca53cc8ed0 100644 --- a/tests/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj +++ b/tests/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj @@ -12,6 +12,7 @@ + diff --git a/tests/Umbraco.Tests.Common/TestHelperBase.cs b/tests/Umbraco.Tests.Common/TestHelperBase.cs index 5801e3291e..da23edc74f 100644 --- a/tests/Umbraco.Tests.Common/TestHelperBase.cs +++ b/tests/Umbraco.Tests.Common/TestHelperBase.cs @@ -86,8 +86,6 @@ namespace Umbraco.Cms.Tests.Common public IVariationContextAccessor VariationContextAccessor { get; } = new TestVariationContextAccessor(); - public abstract IDbProviderFactoryCreator DbProviderFactoryCreator { get; } - public abstract IBulkSqlInsertProvider BulkSqlInsertProvider { get; } public abstract IMarchal Marchal { get; } diff --git a/tests/Umbraco.Tests.Common/TestHelpers/TestDatabase.cs b/tests/Umbraco.Tests.Common/TestHelpers/TestDatabase.cs index 1eec4f5ae7..72f22e9960 100644 --- a/tests/Umbraco.Tests.Common/TestHelpers/TestDatabase.cs +++ b/tests/Umbraco.Tests.Common/TestHelpers/TestDatabase.cs @@ -17,6 +17,7 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; +using Umbraco.Cms.Persistence.SqlServer.Services; namespace Umbraco.Cms.Tests.Common.TestHelpers { diff --git a/tests/Umbraco.Tests.Common/Umbraco.Tests.Common.csproj b/tests/Umbraco.Tests.Common/Umbraco.Tests.Common.csproj index fffcad5a32..f03331bb49 100644 --- a/tests/Umbraco.Tests.Common/Umbraco.Tests.Common.csproj +++ b/tests/Umbraco.Tests.Common/Umbraco.Tests.Common.csproj @@ -22,6 +22,7 @@ + diff --git a/tests/Umbraco.Tests.Integration.SqlCe/Umbraco.Infrastructure/Persistence/DatabaseBuilderTests.cs b/tests/Umbraco.Tests.Integration.SqlCe/Umbraco.Infrastructure/Persistence/DatabaseBuilderTests.cs deleted file mode 100644 index 500d71dddc..0000000000 --- a/tests/Umbraco.Tests.Integration.SqlCe/Umbraco.Infrastructure/Persistence/DatabaseBuilderTests.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.IO; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using NPoco; -using NUnit.Framework; -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Configuration; -using Umbraco.Cms.Core.Events; -using Umbraco.Cms.Infrastructure.Migrations.Install; -using Umbraco.Cms.Infrastructure.Persistence; -using Umbraco.Cms.Tests.Common.TestHelpers; -using Umbraco.Cms.Tests.Common.Testing; -using Umbraco.Cms.Tests.Integration.Testing; - -namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence -{ - [TestFixture] - [UmbracoTest] - [Platform("Win")] - public class DatabaseBuilderTests : UmbracoIntegrationTest - { - private IDbProviderFactoryCreator DbProviderFactoryCreator => GetRequiredService(); - private IUmbracoDatabaseFactory UmbracoDatabaseFactory => GetRequiredService(); - private IDatabaseCreator EmbeddedDatabaseCreator => GetRequiredService(); - - public DatabaseBuilderTests() - { - TestOptionAttributeBase.ScanAssemblies.Add(typeof(DatabaseBuilderTests).Assembly); - } - - [Test] - public void CreateDatabase() - { - var path = TestContext.CurrentContext.TestDirectory.Split("bin")[0]; - AppDomain.CurrentDomain.SetData("DataDirectory", path); - const string dbFile = "DatabaseContextTests.sdf"; - // delete database file - // NOTE: using a custom db file for this test since we're re-using the one created with BaseDatabaseFactoryTest - var filePath = string.Concat(path, dbFile); - if (File.Exists(filePath)) - File.Delete(filePath); - - var connectionString = $"Datasource=|DataDirectory|{dbFile};Flush Interval=1"; - - UmbracoDatabaseFactory.Configure(connectionString, Constants.DbProviderNames.SqlCe); - DbProviderFactoryCreator.CreateDatabase(Constants.DbProviderNames.SqlCe, connectionString); - UmbracoDatabaseFactory.CreateDatabase(); - - // test get database type (requires an actual database) - using (var database = UmbracoDatabaseFactory.CreateDatabase()) - { - var databaseType = database.DatabaseType; - Assert.AreEqual(DatabaseType.SQLCe, databaseType); - } - - // create application context - //var appCtx = new ApplicationContext( - // _databaseFactory, - // new ServiceContext(migrationEntryService: Mock.Of()), - // CacheHelper.CreateDisabledCacheHelper(), - // new ProfilingLogger(Mock.Of(), Mock.Of())); - - // create the umbraco database - DatabaseSchemaCreator schemaHelper; - using (var database = UmbracoDatabaseFactory.CreateDatabase()) - using (var transaction = database.GetTransaction()) - { - schemaHelper = new DatabaseSchemaCreator(database, Mock.Of>(), NullLoggerFactory.Instance, new UmbracoVersion(), Mock.Of()); - schemaHelper.InitializeDatabaseSchema(); - transaction.Complete(); - } - - var umbracoNodeTable = schemaHelper.TableExists("umbracoNode"); - var umbracoUserTable = schemaHelper.TableExists("umbracoUser"); - var cmsTagsTable = schemaHelper.TableExists("cmsTags"); - - Assert.That(umbracoNodeTable, Is.True); - Assert.That(umbracoUserTable, Is.True); - Assert.That(cmsTagsTable, Is.True); - } - - } -} diff --git a/tests/Umbraco.Tests.Integration.SqlCe/Umbraco.Tests.Integration.SqlCe.csproj b/tests/Umbraco.Tests.Integration.SqlCe/Umbraco.Tests.Integration.SqlCe.csproj deleted file mode 100644 index 2c05ea6bf9..0000000000 --- a/tests/Umbraco.Tests.Integration.SqlCe/Umbraco.Tests.Integration.SqlCe.csproj +++ /dev/null @@ -1,32 +0,0 @@ - - - - net6.0 - false - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - diff --git a/tests/Umbraco.Tests.Integration/GlobalSetupTeardown.cs b/tests/Umbraco.Tests.Integration/GlobalSetupTeardown.cs index c952fcc663..3c5929877b 100644 --- a/tests/Umbraco.Tests.Integration/GlobalSetupTeardown.cs +++ b/tests/Umbraco.Tests.Integration/GlobalSetupTeardown.cs @@ -2,12 +2,12 @@ // See LICENSE for more details. using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Text; +using System.Linq; +using Microsoft.Extensions.Configuration; using NUnit.Framework; +using Umbraco.Cms.Tests.Integration.Implementations; using Umbraco.Cms.Tests.Integration.Testing; - // ReSharper disable once CheckNamespace /// @@ -19,19 +19,39 @@ using Umbraco.Cms.Tests.Integration.Testing; [SetUpFixture] public class GlobalSetupTeardown { + public static IConfiguration TestConfiguration { get; private set; } + private Stopwatch _stopwatch; [OneTimeSetUp] public void SetUp() { + var builder = new ConfigurationBuilder(); + builder.AddJsonFile("appsettings.Tests.json"); + builder.AddJsonFile("appsettings.Tests.Local.json", optional: true); + builder.AddEnvironmentVariables(); + TestConfiguration = builder.Build(); + + var testHelper = new TestHelper(); + var databaseType = TestConfiguration.GetValue("Tests:Database:DatabaseType"); + var version = testHelper.GetUmbracoVersion().SemanticVersion; + + TestContext.Progress.WriteLine($"******************************************************************************"); + TestContext.Progress.WriteLine($"* Umbraco.Tests.Integration"); + TestContext.Progress.WriteLine($"*"); + TestContext.Progress.WriteLine($"* DatabaseType : {databaseType}"); + TestContext.Progress.WriteLine($"* UmbracoVersion : {version.ToString().Split('+').First()}"); + TestContext.Progress.WriteLine($"* WorkingDirectory : {testHelper.WorkingDirectory}"); + TestContext.Progress.WriteLine($"******************************************************************************"); + _stopwatch = Stopwatch.StartNew(); } [OneTimeTearDown] public void TearDown() { - LocalDbTestDatabase.Instance?.Finish(); - SqlDeveloperTestDatabase.Instance?.Finish(); + BaseTestDatabase.Instance?.TearDown(); + Console.WriteLine("TOTAL TESTS DURATION: {0}", _stopwatch.Elapsed); } } diff --git a/tests/Umbraco.Tests.Integration/Implementations/TestHelper.cs b/tests/Umbraco.Tests.Integration/Implementations/TestHelper.cs index f8afe1d6ae..4abace502b 100644 --- a/tests/Umbraco.Tests.Integration/Implementations/TestHelper.cs +++ b/tests/Umbraco.Tests.Integration/Implementations/TestHelper.cs @@ -32,6 +32,7 @@ using Umbraco.Cms.Core.Net; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Runtime; using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Persistence.SqlServer.Services; using Umbraco.Cms.Tests.Common; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Web.Common.AspNetCore; @@ -103,9 +104,6 @@ namespace Umbraco.Cms.Tests.Integration.Implementations public IWebHostEnvironment GetWebHostEnvironment() => _hostEnvironment; - public override IDbProviderFactoryCreator DbProviderFactoryCreator => - new SqlServerDbProviderFactoryCreator(DbProviderFactories.GetFactory, Options.Create(new GlobalSettings())); - public override IBulkSqlInsertProvider BulkSqlInsertProvider => new SqlServerBulkSqlInsertProvider(); public override IMarchal Marchal { get; } = new AspNetCoreMarchal(); diff --git a/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs b/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs index 8c7eabadde..3c0e0aeb81 100644 --- a/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs +++ b/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs @@ -19,6 +19,8 @@ using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Persistence.Sqlite; +using Umbraco.Cms.Persistence.SqlServer; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.DependencyInjection; using Umbraco.Cms.Tests.Integration.Testing; @@ -139,6 +141,7 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest context.HostingEnvironment = TestHelper.GetWebHostEnvironment(); configBuilder.Sources.Clear(); configBuilder.AddInMemoryCollection(InMemoryConfiguration); + configBuilder.AddConfiguration(GlobalSetupTeardown.TestConfiguration); Configuration = configBuilder.Build(); }) @@ -224,6 +227,8 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest }) .AddWebServer() .AddWebsite() + .AddUmbracoSqlServerSupport() + .AddUmbracoSqliteSupport() .AddTestServices(TestHelper) // This is the important one! .Build(); } diff --git a/tests/Umbraco.Tests.Integration/Testing/BaseTestDatabase.cs b/tests/Umbraco.Tests.Integration/Testing/BaseTestDatabase.cs index 47d81e3c11..1afe45bc90 100644 --- a/tests/Umbraco.Tests.Integration/Testing/BaseTestDatabase.cs +++ b/tests/Umbraco.Tests.Integration/Testing/BaseTestDatabase.cs @@ -5,36 +5,33 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Data; +using System.Data.Common; using System.Diagnostics; -using System.Linq; using System.Threading; -using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; -using Moq; -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Configuration; -using Umbraco.Cms.Core.Events; -using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Persistence; namespace Umbraco.Cms.Tests.Integration.Testing { public abstract class BaseTestDatabase { + public static bool IsSqlite() => BaseTestDatabase.Instance is SqliteTestDatabase; + public static bool IsSqlServer() => BaseTestDatabase.Instance is SqlServerBaseTestDatabase; + protected ILoggerFactory _loggerFactory; protected IUmbracoDatabaseFactory _databaseFactory; protected IList _testDatabases; - - protected UmbracoDatabase.CommandInfo[] _cachedDatabaseInitCommands = new UmbracoDatabase.CommandInfo[0]; - protected BlockingCollection _prepareQueue; protected BlockingCollection _readySchemaQueue; protected BlockingCollection _readyEmptyQueue; + public static BaseTestDatabase Instance { get; private set; } + + public BaseTestDatabase() => Instance = this; protected abstract void Initialize(); - public TestDbMeta AttachEmpty() + public virtual TestDbMeta AttachEmpty() { if (_prepareQueue == null) { @@ -44,7 +41,7 @@ namespace Umbraco.Cms.Tests.Integration.Testing return _readyEmptyQueue.Take(); } - public TestDbMeta AttachSchema() + public virtual TestDbMeta AttachSchema() { if (_prepareQueue == null) { @@ -54,110 +51,47 @@ namespace Umbraco.Cms.Tests.Integration.Testing return _readySchemaQueue.Take(); } - public void Detach(TestDbMeta meta) + public virtual void Detach(TestDbMeta meta) { _prepareQueue.TryAdd(meta); } - protected void PrepareDatabase() => + protected virtual void PrepareDatabase() => Retry(10, () => + { + while (_prepareQueue.IsCompleted == false) { - while (_prepareQueue.IsCompleted == false) + TestDbMeta meta; + try { - TestDbMeta meta; - try - { - meta = _prepareQueue.Take(); - } - catch (InvalidOperationException) - { - continue; - } + meta = _prepareQueue.Take(); + } + catch (InvalidOperationException) + { + continue; + } - using (var conn = new SqlConnection(meta.ConnectionString)) - using (SqlCommand cmd = conn.CreateCommand()) + ResetTestDatabase(meta); + + if (!meta.IsEmpty) + { + using (var conn = GetConnection(meta)) { conn.Open(); - ResetTestDatabase(cmd); - - if (!meta.IsEmpty) + using (var cmd = conn.CreateCommand()) { RebuildSchema(cmd, meta); } } - if (!meta.IsEmpty) - { - _readySchemaQueue.TryAdd(meta); - } - else - { - _readyEmptyQueue.TryAdd(meta); - } + _readySchemaQueue.TryAdd(meta); + } + else + { + _readyEmptyQueue.TryAdd(meta); } - }); - - private void RebuildSchema(IDbCommand command, TestDbMeta meta) - { - lock (_cachedDatabaseInitCommands) - { - if (!_cachedDatabaseInitCommands.Any()) - { - RebuildSchemaFirstTime(meta); - return; } - } - - foreach (UmbracoDatabase.CommandInfo dbCommand in _cachedDatabaseInitCommands) - { - if (dbCommand.Text.StartsWith("SELECT ")) - { - continue; - } - - command.CommandText = dbCommand.Text; - command.Parameters.Clear(); - - foreach (UmbracoDatabase.ParameterInfo parameterInfo in dbCommand.Parameters) - { - AddParameter(command, parameterInfo); - } - - command.ExecuteNonQuery(); - } - } - - private void RebuildSchemaFirstTime(TestDbMeta meta) - { - _databaseFactory.Configure(meta.ConnectionString, Constants.DatabaseProviders.SqlServer); - - using (var database = (UmbracoDatabase)_databaseFactory.CreateDatabase()) - { - database.LogCommands = true; - - using (NPoco.ITransaction transaction = database.GetTransaction()) - { - var schemaCreator = new DatabaseSchemaCreator(database, _loggerFactory.CreateLogger(), _loggerFactory, new UmbracoVersion(), Mock.Of()); - schemaCreator.InitializeDatabaseSchema(); - - transaction.Complete(); - - _cachedDatabaseInitCommands = database.Commands.ToArray(); - } - } - } - - protected static void SetCommand(SqlCommand command, string sql, params object[] args) - { - command.CommandType = CommandType.Text; - command.CommandText = sql; - command.Parameters.Clear(); - - for (int i = 0; i < args.Length; i++) - { - command.Parameters.AddWithValue("@" + i, args[i]); - } - } + }); protected static void AddParameter(IDbCommand cmd, UmbracoDatabase.ParameterInfo parameterInfo) { @@ -169,32 +103,11 @@ namespace Umbraco.Cms.Tests.Integration.Testing cmd.Parameters.Add(p); } - protected static void ResetTestDatabase(IDbCommand cmd) - { - // https://stackoverflow.com/questions/536350 - cmd.CommandType = CommandType.Text; - cmd.CommandText = @" - declare @n char(1); - set @n = char(10); - declare @stmt nvarchar(max); - -- check constraints - select @stmt = isnull( @stmt + @n, '' ) + - 'alter table [' + schema_name(schema_id) + '].[' + object_name( parent_object_id ) + '] drop constraint [' + name + ']' - from sys.check_constraints; - -- foreign keys - select @stmt = isnull( @stmt + @n, '' ) + - 'alter table [' + schema_name(schema_id) + '].[' + object_name( parent_object_id ) + '] drop constraint [' + name + ']' - from sys.foreign_keys; - -- tables - select @stmt = isnull( @stmt + @n, '' ) + - 'drop table [' + schema_name(schema_id) + '].[' + name + ']' - from sys.tables; - exec sp_executesql @stmt; - "; + protected abstract DbConnection GetConnection(TestDbMeta meta); - // rudimentary retry policy since a db can still be in use when we try to drop - Retry(10, () => cmd.ExecuteNonQuery()); - } + protected abstract void RebuildSchema(IDbCommand command, TestDbMeta meta); + + protected abstract void ResetTestDatabase(TestDbMeta meta); protected static void Retry(int maxIterations, Action action) { @@ -205,7 +118,7 @@ namespace Umbraco.Cms.Tests.Integration.Testing action(); return; } - catch (SqlException) + catch (DbException ex) { // Console.Error.WriteLine($"SqlException occured, but we try again {i+1}/{maxIterations}.\n{e}"); // This can occur when there's a transaction deadlock which means (i think) that the database is still in use and hasn't been closed properly yet @@ -223,5 +136,7 @@ namespace Umbraco.Cms.Tests.Integration.Testing } } } + + public abstract void TearDown(); } } diff --git a/tests/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs b/tests/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs index 59ddfe55e8..30c306bbee 100644 --- a/tests/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs +++ b/tests/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs @@ -15,7 +15,7 @@ namespace Umbraco.Cms.Tests.Integration.Testing /// /// Manages a pool of LocalDb databases for integration testing /// - public class LocalDbTestDatabase : BaseTestDatabase, ITestDatabase + public class LocalDbTestDatabase : SqlServerBaseTestDatabase, ITestDatabase { public const string InstanceName = "UmbracoTests"; public const string DatabaseName = "UmbracoTests"; @@ -25,19 +25,15 @@ namespace Umbraco.Cms.Tests.Integration.Testing private static LocalDb.Instance s_localDbInstance; private static string s_filesPath; - public static LocalDbTestDatabase Instance { get; private set; } - // It's internal because `Umbraco.Core.Persistence.LocalDb` is internal - internal LocalDbTestDatabase(TestDatabaseSettings settings, ILoggerFactory loggerFactory, LocalDb localDb, string filesPath, IUmbracoDatabaseFactory dbFactory) + internal LocalDbTestDatabase(TestDatabaseSettings settings, ILoggerFactory loggerFactory, LocalDb localDb, IUmbracoDatabaseFactory dbFactory) { _loggerFactory = loggerFactory; _databaseFactory = dbFactory; _settings = settings; _localDb = localDb; - s_filesPath = filesPath; - - Instance = this; // For GlobalSetupTeardown.cs + s_filesPath = settings.FilesPath; var counter = 0; @@ -90,7 +86,7 @@ namespace Umbraco.Cms.Tests.Integration.Testing } } - public void Finish() + public override void TearDown() { if (_prepareQueue == null) { diff --git a/tests/Umbraco.Tests.Integration/Testing/SqlServerBaseTestDatabase.cs b/tests/Umbraco.Tests.Integration/Testing/SqlServerBaseTestDatabase.cs new file mode 100644 index 0000000000..10d24eb01b --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Testing/SqlServerBaseTestDatabase.cs @@ -0,0 +1,119 @@ +using System; +using System.Data; +using System.Data.Common; +using System.Linq; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Logging; +using Moq; +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Infrastructure.Migrations.Install; +using Umbraco.Cms.Infrastructure.Persistence; + +namespace Umbraco.Cms.Tests.Integration.Testing; + +public abstract class SqlServerBaseTestDatabase : BaseTestDatabase +{ + + protected UmbracoDatabase.CommandInfo[] _cachedDatabaseInitCommands = new UmbracoDatabase.CommandInfo[0]; + + protected override void ResetTestDatabase(TestDbMeta meta) + { + using var connection = GetConnection(meta); + connection.Open(); + + using (var cmd = connection.CreateCommand()) + { + // https://stackoverflow.com/questions/536350 + cmd.CommandType = CommandType.Text; + cmd.CommandText = @" + declare @n char(1); + set @n = char(10); + declare @stmt nvarchar(max); + -- check constraints + select @stmt = isnull( @stmt + @n, '' ) + + 'alter table [' + schema_name(schema_id) + '].[' + object_name( parent_object_id ) + '] drop constraint [' + name + ']' + from sys.check_constraints; + -- foreign keys + select @stmt = isnull( @stmt + @n, '' ) + + 'alter table [' + schema_name(schema_id) + '].[' + object_name( parent_object_id ) + '] drop constraint [' + name + ']' + from sys.foreign_keys; + -- tables + select @stmt = isnull( @stmt + @n, '' ) + + 'drop table [' + schema_name(schema_id) + '].[' + name + ']' + from sys.tables; + exec sp_executesql @stmt; + "; + + // rudimentary retry policy since a db can still be in use when we try to drop + Retry(10, () => cmd.ExecuteNonQuery()); + } + } + + protected static void SetCommand(SqlCommand command, string sql, params object[] args) + { + command.CommandType = CommandType.Text; + command.CommandText = sql; + command.Parameters.Clear(); + + for (int i = 0; i < args.Length; i++) + { + command.Parameters.AddWithValue("@" + i, args[i]); + } + } + + + + protected override DbConnection GetConnection(TestDbMeta meta) => new SqlConnection(meta.ConnectionString); + + protected override void RebuildSchema(IDbCommand command, TestDbMeta meta) + { + lock (_cachedDatabaseInitCommands) + { + if (!_cachedDatabaseInitCommands.Any()) + { + RebuildSchemaFirstTime(meta); + return; + } + } + + foreach (UmbracoDatabase.CommandInfo dbCommand in _cachedDatabaseInitCommands) + { + command.CommandText = dbCommand.Text; + command.Parameters.Clear(); + + foreach (UmbracoDatabase.ParameterInfo parameterInfo in dbCommand.Parameters) + { + AddParameter(command, parameterInfo); + } + + command.ExecuteNonQuery(); + } + } + + private void RebuildSchemaFirstTime(TestDbMeta meta) + { + _databaseFactory.Configure(meta.ToStronglyTypedConnectionString()); + + using (var database = (UmbracoDatabase)_databaseFactory.CreateDatabase()) + { + database.LogCommands = true; + + using (NPoco.ITransaction transaction = database.GetTransaction()) + { + var schemaCreator = new DatabaseSchemaCreator( + database, + _loggerFactory.CreateLogger(), _loggerFactory, + new UmbracoVersion(), + Mock.Of()); + schemaCreator.InitializeDatabaseSchema(); + + transaction.Complete(); + + _cachedDatabaseInitCommands = database.Commands + .Where(x => !x.Text.StartsWith("SELECT ", StringComparison.OrdinalIgnoreCase)) + .ToArray(); + } + } + } +} diff --git a/tests/Umbraco.Tests.Integration/Testing/SqlDeveloperTestDatabase.cs b/tests/Umbraco.Tests.Integration/Testing/SqlServerTestDatabase.cs similarity index 75% rename from tests/Umbraco.Tests.Integration/Testing/SqlDeveloperTestDatabase.cs rename to tests/Umbraco.Tests.Integration/Testing/SqlServerTestDatabase.cs index 9daa55a1cb..378ee28c53 100644 --- a/tests/Umbraco.Tests.Integration/Testing/SqlDeveloperTestDatabase.cs +++ b/tests/Umbraco.Tests.Integration/Testing/SqlServerTestDatabase.cs @@ -16,33 +16,30 @@ namespace Umbraco.Cms.Tests.Integration.Testing /// /// It's not meant to be pretty, rushed port of LocalDb.cs + LocalDbTestDatabase.cs /// - public class SqlDeveloperTestDatabase : BaseTestDatabase, ITestDatabase + public class SqlServerTestDatabase : SqlServerBaseTestDatabase, ITestDatabase { private readonly TestDatabaseSettings _settings; - private readonly string _masterConnectionString; public const string DatabaseName = "UmbracoTests"; - public static SqlDeveloperTestDatabase Instance { get; private set; } - - public SqlDeveloperTestDatabase(TestDatabaseSettings settings, ILoggerFactory loggerFactory, IUmbracoDatabaseFactory databaseFactory, string masterConnectionString) + public SqlServerTestDatabase(TestDatabaseSettings settings, ILoggerFactory loggerFactory, + IUmbracoDatabaseFactory databaseFactory) { _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); _databaseFactory = databaseFactory ?? throw new ArgumentNullException(nameof(databaseFactory)); _settings = settings; - _masterConnectionString = masterConnectionString; var counter = 0; var schema = Enumerable.Range(0, _settings.SchemaDatabaseCount) - .Select(x => TestDbMeta.CreateWithMasterConnectionString($"{DatabaseName}-{++counter}", false, masterConnectionString)); + .Select(x => TestDbMeta.CreateWithMasterConnectionString($"{DatabaseName}-{++counter}", false, + _settings.SQLServerMasterConnectionString)); var empty = Enumerable.Range(0, _settings.EmptyDatabasesCount) - .Select(x => TestDbMeta.CreateWithMasterConnectionString($"{DatabaseName}-{++counter}", true, masterConnectionString)); + .Select(x => TestDbMeta.CreateWithMasterConnectionString($"{DatabaseName}-{++counter}", true, + _settings.SQLServerMasterConnectionString)); _testDatabases = schema.Concat(empty).ToList(); - - Instance = this; // For GlobalSetupTeardown.cs } protected override void Initialize() @@ -66,7 +63,9 @@ namespace Umbraco.Cms.Tests.Integration.Testing private void CreateDatabase(TestDbMeta meta) { - using (var connection = new SqlConnection(_masterConnectionString)) + Drop(meta); + + using (var connection = new SqlConnection(_settings.SQLServerMasterConnectionString)) { connection.Open(); using (SqlCommand command = connection.CreateCommand()) @@ -79,14 +78,21 @@ namespace Umbraco.Cms.Tests.Integration.Testing private void Drop(TestDbMeta meta) { - using (var connection = new SqlConnection(_masterConnectionString)) + using (var connection = new SqlConnection(_settings.SQLServerMasterConnectionString)) { connection.Open(); using (SqlCommand command = connection.CreateCommand()) { + SetCommand(command, "select count(1) from sys.databases where name = @0", meta.Name); + var records = (int)command.ExecuteScalar(); + if (records == 0) + { + return; + } + string sql = $@" - ALTER DATABASE{LocalDb.QuotedName(meta.Name)} - SET SINGLE_USER + ALTER DATABASE {LocalDb.QuotedName(meta.Name)} + SET SINGLE_USER WITH ROLLBACK IMMEDIATE"; SetCommand(command, sql); command.ExecuteNonQuery(); @@ -97,7 +103,7 @@ namespace Umbraco.Cms.Tests.Integration.Testing } } - public void Finish() + public override void TearDown() { if (_prepareQueue == null) { diff --git a/tests/Umbraco.Tests.Integration/Testing/SqliteTestDatabase.cs b/tests/Umbraco.Tests.Integration/Testing/SqliteTestDatabase.cs new file mode 100644 index 0000000000..d8274c4a53 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Testing/SqliteTestDatabase.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Concurrent; +using System.Data; +using System.Data.Common; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Data.Sqlite; +using Microsoft.Extensions.Logging; +using Moq; +using NPoco; +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Infrastructure.Migrations.Install; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.Mappers; +using Umbraco.Cms.Persistence.Sqlite.Mappers; +using Umbraco.Cms.Persistence.Sqlite.Services; + +namespace Umbraco.Cms.Tests.Integration.Testing; + +public class SqliteTestDatabase : BaseTestDatabase, ITestDatabase +{ + private readonly TestDatabaseSettings _settings; + private readonly TestUmbracoDatabaseFactoryProvider _dbFactoryProvider; + public const string DatabaseName = "UmbracoTests"; + + protected UmbracoDatabase.CommandInfo[] _cachedDatabaseInitCommands = new UmbracoDatabase.CommandInfo[0]; + + public SqliteTestDatabase(TestDatabaseSettings settings, TestUmbracoDatabaseFactoryProvider dbFactoryProvider, + ILoggerFactory loggerFactory) + { + _settings = settings ?? throw new ArgumentNullException(nameof(settings)); + _dbFactoryProvider = dbFactoryProvider; + _databaseFactory = dbFactoryProvider.Create(); + _loggerFactory = loggerFactory; + + var schema = Enumerable.Range(0, _settings.SchemaDatabaseCount) + .Select(x => CreateSqLiteMeta(false)); + + var empty = Enumerable.Range(0, _settings.EmptyDatabasesCount) + .Select(x => CreateSqLiteMeta(true)); + + _testDatabases = schema.Concat(empty).ToList(); + } + + protected override void Initialize() + { + _prepareQueue = new BlockingCollection(); + _readySchemaQueue = new BlockingCollection(); + _readyEmptyQueue = new BlockingCollection(); + + foreach (TestDbMeta meta in _testDatabases) + { + _prepareQueue.Add(meta); + } + + for (var i = 0; i < _settings.PrepareThreadCount; i++) + { + var thread = new Thread(PrepareDatabase); + thread.Start(); + } + } + + protected override void ResetTestDatabase(TestDbMeta meta) + { + // Database survives in memory until all connections closed. + meta.Connection = GetConnection(meta); + meta.Connection.Open(); + } + + public override void Detach(TestDbMeta meta) + { + meta.Connection.Close(); + _prepareQueue.TryAdd(CreateSqLiteMeta(meta.IsEmpty)); + } + + protected override DbConnection GetConnection(TestDbMeta meta) => new SqliteConnection(meta.ConnectionString); + + protected override void RebuildSchema(IDbCommand command, TestDbMeta meta) + { + using var connection = GetConnection(meta); + connection.Open(); + + lock (_cachedDatabaseInitCommands) + { + if (!_cachedDatabaseInitCommands.Any()) + { + RebuildSchemaFirstTime(meta); + return; + } + } + + // Get NPoco to handle all the type mappings (e.g. dates) for us. + var database = new Database(connection, DatabaseType.SQLite); + database.BeginTransaction(); + + database.Mappers.Add(new NullableDateMapper()); + database.Mappers.Add(new SqlitePocoGuidMapper()); + + foreach (UmbracoDatabase.CommandInfo dbCommand in _cachedDatabaseInitCommands) + { + database.Execute(dbCommand.Text, dbCommand.Parameters.Select(x => x.Value).ToArray()); + } + + database.CompleteTransaction(); + } + + private void RebuildSchemaFirstTime(TestDbMeta meta) + { + var dbFactory = _dbFactoryProvider.Create(); + dbFactory.Configure(meta.ToStronglyTypedConnectionString()); + + using var database = (UmbracoDatabase)dbFactory.CreateDatabase(); + database.LogCommands = true; + + using NPoco.ITransaction transaction = database.GetTransaction(); + + var schemaCreator = new DatabaseSchemaCreator( + database, + _loggerFactory.CreateLogger(), _loggerFactory, + new UmbracoVersion(), + Mock.Of()); + + schemaCreator.InitializeDatabaseSchema(); + transaction.Complete(); + + _cachedDatabaseInitCommands = database.Commands + .Where(x => !x.Text.StartsWith("SELECT ", StringComparison.OrdinalIgnoreCase)) + .ToArray(); + } + + public override void TearDown() + { + if (_prepareQueue == null) + { + return; + } + + _prepareQueue.CompleteAdding(); + while (_prepareQueue.TryTake(out _)) { } + + _readyEmptyQueue.CompleteAdding(); + while (_readyEmptyQueue.TryTake(out _)) { } + + _readySchemaQueue.CompleteAdding(); + while (_readySchemaQueue.TryTake(out _)) { } + } + + private TestDbMeta CreateSqLiteMeta(bool empty) + { + var builder = new SqliteConnectionStringBuilder() + { + DataSource = $"{Guid.NewGuid()}", + Mode = SqliteOpenMode.Memory, + ForeignKeys = true, + Pooling = false, // When pooling true, files kept open after connections closed, bad for cleanup. + Cache = SqliteCacheMode.Shared, + }; + + return new TestDbMeta(builder.DataSource, empty, builder.ConnectionString, Persistence.Sqlite.Constants.ProviderName, "InMemory"); + } +} diff --git a/tests/Umbraco.Tests.Integration/Testing/TestDatabaseFactory.cs b/tests/Umbraco.Tests.Integration/Testing/TestDatabaseFactory.cs index 5f14a4928d..c746e52e6c 100644 --- a/tests/Umbraco.Tests.Integration/Testing/TestDatabaseFactory.cs +++ b/tests/Umbraco.Tests.Integration/Testing/TestDatabaseFactory.cs @@ -8,24 +8,38 @@ using Umbraco.Cms.Infrastructure.Persistence; namespace Umbraco.Cms.Tests.Integration.Testing { - public class TestDatabaseFactory + public static class TestDatabaseFactory { - public static ITestDatabase Create(TestDatabaseSettings settings, string filesPath, ILoggerFactory loggerFactory, TestUmbracoDatabaseFactoryProvider dbFactory) - { - string connectionString = Environment.GetEnvironmentVariable("UmbracoIntegrationTestConnectionString"); - - return string.IsNullOrEmpty(connectionString) - ? CreateLocalDb(settings, filesPath, loggerFactory, dbFactory) - : CreateSqlDeveloper(settings, loggerFactory, dbFactory, connectionString); - } - - private static ITestDatabase CreateLocalDb(TestDatabaseSettings settings, string filesPath, ILoggerFactory loggerFactory, TestUmbracoDatabaseFactoryProvider dbFactory) - { - if (!Directory.Exists(filesPath)) + /// + /// Creates a TestDatabase instance + /// + /// + /// SQL Server setup requires configured master connection string & privileges to create database. + /// + /// + /// + /// # SQL Server Environment variable setup + /// $ export Tests__Database__DatabaseType="SqlServer" + /// $ export Tests__Database__SQLServerMasterConnectionString="Server=localhost,1433; User Id=sa; Password=MySuperSecretPassword123!;" + /// + /// + /// + /// + /// # Docker cheat sheet + /// $ docker run -e 'ACCEPT_EULA=Y' -e "SA_PASSWORD=MySuperSecretPassword123!" -e 'MSSQL_PID=Developer' -p 1433:1433 -d mcr.microsoft.com/mssql/server:2017-latest-ubuntu + /// + /// + public static ITestDatabase Create(TestDatabaseSettings settings, TestUmbracoDatabaseFactoryProvider dbFactory, ILoggerFactory loggerFactory) => + settings.DatabaseType switch { - Directory.CreateDirectory(filesPath); - } + TestDatabaseSettings.TestDatabaseType.Sqlite=> new SqliteTestDatabase(settings, dbFactory, loggerFactory), + TestDatabaseSettings.TestDatabaseType.SqlServer => CreateSqlServer(settings, loggerFactory, dbFactory), + TestDatabaseSettings.TestDatabaseType.LocalDb => CreateLocalDb(settings, loggerFactory, dbFactory), + _ => throw new ApplicationException("Unsupported test database provider") + }; + private static ITestDatabase CreateLocalDb(TestDatabaseSettings settings, ILoggerFactory loggerFactory, TestUmbracoDatabaseFactoryProvider dbFactory) + { var localDb = new LocalDb(); if (!localDb.IsAvailable) @@ -33,21 +47,12 @@ namespace Umbraco.Cms.Tests.Integration.Testing throw new InvalidOperationException("LocalDB is not available."); } - return new LocalDbTestDatabase(settings, loggerFactory, localDb, filesPath, dbFactory.Create()); + return new LocalDbTestDatabase(settings, loggerFactory, localDb, dbFactory.Create()); } - private static ITestDatabase CreateSqlDeveloper(TestDatabaseSettings settings, ILoggerFactory loggerFactory, TestUmbracoDatabaseFactoryProvider dbFactory, string connectionString) + private static ITestDatabase CreateSqlServer(TestDatabaseSettings settings, ILoggerFactory loggerFactory, TestUmbracoDatabaseFactoryProvider dbFactory) { - // NOTE: Example setup for Linux box. - // $ export SA_PASSWORD=Foobar123! - // $ export UmbracoIntegrationTestConnectionString="Server=localhost,1433;User Id=sa;Password=$SA_PASSWORD;" - // $ docker run -e 'ACCEPT_EULA=Y' -e "SA_PASSWORD=$SA_PASSWORD" -e 'MSSQL_PID=Developer' -p 1433:1433 -d mcr.microsoft.com/mssql/server:2017-latest-ubuntu - if (string.IsNullOrEmpty(connectionString)) - { - throw new InvalidOperationException("ENV: UmbracoIntegrationTestConnectionString is not set"); - } - - return new SqlDeveloperTestDatabase(settings, loggerFactory, dbFactory.Create(), connectionString); + return new SqlServerTestDatabase(settings, loggerFactory, dbFactory.Create()); } } } diff --git a/tests/Umbraco.Tests.Integration/Testing/TestDatabaseSettings.cs b/tests/Umbraco.Tests.Integration/Testing/TestDatabaseSettings.cs index b2c9b0cfa2..3387c9d3e7 100644 --- a/tests/Umbraco.Tests.Integration/Testing/TestDatabaseSettings.cs +++ b/tests/Umbraco.Tests.Integration/Testing/TestDatabaseSettings.cs @@ -3,10 +3,27 @@ namespace Umbraco.Cms.Tests.Integration.Testing { public class TestDatabaseSettings { + public TestDatabaseType DatabaseType { get; set; } + public int PrepareThreadCount { get; set; } public int SchemaDatabaseCount { get; set; } public int EmptyDatabasesCount { get; set; } + + public string FilesPath { get; set; } + + /// + /// Only used for SQL Server e.g. on Linux/MacOS (not required for localdb). + /// + public string SQLServerMasterConnectionString { get; set; } + + public enum TestDatabaseType + { + Unknown, + Sqlite, + SqlServer, + LocalDb + } } } diff --git a/tests/Umbraco.Tests.Integration/Testing/TestDbMeta.cs b/tests/Umbraco.Tests.Integration/Testing/TestDbMeta.cs index 8e3dd355d5..d4395b4461 100644 --- a/tests/Umbraco.Tests.Integration/Testing/TestDbMeta.cs +++ b/tests/Umbraco.Tests.Integration/Testing/TestDbMeta.cs @@ -1,23 +1,28 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using System.Data.Common; using System.Text.RegularExpressions; +using Umbraco.Cms.Core.Configuration.Models; namespace Umbraco.Cms.Tests.Integration.Testing { public class TestDbMeta { public string Name { get; } - public bool IsEmpty { get; } - public string ConnectionString { get; set; } + public string Provider { get; set; } + public string Path { get; set; } // Null if not embedded. + public DbConnection Connection { get; set; } // for SQLite in memory, can move to subclass later. - private TestDbMeta(string name, bool isEmpty, string connectionString) + public TestDbMeta(string name, bool isEmpty, string connectionString, string providerName, string path) { IsEmpty = isEmpty; Name = name; ConnectionString = connectionString; + Provider = providerName; + Path = path; } private static string ConstructConnectionString(string masterConnectionString, string databaseName) @@ -28,10 +33,18 @@ namespace Umbraco.Cms.Tests.Integration.Testing } public static TestDbMeta CreateWithMasterConnectionString(string name, bool isEmpty, string masterConnectionString) => - new TestDbMeta(name, isEmpty, ConstructConnectionString(masterConnectionString, name)); + new TestDbMeta(name, isEmpty, ConstructConnectionString(masterConnectionString, name), Persistence.SqlServer.Constants.ProviderName, null); // LocalDb mdf funtimes public static TestDbMeta CreateWithoutConnectionString(string name, bool isEmpty) => - new TestDbMeta(name, isEmpty, null); + new TestDbMeta(name, isEmpty, null, Persistence.SqlServer.Constants.ProviderName, null); + + public ConnectionStrings ToStronglyTypedConnectionString() => + new ConnectionStrings + { + Name = Name, + ConnectionString = ConnectionString, + ProviderName = Provider + }; } } diff --git a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index dd9fd3fe4e..2fd07b131d 100644 --- a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -20,6 +20,8 @@ using Umbraco.Cms.Core.Web; using Umbraco.Cms.Infrastructure.DependencyInjection; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Scoping; +using Umbraco.Cms.Persistence.Sqlite; +using Umbraco.Cms.Persistence.SqlServer; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Integration.DependencyInjection; using Umbraco.Cms.Tests.Integration.Extensions; @@ -42,7 +44,7 @@ namespace Umbraco.Cms.Tests.Integration.Testing [SetUp] public void Setup() { - InMemoryConfiguration[Constants.Configuration.ConfigUnattended + ":" + nameof(UnattendedSettings.InstallUnattended)] = "true"; + InMemoryConfiguration[Core.Constants.Configuration.ConfigUnattended + ":" + nameof(UnattendedSettings.InstallUnattended)] = "true"; IHostBuilder hostBuilder = CreateHostBuilder(); _host = hostBuilder.Build(); @@ -74,6 +76,7 @@ namespace Umbraco.Cms.Tests.Integration.Testing context.HostingEnvironment = TestHelper.GetWebHostEnvironment(); configBuilder.Sources.Clear(); configBuilder.AddInMemoryCollection(InMemoryConfiguration); + configBuilder.AddConfiguration(GlobalSetupTeardown.TestConfiguration); Configuration = configBuilder.Build(); }) @@ -96,7 +99,6 @@ namespace Umbraco.Cms.Tests.Integration.Testing protected void ConfigureServices(IServiceCollection services) { services.AddUnique(CreateLoggerFactory()); - services.AddSingleton(TestHelper.DbProviderFactoryCreator); services.AddTransient(); IWebHostEnvironment webHostEnvironment = TestHelper.GetWebHostEnvironment(); services.AddRequiredNetCoreServices(TestHelper, webHostEnvironment); @@ -125,6 +127,8 @@ namespace Umbraco.Cms.Tests.Integration.Testing .AddBackOfficeIdentity() .AddMembersIdentity() .AddExamine() + .AddUmbracoSqlServerSupport() + .AddUmbracoSqliteSupport() .AddTestServices(TestHelper); if (TestOptions.Mapper) diff --git a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestBase.cs b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestBase.cs index b77ec1806f..237bf87daa 100644 --- a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestBase.cs +++ b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestBase.cs @@ -8,9 +8,11 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; using NUnit.Framework; using Serilog; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Tests.Common.Testing; @@ -116,105 +118,109 @@ public abstract class UmbracoIntegrationTestBase TestUmbracoDatabaseFactoryProvider testDatabaseFactoryProvider = serviceProvider.GetRequiredService(); IUmbracoDatabaseFactory databaseFactory = serviceProvider.GetRequiredService(); ILoggerFactory loggerFactory = serviceProvider.GetRequiredService(); + IOptionsMonitor connectionStrings = serviceProvider.GetRequiredService>(); // This will create a db, install the schema and ensure the app is configured to run - SetupTestDatabase(testDatabaseFactoryProvider, databaseFactory, loggerFactory, state, TestHelper.WorkingDirectory); + SetupTestDatabase(testDatabaseFactoryProvider, connectionStrings, databaseFactory, loggerFactory, state); } - private void ConfigureTestDatabaseFactory(TestDbMeta meta, IUmbracoDatabaseFactory factory, IRuntimeState state) + private void ConfigureTestDatabaseFactory( + TestDbMeta meta, + IUmbracoDatabaseFactory factory, + IRuntimeState state, + IOptionsMonitor connectionStrings) { // It's just been pulled from container and wasn't used to create test database Assert.IsFalse(factory.Configured); - factory.Configure(meta.ConnectionString, Constants.DatabaseProviders.SqlServer); + factory.Configure(meta.ToStronglyTypedConnectionString()); + connectionStrings.CurrentValue.ConnectionString = meta.ConnectionString; + connectionStrings.CurrentValue.ProviderName = meta.Provider; state.DetermineRuntimeLevel(); } private void SetupTestDatabase( - TestUmbracoDatabaseFactoryProvider testUmbracoDatabaseFactoryProvider, - IUmbracoDatabaseFactory databaseFactory, - ILoggerFactory loggerFactory, - IRuntimeState runtimeState, - string workingDirectory) - { - if (TestOptions.Database == UmbracoTestOptions.Database.None) + TestUmbracoDatabaseFactoryProvider testUmbracoDatabaseFactoryProvider, + IOptionsMonitor connectionStrings, + IUmbracoDatabaseFactory databaseFactory, + ILoggerFactory loggerFactory, + IRuntimeState runtimeState) { - return; - } + if (TestOptions.Database == UmbracoTestOptions.Database.None) + { + return; + } - // need to manually register this factory - DbProviderFactories.RegisterFactory(Constants.DbProviderNames.SqlServer, SqlClientFactory.Instance); + ITestDatabase db = GetOrCreateDatabase(loggerFactory, testUmbracoDatabaseFactoryProvider); - var dbFilePath = Path.Combine(workingDirectory, "LocalDb"); + switch (TestOptions.Database) + { + case UmbracoTestOptions.Database.NewSchemaPerTest: - ITestDatabase db = GetOrCreateDatabase(dbFilePath, loggerFactory, testUmbracoDatabaseFactoryProvider); - - switch (TestOptions.Database) - { - case UmbracoTestOptions.Database.NewSchemaPerTest: - - // New DB + Schema - TestDbMeta newSchemaDbMeta = db.AttachSchema(); - - // Add teardown callback - AddOnTestTearDown(() => db.Detach(newSchemaDbMeta)); - - ConfigureTestDatabaseFactory(newSchemaDbMeta, databaseFactory, runtimeState); - - Assert.AreEqual(RuntimeLevel.Run, runtimeState.Level); - - break; - case UmbracoTestOptions.Database.NewEmptyPerTest: - TestDbMeta newEmptyDbMeta = db.AttachEmpty(); - - // Add teardown callback - AddOnTestTearDown(() => db.Detach(newEmptyDbMeta)); - - ConfigureTestDatabaseFactory(newEmptyDbMeta, databaseFactory, runtimeState); - - Assert.AreEqual(RuntimeLevel.Install, runtimeState.Level); - - break; - case UmbracoTestOptions.Database.NewSchemaPerFixture: - // Only attach schema once per fixture - // Doing it more than once will block the process since the old db hasn't been detached - // and it would be the same as NewSchemaPerTest even if it didn't block - if (_firstTestInFixture) - { // New DB + Schema - TestDbMeta newSchemaFixtureDbMeta = db.AttachSchema(); - s_fixtureDbMeta = newSchemaFixtureDbMeta; + TestDbMeta newSchemaDbMeta = db.AttachSchema(); // Add teardown callback - AddOnFixtureTearDown(() => db.Detach(newSchemaFixtureDbMeta)); - } + AddOnTestTearDown(() => db.Detach(newSchemaDbMeta)); - ConfigureTestDatabaseFactory(s_fixtureDbMeta, databaseFactory, runtimeState); + ConfigureTestDatabaseFactory(newSchemaDbMeta, databaseFactory, runtimeState, connectionStrings); - break; - case UmbracoTestOptions.Database.NewEmptyPerFixture: - // Only attach schema once per fixture - // Doing it more than once will block the process since the old db hasn't been detached - // and it would be the same as NewSchemaPerTest even if it didn't block - if (_firstTestInFixture) - { - // New DB + Schema - TestDbMeta newEmptyFixtureDbMeta = db.AttachEmpty(); - s_fixtureDbMeta = newEmptyFixtureDbMeta; + Assert.AreEqual(RuntimeLevel.Run, runtimeState.Level); + + break; + case UmbracoTestOptions.Database.NewEmptyPerTest: + TestDbMeta newEmptyDbMeta = db.AttachEmpty(); // Add teardown callback - AddOnFixtureTearDown(() => db.Detach(newEmptyFixtureDbMeta)); - } + AddOnTestTearDown(() => db.Detach(newEmptyDbMeta)); - ConfigureTestDatabaseFactory(s_fixtureDbMeta, databaseFactory, runtimeState); + ConfigureTestDatabaseFactory(newEmptyDbMeta, databaseFactory, runtimeState, connectionStrings); - break; - default: - throw new ArgumentOutOfRangeException(nameof(TestOptions), TestOptions, null); + Assert.AreEqual(RuntimeLevel.Install, runtimeState.Level); + + break; + case UmbracoTestOptions.Database.NewSchemaPerFixture: + // Only attach schema once per fixture + // Doing it more than once will block the process since the old db hasn't been detached + // and it would be the same as NewSchemaPerTest even if it didn't block + if (_firstTestInFixture) + { + // New DB + Schema + TestDbMeta newSchemaFixtureDbMeta = db.AttachSchema(); + s_fixtureDbMeta = newSchemaFixtureDbMeta; + + // Add teardown callback + AddOnFixtureTearDown(() => db.Detach(newSchemaFixtureDbMeta)); + } + + ConfigureTestDatabaseFactory(s_fixtureDbMeta, databaseFactory, runtimeState, connectionStrings); + + break; + case UmbracoTestOptions.Database.NewEmptyPerFixture: + // Only attach schema once per fixture + // Doing it more than once will block the process since the old db hasn't been detached + // and it would be the same as NewSchemaPerTest even if it didn't block + if (_firstTestInFixture) + { + // New DB + Schema + TestDbMeta newEmptyFixtureDbMeta = db.AttachEmpty(); + s_fixtureDbMeta = newEmptyFixtureDbMeta; + + // Add teardown callback + AddOnFixtureTearDown(() => db.Detach(newEmptyFixtureDbMeta)); + } + + ConfigureTestDatabaseFactory(s_fixtureDbMeta, databaseFactory, runtimeState, connectionStrings); + + break; + default: + throw new ArgumentOutOfRangeException(nameof(TestOptions), TestOptions, null); + } } - } - private static ITestDatabase GetOrCreateDatabase(string filesPath, ILoggerFactory loggerFactory, TestUmbracoDatabaseFactoryProvider dbFactory) + + private ITestDatabase GetOrCreateDatabase(ILoggerFactory loggerFactory, + TestUmbracoDatabaseFactoryProvider dbFactory) { lock (s_dbLocker) { @@ -223,15 +229,19 @@ public abstract class UmbracoIntegrationTestBase return s_dbInstance; } - // TODO: pull from IConfiguration var settings = new TestDatabaseSettings { - PrepareThreadCount = 4, - EmptyDatabasesCount = 2, - SchemaDatabaseCount = 4 + FilesPath = Path.Combine(TestHelper.WorkingDirectory, "databases"), + DatabaseType = Configuration.GetValue("Tests:Database:DatabaseType"), + PrepareThreadCount = Configuration.GetValue("Tests:Database:PrepareThreadCount"), + EmptyDatabasesCount = Configuration.GetValue("Tests:Database:EmptyDatabasesCount"), + SchemaDatabaseCount = Configuration.GetValue("Tests:Database:SchemaDatabaseCount"), + SQLServerMasterConnectionString = Configuration.GetValue("Tests:Database:SQLServerMasterConnectionString"), }; - s_dbInstance = TestDatabaseFactory.Create(settings, filesPath, loggerFactory, dbFactory); + Directory.CreateDirectory(settings.FilesPath); + + s_dbInstance = TestDatabaseFactory.Create(settings, dbFactory, loggerFactory); return s_dbInstance; } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs index 19d4080524..c2e5cc6c4f 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Migrations/AdvancedMigrationTests.cs @@ -18,6 +18,7 @@ using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Migrations.Upgrade; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Tests.Common.TestHelpers; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; @@ -47,7 +48,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Migrations return new CreateTableOfTDtoMigration(c); }); - using (IScope scope = ScopeProvider.CreateScope()) + using (ScopeProvider.CreateScope(autoComplete: true)) { var upgrader = new Upgrader( new MigrationPlan("test") @@ -56,11 +57,10 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Migrations upgrader.Execute(MigrationPlanExecutor, ScopeProvider, Mock.Of()); - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, LoggerFactory.CreateLogger(), LoggerFactory, UmbracoVersion, EventAggregator); - bool exists = helper.TableExists("umbracoUser"); - Assert.IsTrue(exists); + var db = ScopeAccessor.AmbientScope.Database; + var exists = ScopeAccessor.AmbientScope.SqlContext.SqlSyntax.DoesTableExist(db, "umbracoUser"); - scope.Complete(); + Assert.IsTrue(exists); } } @@ -99,6 +99,13 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Migrations [Test] public void CreateKeysAndIndexesOfTDto() { + if (BaseTestDatabase.IsSqlite()) + { + // TODO: Think about this for future migrations. + Assert.Ignore("Can't add / drop keys in SQLite."); + return; + } + IMigrationBuilder builder = Mock.Of(); Mock.Get(builder) .Setup(x => x.Build(It.IsAny(), It.IsAny())) @@ -134,6 +141,13 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Migrations [Test] public void CreateKeysAndIndexes() { + if (BaseTestDatabase.IsSqlite()) + { + // TODO: Think about this for future migrations. + Assert.Ignore("Can't add / drop keys in SQLite."); + return; + } + IMigrationBuilder builder = Mock.Of(); Mock.Get(builder) .Setup(x => x.Build(It.IsAny(), It.IsAny())) @@ -167,7 +181,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Migrations } [Test] - public void CreateColumn() + public void AddColumn() { IMigrationBuilder builder = Mock.Of(); Mock.Get(builder) @@ -179,22 +193,33 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Migrations case "CreateTableOfTDtoMigration": return new CreateTableOfTDtoMigration(c); case "CreateColumnMigration": - return new CreateColumnMigration(c); + return new AddColumnMigration(c); default: throw new NotSupportedException(); } }); - using (IScope scope = ScopeProvider.CreateScope()) + using (ScopeProvider.CreateScope(autoComplete: true)) { var upgrader = new Upgrader( new MigrationPlan("test") .From(string.Empty) .To("a") - .To("done")); + .To("done")); upgrader.Execute(MigrationPlanExecutor, ScopeProvider, Mock.Of()); - scope.Complete(); + + var db = ScopeAccessor.AmbientScope.Database; + + var columnInfo = ScopeAccessor.AmbientScope.SqlContext.SqlSyntax.GetColumnsInSchema(db) + .Where(x => x.TableName == "umbracoUser") + .FirstOrDefault(x => x.ColumnName == "Foo"); + + Assert.Multiple(() => + { + Assert.NotNull(columnInfo); + Assert.IsTrue(columnInfo.DataType.Contains("nvarchar")); + }); } } @@ -273,24 +298,16 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Migrations } } - public class CreateColumnMigration : MigrationBase + public class AddColumnMigration : MigrationBase { - public CreateColumnMigration(IMigrationContext context) + public AddColumnMigration(IMigrationContext context) : base(context) { } protected override void Migrate() { - // cannot delete the column without this, of course - Delete.KeysAndIndexes("umbracoUser").Do(); - - Delete.Column("id").FromTable("umbracoUser").Do(); - - TableDefinition table = DefinitionFactory.GetTableDefinition(typeof(UserDto), SqlSyntax); - ColumnDefinition column = table.Columns.First(x => x.Name == "id"); - string create = SqlSyntax.Format(column); // returns [id] INTEGER NOT NULL IDENTITY(1060,1) - Database.Execute($"ALTER TABLE {SqlSyntax.GetQuotedTableName("umbracoUser")} ADD " + create); + Database.Execute($"ALTER TABLE {SqlSyntax.GetQuotedTableName("umbracoUser")} ADD Foo nvarchar(255)"); } } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Packaging/CreatedPackageSchemaTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Packaging/CreatedPackageSchemaTests.cs index 6a5ee88426..c07f23ecb2 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Packaging/CreatedPackageSchemaTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Packaging/CreatedPackageSchemaTests.cs @@ -53,10 +53,10 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Packaging packageDefinition.Name = "UpdatedName"; CreatedPackageSchemaRepository.SavePackage(packageDefinition); - var result = CreatedPackageSchemaRepository.GetAll().ToList(); + var results = CreatedPackageSchemaRepository.GetAll().ToList(); - Assert.AreEqual(result.Count, 1); - Assert.AreEqual(result.FirstOrDefault()?.Name, "UpdatedName"); + Assert.AreEqual(expected: 1, actual: results.Count); + Assert.AreEqual(expected: "UpdatedName", actual: results.FirstOrDefault()?.Name); } } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs index e27163bf3b..1214098915 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs @@ -4,13 +4,16 @@ using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; -using Microsoft.Data.SqlClient; +using Microsoft.Extensions.DependencyInjection; using NPoco; using NUnit.Framework; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.DistributedLocking.Exceptions; using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Persistence.Sqlite.Interceptors; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; +using Umbraco.Extensions; namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { @@ -19,6 +22,12 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Logger = UmbracoTestOptions.Logger.Console)] public class LocksTests : UmbracoIntegrationTest { + protected override void ConfigureTestServices(IServiceCollection services) + { + // SQLite + retry policy makes tests fail, we retry before throwing distributed locking timeout. + services.RemoveAll(x => x.ImplementationType == typeof(SqliteAddRetryPolicyInterceptor)); + } + [SetUp] protected void SetUp() { @@ -47,6 +56,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence [Test] public void ConcurrentReadersTest() { + const int threadCount = 8; var threads = new Thread[threadCount]; var exceptions = new Exception[threadCount]; @@ -145,6 +155,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence [Test] public void ConcurrentWritersTest() { + const int threadCount = 8; var threads = new Thread[threadCount]; var exceptions = new Exception[threadCount]; @@ -239,6 +250,12 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence [Test] public void DeadLockTest() { + if (BaseTestDatabase.IsSqlite()) + { + Assert.Ignore("This test doesn't work with Microsoft.Data.Sqlite - SELECT * FROM sys.dm_tran_locks;"); + return; + } + Exception e1 = null, e2 = null; AutoResetEvent ev1 = new AutoResetEvent(false), ev2 = new AutoResetEvent(false); @@ -264,7 +281,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence //Assert.IsNotNull(e1); if (e1 != null) { - AssertIsSqlLockException(e1); + AssertIsDistributedLockingTimeoutException(e1); } // the assertion below depends on timing conditions - on a fast enough environment, @@ -275,17 +292,17 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence //Assert.IsNull(e2); if (e2 != null) { - AssertIsSqlLockException(e2); + AssertIsDistributedLockingTimeoutException(e2); } } - private void AssertIsSqlLockException(Exception e) + private void AssertIsDistributedLockingTimeoutException(Exception e) { - var sqlException = e as SqlException; + var sqlException = e as DistributedLockingTimeoutException; Assert.IsNotNull(sqlException); - Assert.AreEqual(1222, sqlException.Number); } + private void DeadLockTestThread(int id1, int id2, EventWaitHandle myEv, WaitHandle otherEv, ref Exception exception) { using (var scope = ScopeProvider.CreateScope()) @@ -327,6 +344,13 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence [Test] public void NoDeadLockTest() { + if (BaseTestDatabase.IsSqlite()) + { + Assert.Ignore("This test doesn't work with Microsoft.Data.Sqlite - SELECT * FROM sys.dm_tran_locks;"); + return; + } + + Exception e1 = null, e2 = null; AutoResetEvent ev1 = new AutoResetEvent(false), ev2 = new AutoResetEvent(false); @@ -353,13 +377,56 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence Assert.IsNull(e2); } - [Test] - public void Throws_When_Lock_Timeout_Is_Exceeded() + [Test] + public void Throws_When_Lock_Timeout_Is_Exceeded_Read() + { + if (BaseTestDatabase.IsSqlite()) + { + // Reader reads snapshot, isolated from the writer. + Assert.Ignore("Doesn't apply to SQLite with journal_mode=wal"); + } + + using (ExecutionContext.SuppressFlow()) + { + var t1 = Task.Run(() => + { + using (var scope = ScopeProvider.CreateScope()) + { + Console.WriteLine("Write lock A"); + // This will acquire right away + scope.EagerWriteLock(TimeSpan.FromMilliseconds(2000), Constants.Locks.ContentTree); + Thread.Sleep(6000); // Wait longer than the Read Lock B timeout + scope.Complete(); + Console.WriteLine("Finished Write lock A"); + } + }); + + Thread.Sleep(500); // 100% sure task 1 starts first + + var t2 = Task.Run(() => + { + using (var scope = ScopeProvider.CreateScope()) + { + Console.WriteLine("Read lock B"); + + // This will wait for the write lock to release but it isn't going to wait long + // enough so an exception will be thrown. + Assert.Throws(() => + scope.EagerReadLock(TimeSpan.FromMilliseconds(3000), Constants.Locks.ContentTree)); + scope.Complete(); + Console.WriteLine("Finished Read lock B"); + } + }); + + Task.WaitAll(t1, t2); + } + } + + [Test] + public void Throws_When_Lock_Timeout_Is_Exceeded_Write() { using (ExecutionContext.SuppressFlow()) { - - var t1 = Task.Run(() => { using (var scope = ScopeProvider.CreateScope()) @@ -380,40 +447,31 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { using (var scope = ScopeProvider.CreateScope()) { - Console.WriteLine("Read lock B"); + Console.WriteLine("Write lock B"); // This will wait for the write lock to release but it isn't going to wait long // enough so an exception will be thrown. - Assert.Throws(() => - scope.EagerReadLock(TimeSpan.FromMilliseconds(3000), Constants.Locks.ContentTree)); - scope.Complete(); - Console.WriteLine("Finished Read lock B"); - } - }); - - var t3 = Task.Run(() => - { - using (var scope = ScopeProvider.CreateScope()) - { - Console.WriteLine("Write lock C"); - - // This will wait for the write lock to release but it isn't going to wait long - // enough so an exception will be thrown. - Assert.Throws(() => + Assert.Throws(() => scope.EagerWriteLock(TimeSpan.FromMilliseconds(3000), Constants.Locks.ContentTree)); scope.Complete(); - Console.WriteLine("Finished Write lock C"); + Console.WriteLine("Finished Write lock B"); } }); - Task.WaitAll(t1, t2, t3); + Task.WaitAll(t1, t2); } } [Test] public void Read_Lock_Waits_For_Write_Lock() { + if (BaseTestDatabase.IsSqlite()) + { + // Reader reads snapshot, isolated from the writer. + Assert.Ignore("Doesn't apply to SQLite with journal_mode=wal"); + } + var locksCompleted = 0; using (ExecutionContext.SuppressFlow()) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoFetchTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoFetchTests.cs index 19976da976..4954d7a371 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoFetchTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoFetchTests.cs @@ -438,7 +438,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.NPoco var k1 = new ThingA12Dto { Name = "a", Thing1Id = tA1A.Id, Thing2Id = tA2A.Id }; ScopeAccessor.AmbientScope.Database.Insert(k1); - var k2 = new ThingA12Dto { Name = "B", Thing1Id = tA1A.Id, Thing2Id = tA2B.Id }; + var k2 = new ThingA12Dto { Name = "b", Thing1Id = tA1A.Id, Thing2Id = tA2B.Id }; ScopeAccessor.AmbientScope.Database.Insert(k2); string sql = @"SELECT a1.id, a1.name, diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs index 2059564cd8..55409fe6d7 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs @@ -25,6 +25,7 @@ using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Cms.Infrastructure.Scoping; +using Umbraco.Cms.Persistence.SqlServer.Services; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MacroRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MacroRepositoryTest.cs index 498e5b10e2..bb1e04fe91 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MacroRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MacroRepositoryTest.cs @@ -2,6 +2,7 @@ // See LICENSE for more details. using System.Collections.Generic; +using System.Data.Common; using System.Linq; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; @@ -41,7 +42,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos var macro = new Macro(ShortStringHelper, "test1", "Test", "~/views/macropartials/test.cshtml"); - Assert.Throws(() => repository.Save(macro)); + Assert.That(() => repository.Save(macro), Throws.InstanceOf()); } } @@ -57,7 +58,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos IMacro macro = repository.Get(1); macro.Alias = "test2"; - Assert.Throws(() => repository.Save(macro)); + Assert.That(() => repository.Save(macro), Throws.InstanceOf()); } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ServerRegistrationRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ServerRegistrationRepositoryTest.cs index ff4ced61ee..bc7058ed58 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ServerRegistrationRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ServerRegistrationRepositoryTest.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Data.Common; using System.Linq; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; @@ -44,7 +45,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos var server = new ServerRegistration("http://shazwazza.com", "COMPUTER1", DateTime.Now); - Assert.Throws(() => repository.Save(server)); + Assert.That(() => repository.Save(server), Throws.InstanceOf()); } } @@ -60,7 +61,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos IServerRegistration server = repository.Get(1); server.ServerIdentity = "COMPUTER2"; - Assert.Throws(() => repository.Save(server)); + Assert.That(() => repository.Save(server), Throws.InstanceOf()); } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SchemaValidationTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SchemaValidationTest.cs index 90f6fab9e1..cd5c45868e 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SchemaValidationTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SchemaValidationTest.cs @@ -9,10 +9,11 @@ using Umbraco.Cms.Tests.Integration.Testing; namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { [TestFixture] - [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewEmptyPerTest)] public class SchemaValidationTest : UmbracoIntegrationTest { private IUmbracoVersion UmbracoVersion => GetRequiredService(); + private IEventAggregator EventAggregator => GetRequiredService(); [Test] @@ -20,9 +21,10 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence { DatabaseSchemaResult result; - using (var scope = ScopeProvider.CreateScope()) + using (ScopeProvider.CreateScope(autoComplete: true)) { var schema = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, LoggerFactory.CreateLogger(), LoggerFactory, UmbracoVersion, EventAggregator); + schema.InitializeDatabaseSchema(); result = schema.ValidateSchema(DatabaseSchemaCreator.OrderedTables); } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SqlServerTableByTableTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SqlServerTableByTableTest.cs deleted file mode 100644 index 8c34202fe4..0000000000 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SqlServerTableByTableTest.cs +++ /dev/null @@ -1,508 +0,0 @@ -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using NUnit.Framework; -using Umbraco.Cms.Core.Configuration; -using Umbraco.Cms.Core.Events; -using Umbraco.Cms.Infrastructure.Migrations.Install; -using Umbraco.Cms.Infrastructure.Persistence.Dtos; -using Umbraco.Cms.Tests.Common.Testing; -using Umbraco.Cms.Tests.Integration.Testing; - -namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence -{ - [TestFixture] - [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] - public class SqlServerTableByTableTest : UmbracoIntegrationTest - { - private IUmbracoVersion UmbracoVersion => GetRequiredService(); - private static ILoggerFactory _loggerFactory = NullLoggerFactory.Instance; - private IEventAggregator EventAggregator => GetRequiredService(); - - [Test] - public void Can_Create_umbracoNode_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_umbracoAccess_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_umbracoAccessRule_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_cmsContentType2ContentType_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_cmsContentTypeAllowedContentType_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_cmsContentType_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_ContentVersion_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_cmsDataType_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_cmsDictionary_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_cmsLanguageText_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_cmsTemplate_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_Document_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_DocumentType_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_umbracoDomains_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_umbracoLogViewerQuery_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_umbracoLanguage_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_umbracoLog_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_cmsMacro_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_cmsMember_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_cmsMember2MemberGroup_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_cmsMemberType_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_PropertyData_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_cmsPropertyType_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_cmsPropertyTypeGroup_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_umbracoRelation_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_umbracoRelationType_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_cmsTags_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_cmsTagRelationship_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_umbracoUser_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_umbracoUserGroup_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_umbracoUser2NodeNotify_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - public void Can_Create_umbracoGroupUser2app_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - - [Test] - public void Can_Create_umbracoUserGroup2NodePermission_Table() - { - using (var scope = ScopeProvider.CreateScope()) - { - var helper = new DatabaseSchemaCreator(ScopeAccessor.AmbientScope.Database, _loggerFactory.CreateLogger(), _loggerFactory, UmbracoVersion, EventAggregator); - - helper.CreateTable(); - helper.CreateTable(); - helper.CreateTable(); - - scope.Complete(); - } - } - } -} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SyntaxProvider/SqlServerSyntaxProviderTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SyntaxProvider/SqlServerSyntaxProviderTests.cs index a90d357b0e..0fae2991a9 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SyntaxProvider/SqlServerSyntaxProviderTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/SyntaxProvider/SqlServerSyntaxProviderTests.cs @@ -15,6 +15,7 @@ using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; +using Umbraco.Cms.Persistence.SqlServer.Services; using Umbraco.Cms.Tests.Common.TestHelpers; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; @@ -56,11 +57,14 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Synta var sqlOutput = SqlContext.SqlSyntax.GetDeleteSubquery("cmsContentNu", "nodeId", subQuery); - Assert.AreEqual(@"DELETE FROM [cmsContentNu] WHERE [nodeId] IN (SELECT [nodeId] FROM (SELECT DISTINCT cmsContentNu.nodeId -FROM [cmsContentNu] -INNER JOIN [umbracoNode] -ON [cmsContentNu].[nodeId] = [umbracoNode].[id] -WHERE (([umbracoNode].[nodeObjectType] = @0))) x)".Replace(Environment.NewLine, " ").Replace("\n", " ").Replace("\r", " "), + string t(string x) => SqlContext.SqlSyntax.GetQuotedTableName(x); + string c(string x) => SqlContext.SqlSyntax.GetQuotedColumnName(x); + + Assert.AreEqual(@$"DELETE FROM {t("cmsContentNu")} WHERE {c("nodeId")} IN (SELECT {c("nodeId")} FROM (SELECT DISTINCT cmsContentNu.nodeId +FROM {t("cmsContentNu")} +INNER JOIN {t("umbracoNode")} +ON {t("cmsContentNu")}.{c("nodeId")} = {t("umbracoNode")}.{c("id")} +WHERE (({t("umbracoNode")}.{c("nodeObjectType")} = @0))) x)".Replace(Environment.NewLine, " ").Replace("\n", " ").Replace("\r", " "), sqlOutput.SQL.Replace(Environment.NewLine, " ").Replace("\n", " ").Replace("\r", " ")); Assert.AreEqual(1, sqlOutput.Arguments.Length); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/UnitOfWorkTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/UnitOfWorkTests.cs index 640321b232..4eb670c4a8 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/UnitOfWorkTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/UnitOfWorkTests.cs @@ -3,7 +3,10 @@ using System; using NUnit.Framework; +using Umbraco.Cms.Core.DistributedLocking; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Persistence.Sqlite.Services; +using Umbraco.Cms.Persistence.SqlServer.Services; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; using Constants = Umbraco.Cms.Core.Constants; @@ -17,6 +20,12 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence [Test] public void ReadLockNonExisting() { + var lockingMechanism = GetRequiredService().DistributedLockingMechanism; + if (lockingMechanism is SqliteDistributedLockingMechanism) + { + Assert.Ignore("SqliteDistributedLockingMechanism doesn't query the umbracoLock table for read locks."); + } + IScopeProvider provider = ScopeProvider; Assert.Throws(() => { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs index ad7df3cee5..bfda211d74 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs @@ -650,7 +650,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith( document.Id, - "{'pd':{'value1':[{'c':'fr','v':'v1fr'},{'c':'en','v':'v1en'}],'value2':[{'v':'v2'}]},'cd':"); + "{'pd':{'value1':[{'c':'en','v':'v1en'},{'c':'fr','v':'v1fr'}],'value2':[{'v':'v2'}]},'cd':"); } [Test] @@ -825,7 +825,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith( document.Id, - "{'pd':{'value1':[{'c':'fr','v':'v1fr'},{'c':'en','v':'v1en'}],'value2':[{'v':'v2'}]},'cd':"); + "{'pd':{'value1':[{'c':'en','v':'v1en'},{'c':'fr','v':'v1fr'}],'value2':[{'v':'v2'}]},'cd':"); // switch other property to Culture contentType.PropertyTypes.First(x => x.Alias == "value2").Variations = ContentVariation.Culture; @@ -844,7 +844,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith( document.Id, - "{'pd':{'value1':[{'c':'fr','v':'v1fr'},{'c':'en','v':'v1en'}],'value2':[{'c':'en','v':'v2'}]},'cd':"); + "{'pd':{'value1':[{'c':'en','v':'v1en'},{'c':'fr','v':'v1fr'}],'value2':[{'c':'en','v':'v2'}]},'cd':"); } [TestCase(ContentVariation.Culture, ContentVariation.Nothing)] @@ -1106,7 +1106,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith( document.Id, - "{'pd':{'value11':[{'c':'fr','v':'v11fr'},{'c':'en','v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'v':'v21en'}],'value22':[{'v':'v22'}]},'cd':"); + "{'pd':{'value11':[{'c':'en','v':'v11en'},{'c':'fr','v':'v11fr'}],'value12':[{'v':'v12'}],'value21':[{'v':'v21en'}],'value22':[{'v':'v22'}]},'cd':"); composed.PropertyTypes.First(x => x.Alias == "value21").Variations = ContentVariation.Culture; ContentTypeService.Save(composed); @@ -1115,7 +1115,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith( document.Id, - "{'pd':{'value11':[{'c':'fr','v':'v11fr'},{'c':'en','v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'c':'fr','v':'v21fr'},{'c':'en','v':'v21en'}],'value22':[{'v':'v22'}]},'cd':"); + "{'pd':{'value11':[{'c':'en','v':'v11en'},{'c':'fr','v':'v11fr'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':"); composing.Variations = ContentVariation.Nothing; ContentTypeService.Save(composing); @@ -1124,7 +1124,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith( document.Id, - "{'pd':{'value11':[{'v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'c':'fr','v':'v21fr'},{'c':'en','v':'v21en'}],'value22':[{'v':'v22'}]},'cd':"); + "{'pd':{'value11':[{'v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':"); composing.Variations = ContentVariation.Culture; ContentTypeService.Save(composing); @@ -1133,7 +1133,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith( document.Id, - "{'pd':{'value11':[{'v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'c':'fr','v':'v21fr'},{'c':'en','v':'v21en'}],'value22':[{'v':'v22'}]},'cd':"); + "{'pd':{'value11':[{'v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':"); composing.PropertyTypes.First(x => x.Alias == "value11").Variations = ContentVariation.Culture; ContentTypeService.Save(composing); @@ -1142,7 +1142,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith( document.Id, - "{'pd':{'value11':[{'c':'fr','v':'v11fr'},{'c':'en','v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'c':'fr','v':'v21fr'},{'c':'en','v':'v21en'}],'value22':[{'v':'v22'}]},'cd':"); + "{'pd':{'value11':[{'c':'en','v':'v11en'},{'c':'fr','v':'v11fr'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':"); } [Test] @@ -1247,7 +1247,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Console.WriteLine(GetJson(document1.Id)); AssertJsonStartsWith( document1.Id, - "{'pd':{'value11':[{'c':'fr','v':'v11fr'},{'c':'en','v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'v':'v21en'}],'value22':[{'v':'v22'}]},'cd':"); + "{'pd':{'value11':[{'c':'en','v':'v11en'},{'c':'fr','v':'v11fr'}],'value12':[{'v':'v12'}],'value21':[{'v':'v21en'}],'value22':[{'v':'v22'}]},'cd':"); Console.WriteLine(GetJson(document2.Id)); AssertJsonStartsWith( @@ -1261,7 +1261,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Console.WriteLine(GetJson(document1.Id)); AssertJsonStartsWith( document1.Id, - "{'pd':{'value11':[{'c':'fr','v':'v11fr'},{'c':'en','v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'c':'fr','v':'v21fr'},{'c':'en','v':'v21en'}],'value22':[{'v':'v22'}]},'cd':"); + "{'pd':{'value11':[{'c':'en','v':'v11en'},{'c':'fr','v':'v11fr'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':"); Console.WriteLine(GetJson(document2.Id)); AssertJsonStartsWith( @@ -1275,7 +1275,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Console.WriteLine(GetJson(document1.Id)); AssertJsonStartsWith( document1.Id, - "{'pd':{'value11':[{'v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'c':'fr','v':'v21fr'},{'c':'en','v':'v21en'}],'value22':[{'v':'v22'}]},'cd':"); + "{'pd':{'value11':[{'v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':"); Console.WriteLine(GetJson(document2.Id)); AssertJsonStartsWith( @@ -1289,7 +1289,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Console.WriteLine(GetJson(document1.Id)); AssertJsonStartsWith( document1.Id, - "{'pd':{'value11':[{'v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'c':'fr','v':'v21fr'},{'c':'en','v':'v21en'}],'value22':[{'v':'v22'}]},'cd':"); + "{'pd':{'value11':[{'v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':"); Console.WriteLine(GetJson(document2.Id)); AssertJsonStartsWith( @@ -1303,7 +1303,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Console.WriteLine(GetJson(document1.Id)); AssertJsonStartsWith( document1.Id, - "{'pd':{'value11':[{'c':'fr','v':'v11fr'},{'c':'en','v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'c':'fr','v':'v21fr'},{'c':'en','v':'v21en'}],'value22':[{'v':'v22'}]},'cd':"); + "{'pd':{'value11':[{'c':'en','v':'v11en'},{'c':'fr','v':'v11fr'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':"); Console.WriteLine(GetJson(document2.Id)); AssertJsonStartsWith( diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ThreadSafetyServiceTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ThreadSafetyServiceTest.cs index 7611bea687..21d15c13a6 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ThreadSafetyServiceTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ThreadSafetyServiceTest.cs @@ -12,6 +12,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Implement; +using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; @@ -54,7 +55,11 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { using (IScope scope = ScopeProvider.CreateScope()) { - ScopeAccessor.AmbientScope.Database.Execute("SET LOCK_TIMEOUT 60000"); + if (ScopeAccessor.AmbientScope.Database.DatabaseType.IsSqlServer()) + { + ScopeAccessor.AmbientScope.Database.Execute("SET LOCK_TIMEOUT 60000"); + } + service.Save(content); scope.Complete(); } @@ -64,7 +69,11 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { using (IScope scope = ScopeProvider.CreateScope()) { - ScopeAccessor.AmbientScope.Database.Execute("SET LOCK_TIMEOUT 60000"); + if (ScopeAccessor.AmbientScope.Database.DatabaseType.IsSqlServer()) + { + ScopeAccessor.AmbientScope.Database.Execute("SET LOCK_TIMEOUT 60000"); + } + service.Save(media); scope.Complete(); } @@ -121,8 +130,10 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { try { - ConcurrentStack currentStack = ((ScopeProvider)ScopeProvider).GetCallContextScopeValue(); - log.LogInformation("[{ThreadId}] Current Stack? {CurrentStack}", Thread.CurrentThread.ManagedThreadId, currentStack?.Count); + ConcurrentStack + currentStack = ((ScopeProvider)ScopeProvider).GetCallContextScopeValue(); + log.LogInformation("[{ThreadId}] Current Stack? {CurrentStack}", + Thread.CurrentThread.ManagedThreadId, currentStack?.Count); // NOTE: This is NULL because we have supressed the execution context flow. // If we don't do that we will get various exceptions because we're trying to run concurrent threads @@ -135,7 +146,8 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services // So although the test passes in v8, there's actually some strange things occuring because Scopes // are being created and disposed concurrently and out of order. var currentScope = ScopeAccessor.AmbientScope; - log.LogInformation("[{ThreadId}] Current Scope? {CurrentScope}", Thread.CurrentThread.ManagedThreadId, currentScope?.GetDebugInfo()); + log.LogInformation("[{ThreadId}] Current Scope? {CurrentScope}", + Thread.CurrentThread.ManagedThreadId, currentScope?.GetDebugInfo()); Assert.IsNull(currentScope); string name1 = "test-" + Guid.NewGuid(); @@ -218,8 +230,10 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { try { - ConcurrentStack currentStack = ((ScopeProvider)ScopeProvider).GetCallContextScopeValue(); - log.LogInformation("[{ThreadId}] Current Stack? {CurrentStack}", Thread.CurrentThread.ManagedThreadId, currentStack?.Count); + ConcurrentStack + currentStack = ((ScopeProvider)ScopeProvider).GetCallContextScopeValue(); + log.LogInformation("[{ThreadId}] Current Stack? {CurrentStack}", + Thread.CurrentThread.ManagedThreadId, currentStack?.Count); // NOTE: This is NULL because we have supressed the execution context flow. // If we don't do that we will get various exceptions because we're trying to run concurrent threads @@ -232,7 +246,8 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services // So although the test passes in v8, there's actually some strange things occuring because Scopes // are being created and disposed concurrently and out of order. var currentScope = ScopeAccessor.AmbientScope; - log.LogInformation("[{ThreadId}] Current Scope? {CurrentScope}", Thread.CurrentThread.ManagedThreadId, currentScope?.GetDebugInfo()); + log.LogInformation("[{ThreadId}] Current Scope? {CurrentScope}", + Thread.CurrentThread.ManagedThreadId, currentScope?.GetDebugInfo()); Assert.IsNull(currentScope); string name1 = "test-" + Guid.NewGuid(); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj index 6070f468b1..103dbc3feb 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj +++ b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj @@ -78,6 +78,9 @@ + + PreserveNewest + @@ -98,6 +101,8 @@ + + diff --git a/tests/Umbraco.Tests.Integration/appsettings.Tests.json b/tests/Umbraco.Tests.Integration/appsettings.Tests.json new file mode 100644 index 0000000000..8580268550 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/appsettings.Tests.json @@ -0,0 +1,11 @@ +{ + "Tests": { + "Database": { + "DatabaseType": "SQLite", + "PrepareThreadCount": 4, + "SchemaDatabaseCount": 4, + "EmptyDatabasesCount": 2, + "SQLServerMasterConnectionString": "" + } + } +} diff --git a/tests/Umbraco.Tests.UnitTests/AutoFixture/Customizations/UmbracoCustomizations.cs b/tests/Umbraco.Tests.UnitTests/AutoFixture/Customizations/UmbracoCustomizations.cs index 672bbd0862..ea6c76225b 100644 --- a/tests/Umbraco.Tests.UnitTests/AutoFixture/Customizations/UmbracoCustomizations.cs +++ b/tests/Umbraco.Tests.UnitTests/AutoFixture/Customizations/UmbracoCustomizations.cs @@ -39,10 +39,6 @@ namespace Umbraco.Cms.Tests.UnitTests.AutoFixture.Customizations // When requesting an IUserStore ensure we actually uses a IUserLockoutStore fixture.Customize>(cc => cc.FromFactory(Mock.Of>)); - fixture.Customize( - u => u.FromFactory( - (a, b, c) => new ConfigConnectionString(a, b, c))); - fixture.Customize( u => u.FromFactory( () => new UmbracoVersion())); @@ -63,11 +59,6 @@ namespace Umbraco.Cms.Tests.UnitTests.AutoFixture.Customizations Mock.Of(x => x.ToAbsolute(It.IsAny()) == "/umbraco" && x.ApplicationVirtualPath == string.Empty), Mock.Of(x => x.Level == RuntimeLevel.Run)))); - var configConnectionString = new ConfigConnectionString( - "ss", - "Data Source=(localdb)\\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\\Umbraco.mdf;Integrated Security=True"); - fixture.Customize(x => x.FromFactory(() => configConnectionString)); - var httpContextAccessor = new HttpContextAccessor { HttpContext = new DefaultHttpContext() }; fixture.Customize(x => x.FromFactory(() => httpContextAccessor.HttpContext)); fixture.Customize(x => x.FromFactory(() => httpContextAccessor)); diff --git a/tests/Umbraco.Tests.UnitTests/TestHelpers/BaseUsingSqlSyntax.cs b/tests/Umbraco.Tests.UnitTests/TestHelpers/BaseUsingSqlSyntax.cs index ca7950a7cf..05ebc64c65 100644 --- a/tests/Umbraco.Tests.UnitTests/TestHelpers/BaseUsingSqlSyntax.cs +++ b/tests/Umbraco.Tests.UnitTests/TestHelpers/BaseUsingSqlSyntax.cs @@ -14,6 +14,7 @@ using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; +using Umbraco.Cms.Persistence.SqlServer.Services; using Umbraco.Extensions; namespace Umbraco.Cms.Tests.UnitTests.TestHelpers diff --git a/tests/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs b/tests/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs index 5c7d4ea3bf..3b13e3ca0c 100644 --- a/tests/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs +++ b/tests/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs @@ -38,6 +38,7 @@ using Umbraco.Cms.Infrastructure.Mail; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; +using Umbraco.Cms.Persistence.SqlServer.Services; using Umbraco.Cms.Tests.Common; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Extensions; @@ -61,8 +62,6 @@ namespace Umbraco.Cms.Tests.UnitTests.TestHelpers { } - public override IDbProviderFactoryCreator DbProviderFactoryCreator { get; } = Mock.Of(); - public override IBulkSqlInsertProvider BulkSqlInsertProvider { get; } = Mock.Of(); public override IMarchal Marchal { get; } = Mock.Of(); @@ -113,8 +112,6 @@ namespace Umbraco.Cms.Tests.UnitTests.TestHelpers public static IVariationContextAccessor VariationContextAccessor => s_testHelperInternal.VariationContextAccessor; - public static IDbProviderFactoryCreator DbProviderFactoryCreator => s_testHelperInternal.DbProviderFactoryCreator; - public static IBulkSqlInsertProvider BulkSqlInsertProvider => s_testHelperInternal.BulkSqlInsertProvider; public static IMarchal Marchal => s_testHelperInternal.Marchal; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Configuration/ConfigureConnectionStringsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Configuration/ConfigureConnectionStringsTests.cs new file mode 100644 index 0000000000..4e7d6f7f1f --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Configuration/ConfigureConnectionStringsTests.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Tests.UnitTests.AutoFixture; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Configuration; + +[TestFixture] +public class ConfigureConnectionStringsTests +{ + private const string UmbracoDbDsn = Constants.System.UmbracoConnectionName; + + private IOptionsSnapshot GetOptions(IDictionary configValues = null) + { + var configurationBuilder = new ConfigurationBuilder(); + if (configValues != null) + { + configurationBuilder.AddInMemoryCollection(configValues); + } + + var configuration = configurationBuilder.Build(); + + var services = new ServiceCollection(); + services.AddOptions().Bind(configuration.GetSection("ConnectionStrings")); + services.AddSingleton, ConfigureConnectionStrings>(); + services.AddSingleton(configuration); + + var container = services.BuildServiceProvider(); + return container.GetRequiredService>(); + } + + [Test] + public void Configure_WithConfigMissingProvider_SetsDefaultValue() + { + var result = GetOptions(); + Assert.Multiple(() => + { + Assert.That(result.Value.ProviderName, Is.Not.Null); + Assert.That(result.Value.ProviderName, Is.EqualTo(ConnectionStrings.DefaultProviderName)); + + Assert.That(result.Get(UmbracoDbDsn).ProviderName, Is.Not.Null); + Assert.That(result.Get(UmbracoDbDsn).ProviderName, Is.EqualTo(ConnectionStrings.DefaultProviderName)); + }); + } + + [Test] + [AutoMoqData] + public void Configure_WithConfiguredProvider_RespectsProviderValue( + string aConnectionString, + string aProviderName) + { + var config = new Dictionary + { + [$"ConnectionStrings:{UmbracoDbDsn}"] = aConnectionString, + [$"ConnectionStrings:{UmbracoDbDsn}_ProviderName"] = aProviderName, + }; + + var result = GetOptions(config); + + Assert.Multiple(() => + { + Assert.That(result.Value.ProviderName, Is.Not.Null); + Assert.That(result.Value.ProviderName, Is.EqualTo(aProviderName)); + + Assert.That(result.Get(UmbracoDbDsn).ProviderName, Is.Not.Null); + Assert.That(result.Get(UmbracoDbDsn).ProviderName, Is.EqualTo(aProviderName)); + }); + } + + [Test] + [AutoMoqData] + public void Configure_WithDataDirectoryPlaceholderInConnectionStringConfig_ReplacesDataDirectoryPlaceholder( + string aDataDirectory, + string aConnectionString, + string aProviderName) + { + AppDomain.CurrentDomain.SetData("DataDirectory", aDataDirectory); + var config = new Dictionary + { + [$"ConnectionStrings:{UmbracoDbDsn}"] = $"{ConnectionStrings.DataDirectoryPlaceholder}/{aConnectionString}", + [$"ConnectionStrings:{UmbracoDbDsn}_ProviderName"] = aProviderName, + }; + + var result = GetOptions(config); + + Assert.Multiple(() => + { + Assert.That(result.Value.ConnectionString, Is.Not.Null); + Assert.That(result.Value.ConnectionString, Contains.Substring($"{aDataDirectory}/{aConnectionString}")); + + Assert.That(result.Get(UmbracoDbDsn).ConnectionString, Is.Not.Null); + Assert.That(result.Get(UmbracoDbDsn).ConnectionString, Contains.Substring($"{aDataDirectory}/{aConnectionString}")); + }); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Configuration/Models/ConnectionStringsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Configuration/Models/ConnectionStringsTests.cs index c545c10c8f..5398dfc7fd 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Configuration/Models/ConnectionStringsTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Configuration/Models/ConnectionStringsTests.cs @@ -1,35 +1,36 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using System; using NUnit.Framework; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Tests.UnitTests.AutoFixture; using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Configuration.Models { + [TestFixture] public class ConnectionStringsTests { [Test] - [TestCase("", ExpectedResult = null)] - [TestCase(null, ExpectedResult = null)] - [TestCase(@"Data Source=|DataDirectory|\Umbraco.sdf;Flush Interval=1;", ExpectedResult = Constants.DbProviderNames.SqlCe)] - [TestCase(@"Server=(LocalDb)\Umbraco;Database=NetCore;Integrated Security=true", ExpectedResult = Constants.DbProviderNames.SqlServer)] - [TestCase(@"Data Source=(LocalDb)\Umbraco;Initial Catalog=NetCore;Integrated Security=true;", ExpectedResult = Constants.DbProviderNames.SqlServer)] - [TestCase(@"Data Source=.\SQLExpress;Integrated Security=true;AttachDbFilename=MyDataFile.mdf;", ExpectedResult = Constants.DbProviderNames.SqlServer)] - public string ParseProviderName(string connectionString) + public void ProviderName_WhenNotExplicitlySet_HasDefaultSet() { - var connectionStrings = new ConnectionStrings + var sut = new ConnectionStrings(); + Assert.That(sut.ProviderName, Is.EqualTo(ConnectionStrings.DefaultProviderName)); + } + + [Test] + [AutoMoqData] + public void ConnectionString_WhenSetterCalled_ReplacesDataDirectoryPlaceholder(string aDataDirectory) + { + AppDomain.CurrentDomain.SetData("DataDirectory", aDataDirectory); + + var sut = new ConnectionStrings { - UmbracoConnectionString = new ConfigConnectionString(Constants.System.UmbracoConnectionName, connectionString) + ConnectionString = $"{ConnectionStrings.DataDirectoryPlaceholder}/foo" }; - - var actual = connectionStrings.UmbracoConnectionString; - - Assert.AreEqual(connectionString, actual.ConnectionString); - Assert.AreEqual(Constants.System.UmbracoConnectionName, actual.Name); - - return connectionStrings.UmbracoConnectionString.ProviderName; + Assert.That(sut.ConnectionString, Contains.Substring($"{aDataDirectory}/foo")); } } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Components/ComponentTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Components/ComponentTests.cs index 152808d6f3..c952f40f82 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Components/ComponentTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Components/ComponentTests.cs @@ -18,6 +18,7 @@ using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.DistributedLocking; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; @@ -27,6 +28,7 @@ using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Mappers; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common; using Umbraco.Cms.Tests.UnitTests.TestHelpers; @@ -53,9 +55,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Components loggerFactory.CreateLogger(), loggerFactory, Options.Create(globalSettings), - Mock.Of>(x => x.CurrentValue == connectionStrings), + Mock.Of>(x => x.Get(It.IsAny()) == connectionStrings), new MapperCollection(() => Enumerable.Empty()), - TestHelper.DbProviderFactoryCreator, + Mock.Of(), new DatabaseSchemaCreatorFactory(loggerFactory.CreateLogger(), loggerFactory, new UmbracoVersion(), Mock.Of()), mapperCollection); @@ -69,7 +71,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Components Mock.Of(), Options.Create(new ContentSettings())); IEventAggregator eventAggregator = Mock.Of(); - var scopeProvider = new ScopeProvider(f, fs, new TestOptionsMonitor(coreDebug), loggerFactory, NoAppCache.Instance, eventAggregator); + var scopeProvider = new ScopeProvider(Mock.Of(),f , fs, new TestOptionsMonitor(coreDebug), loggerFactory, NoAppCache.Instance, eventAggregator); mock.Setup(x => x.GetService(typeof(ILogger))).Returns(logger); mock.Setup(x => x.GetService(typeof(ILogger))).Returns(loggerFactory.CreateLogger); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Configuration/Models/Validation/GlobalSettingsValidatorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Configuration/Models/Validation/GlobalSettingsValidatorTests.cs index a08554d669..1c63e16ad3 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Configuration/Models/Validation/GlobalSettingsValidatorTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Configuration/Models/Validation/GlobalSettingsValidatorTests.cs @@ -43,7 +43,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Configuration.Models.Validati var validator = new GlobalSettingsValidator(); var options = new GlobalSettings { - SqlWriteLockTimeOut = TimeSpan.Parse("00:00:00.099") + DistributedLockingWriteLockDefaultTimeout = TimeSpan.Parse("00:00:00.099") }; ValidateOptionsResult result = validator.Validate("settings", options); @@ -56,7 +56,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Configuration.Models.Validati var validator = new GlobalSettingsValidator(); var options = new GlobalSettings { - SqlWriteLockTimeOut = TimeSpan.Parse("00:00:21") + DistributedLockingWriteLockDefaultTimeout = TimeSpan.Parse("00:00:21") }; ValidateOptionsResult result = validator.Validate("settings", options); @@ -69,7 +69,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Configuration.Models.Validati var validator = new GlobalSettingsValidator(); var options = new GlobalSettings { - SqlWriteLockTimeOut = TimeSpan.Parse("00:00:20") + DistributedLockingWriteLockDefaultTimeout = TimeSpan.Parse("00:00:20") }; ValidateOptionsResult result = validator.Validate("settings", options); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/UmbracoBuilderExtensionsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/UmbracoBuilderExtensionsTests.cs index 4894a15ba0..aaaa16b68a 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/UmbracoBuilderExtensionsTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/UmbracoBuilderExtensionsTests.cs @@ -64,7 +64,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Extensions public IHostingEnvironment BuilderHostingEnvironment { get; } public IProfiler Profiler { get; } public AppCaches AppCaches { get; } - public TBuilder WithCollectionBuilder() where TBuilder : ICollectionBuilder, new() => default; + public TBuilder WithCollectionBuilder() where TBuilder : ICollectionBuilder => default; public UmbracoBuildStub() => Services = new ServiceCollection(); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Scoping/ScopedNotificationPublisherTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Scoping/ScopedNotificationPublisherTests.cs index 6d614b62e9..031a9767c1 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Scoping/ScopedNotificationPublisherTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Scoping/ScopedNotificationPublisherTests.cs @@ -7,6 +7,7 @@ using Moq; using NUnit.Framework; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DistributedLocking; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; @@ -16,6 +17,7 @@ using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Scoping @@ -91,6 +93,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Scoping eventAggregatorMock = new Mock(); return new ScopeProvider( + Mock.Of(), Mock.Of(), fileSystems, new TestOptionsMonitor(new CoreDebugSettings()), diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs index 2e1ac83cd8..b9f45afd67 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationPlanTests.cs @@ -20,6 +20,7 @@ using Umbraco.Cms.Infrastructure.Migrations.Upgrade; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Cms.Infrastructure.Scoping; +using Umbraco.Cms.Persistence.SqlServer.Services; using Umbraco.Cms.Tests.Common.TestHelpers; using Umbraco.Cms.Tests.UnitTests.TestHelpers; using Umbraco.Extensions; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/PostMigrationTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/PostMigrationTests.cs index 09d0cb99d1..37cb93116b 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/PostMigrationTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/PostMigrationTests.cs @@ -19,6 +19,7 @@ using Umbraco.Cms.Infrastructure.Migrations.Upgrade; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Cms.Infrastructure.Scoping; +using Umbraco.Cms.Persistence.SqlServer.Services; using Umbraco.Cms.Tests.Common.TestHelpers; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Migrations diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/BulkDataReaderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/BulkDataReaderTests.cs index 8ecf6870a4..c1b2bc052b 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/BulkDataReaderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/BulkDataReaderTests.cs @@ -9,6 +9,7 @@ using System.Data.Common; using Microsoft.Data.SqlClient; using NUnit.Framework; using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Persistence.SqlServer.Services; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Persistence { diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoSqlTemplateTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoSqlTemplateTests.cs index 3eddb0fe29..b32f3e9823 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoSqlTemplateTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoSqlTemplateTests.cs @@ -10,6 +10,7 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; +using Umbraco.Cms.Persistence.SqlServer.Services; using Umbraco.Cms.Tests.Common.TestHelpers; using Umbraco.Extensions; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/Querying/ExpressionTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/Querying/ExpressionTests.cs index 6b8287946c..b94d8bbaa7 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/Querying/ExpressionTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/Querying/ExpressionTests.cs @@ -20,6 +20,7 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Cms.Infrastructure.Serialization; +using Umbraco.Cms.Persistence.SqlServer.Services; using Umbraco.Cms.Tests.UnitTests.TestHelpers; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Persistence.Querying diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/DatabaseContextTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/SqlAzureDatabaseProviderMetadataTests.cs similarity index 72% rename from tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/DatabaseContextTests.cs rename to tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/SqlAzureDatabaseProviderMetadataTests.cs index 34e629d596..eed43b3aba 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/DatabaseContextTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/SqlAzureDatabaseProviderMetadataTests.cs @@ -1,12 +1,13 @@ using NUnit.Framework; -using Umbraco.Cms.Infrastructure.Migrations.Install; +using Umbraco.Cms.Core.Install.Models; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Persistence.SqlServer.Services; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Persistence { [TestFixture] - public class DatabaseContextTests + public class SqlAzureDatabaseProviderMetadataTests { - [TestCase("MyServer", "MyDatabase", "MyUser", "MyPassword")] [TestCase("MyServer", "MyDatabase", "MyUser@MyServer", "MyPassword")] [TestCase("tcp:MyServer", "MyDatabase", "MyUser", "MyPassword")] @@ -19,7 +20,13 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Persistence [TestCase("tcp:MyServer.database.windows.net,1433", "MyDatabase", "MyUser@MyServer", "MyPassword")] public void Build_Azure_Connection_String_Regular(string server, string databaseName, string userName, string password) { - var connectionString = DatabaseBuilder.GetAzureConnectionString(server, databaseName, userName, password); + var settings = new DatabaseModel + { + Server = server, DatabaseName = databaseName, Login = userName, Password = password + }; + + var sut = new SqlAzureDatabaseProviderMetadata(); + var connectionString = sut.GenerateConnectionString(settings); Assert.AreEqual(connectionString, "Server=tcp:MyServer.database.windows.net,1433;Database=MyDatabase;User ID=MyUser@MyServer;Password=MyPassword"); } @@ -29,7 +36,13 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Persistence [TestCase("tcp:kzeej5z8ty.ssmsawacluster4.windowsazure.mscds.com", "MyDatabase", "MyUser@kzeej5z8ty", "MyPassword")] public void Build_Azure_Connection_String_CustomServer(string server, string databaseName, string userName, string password) { - var connectionString = DatabaseBuilder.GetAzureConnectionString(server, databaseName, userName, password); + var settings = new DatabaseModel + { + Server = server, DatabaseName = databaseName, Login = userName, Password = password + }; + + var sut = new SqlAzureDatabaseProviderMetadata(); + var connectionString = sut.GenerateConnectionString(settings); Assert.AreEqual(connectionString, "Server=tcp:kzeej5z8ty.ssmsawacluster4.windowsazure.mscds.com,1433;Database=MyDatabase;User ID=MyUser@kzeej5z8ty;Password=MyPassword"); } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Scoping/ScopeUnitTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Scoping/ScopeUnitTests.cs index 6ef176ceef..ac2cd27f41 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Scoping/ScopeUnitTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Scoping/ScopeUnitTests.cs @@ -11,6 +11,7 @@ using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DistributedLocking; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; @@ -18,7 +19,9 @@ using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Tests.Common; +using Umbraco.Cms.Tests.UnitTests.AutoFixture; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Scoping { @@ -30,7 +33,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Scoping /// /// The mock of the ISqlSyntaxProvider2, used to count method calls. /// - private ScopeProvider GetScopeProvider(out Mock syntaxProviderMock) + private ScopeProvider GetScopeProvider(out Mock lockingMechanism) { var loggerFactory = NullLoggerFactory.Instance; var fileSystems = new FileSystems(loggerFactory, @@ -45,7 +48,16 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Scoping var databaseFactory = new Mock(); var database = new Mock(); var sqlContext = new Mock(); - syntaxProviderMock = new Mock(); + + lockingMechanism = new Mock(); + lockingMechanism.Setup(x => x.ReadLock(It.IsAny(), It.IsAny())) + .Returns(Mock.Of()); + lockingMechanism.Setup(x => x.WriteLock(It.IsAny(), It.IsAny())) + .Returns(Mock.Of()); + + var lockingMechanismFactory = new Mock(); + lockingMechanismFactory.Setup(x => x.DistributedLockingMechanism) + .Returns(lockingMechanism.Object); // Setup mock of database factory to return mock of database. databaseFactory.Setup(x => x.CreateDatabase()).Returns(database.Object); @@ -54,10 +66,12 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Scoping // Setup mock of database to return mock of sql SqlContext database.Setup(x => x.SqlContext).Returns(sqlContext.Object); + var syntaxProviderMock = new Mock(); // Setup mock of ISqlContext to return syntaxProviderMock sqlContext.Setup(x => x.SqlSyntax).Returns(syntaxProviderMock.Object); return new ScopeProvider( + lockingMechanismFactory.Object, databaseFactory.Object, fileSystems, new TestOptionsMonitor(new CoreDebugSettings()), @@ -125,8 +139,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Scoping outerScope.Complete(); } - syntaxProviderMock.Verify(x => x.WriteLock(It.IsAny(), Constants.Locks.Domains), Times.Once); - syntaxProviderMock.Verify(x => x.WriteLock(It.IsAny(), Constants.Locks.Languages), Times.Once); + syntaxProviderMock.Verify(x => x.WriteLock(Constants.Locks.Domains, It.IsAny()), Times.Once); + syntaxProviderMock.Verify(x => x.WriteLock(Constants.Locks.Languages, It.IsAny()), Times.Once); } [Test] @@ -149,8 +163,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Scoping outerScope.Complete(); } - syntaxProviderMock.Verify(x => x.WriteLock(It.IsAny(), Constants.Locks.Languages), Times.Once); - syntaxProviderMock.Verify(x => x.WriteLock(It.IsAny(), Constants.Locks.ContentTree), Times.Once); + syntaxProviderMock.Verify(x => x.WriteLock(Constants.Locks.Languages, It.IsAny()), Times.Once); + syntaxProviderMock.Verify(x => x.WriteLock(Constants.Locks.ContentTree, It.IsAny()), Times.Once); } [Test] @@ -181,8 +195,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Scoping outerScope.Complete(); } - syntaxProviderMock.Verify(x => x.WriteLock(It.IsAny(), timeout, Constants.Locks.Domains), Times.Once); - syntaxProviderMock.Verify(x => x.WriteLock(It.IsAny(), timeout, Constants.Locks.Languages), Times.Once); + syntaxProviderMock.Verify(x => x.WriteLock(Constants.Locks.Domains, It.IsAny()), Times.Once); + syntaxProviderMock.Verify(x => x.WriteLock(Constants.Locks.Languages, It.IsAny()), Times.Once); } [Test] @@ -214,8 +228,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Scoping outerScope.Complete(); } - syntaxProviderMock.Verify(x => x.ReadLock(It.IsAny(), Constants.Locks.Domains), Times.Once); - syntaxProviderMock.Verify(x => x.ReadLock(It.IsAny(), Constants.Locks.Languages), Times.Once); + syntaxProviderMock.Verify(x => x.ReadLock(Constants.Locks.Domains, It.IsAny()), Times.Once); + syntaxProviderMock.Verify(x => x.ReadLock(Constants.Locks.Languages, It.IsAny()), Times.Once); } [Test] @@ -248,8 +262,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Scoping outerScope.Complete(); } - syntaxProviderMock.Verify(x => x.ReadLock(It.IsAny(), timeOut, Constants.Locks.Domains), Times.Once); - syntaxProviderMock.Verify(x => x.ReadLock(It.IsAny(), timeOut, Constants.Locks.Languages), Times.Once); + syntaxProviderMock.Verify(x => x.ReadLock(Constants.Locks.Domains, It.IsAny()), Times.Once); + syntaxProviderMock.Verify(x => x.ReadLock(Constants.Locks.Languages, It.IsAny()), Times.Once); } [Test] @@ -272,8 +286,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Scoping outerScope.Complete(); } - syntaxProviderMock.Verify(x => x.ReadLock(It.IsAny(), Constants.Locks.Languages), Times.Once); - syntaxProviderMock.Verify(x => x.ReadLock(It.IsAny(), Constants.Locks.ContentTree), Times.Once); + syntaxProviderMock.Verify(x => x.ReadLock(Constants.Locks.Languages, It.IsAny()), Times.Once); + syntaxProviderMock.Verify(x => x.ReadLock(Constants.Locks.ContentTree, It.IsAny()), Times.Once); } [Test] @@ -467,7 +481,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Scoping public void WriteLock_Doesnt_Increment_On_Error() { var scopeProvider = GetScopeProvider(out var syntaxProviderMock); - syntaxProviderMock.Setup(x => x.WriteLock(It.IsAny(), It.IsAny())).Throws(new Exception("Boom")); + syntaxProviderMock.Setup(x => x.WriteLock(It.IsAny(), It.IsAny())).Throws(new Exception("Boom")); using (var scope = (Scope)scopeProvider.CreateScope()) { @@ -481,7 +495,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Scoping public void ReadLock_Doesnt_Increment_On_Error() { var scopeProvider = GetScopeProvider(out var syntaxProviderMock); - syntaxProviderMock.Setup(x => x.ReadLock(It.IsAny(), It.IsAny())).Throws(new Exception("Boom")); + syntaxProviderMock.Setup(x => x.ReadLock(It.IsAny(), It.IsAny())).Throws(new Exception("Boom")); using (var scope = (Scope)scopeProvider.CreateScope()) { diff --git a/umbraco-netcore-only.sln b/umbraco-netcore-only.sln deleted file mode 100644 index 80a688bde5..0000000000 --- a/umbraco-netcore-only.sln +++ /dev/null @@ -1,225 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29209.152 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Web.UI", "src\Umbraco.Web.UI\Umbraco.Web.UI.csproj", "{DCDFE97C-5630-4F6F-855D-8AEEB96556A5}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{2849E9D4-3B4E-40A3-A309-F3CB4F0E125F}" - ProjectSection(SolutionItems) = preProject - build\azure-pipelines.yml = build\azure-pipelines.yml - build\build-bootstrap.ps1 = build\build-bootstrap.ps1 - build\build.ps1 = build\build.ps1 - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{FD962632-184C-4005-A5F3-E705D92FC645}" - ProjectSection(SolutionItems) = preProject - .github\BUILD.md = .github\BUILD.md - .github\CLEAR.md = .github\CLEAR.md - .github\CODE_OF_CONDUCT.md = .github\CODE_OF_CONDUCT.md - .github\CONTRIBUTING.md = .github\CONTRIBUTING.md - .github\CONTRIBUTING_DETAILED.md = .github\CONTRIBUTING_DETAILED.md - .github\CONTRIBUTION_GUIDELINES.md = .github\CONTRIBUTION_GUIDELINES.md - .github\PULL_REQUEST_TEMPLATE.md = .github\PULL_REQUEST_TEMPLATE.md - .github\README.md = .github\README.md - .github\REVIEW_PROCESS.md = .github\REVIEW_PROCESS.md - .github\V8_GETTING_STARTED.md = .github\V8_GETTING_STARTED.md - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{B5BD12C1-A454-435E-8A46-FF4A364C0382}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NuSpecs", "NuSpecs", "{227C3B55-80E5-4E7E-A802-BE16C5128B9D}" - ProjectSection(SolutionItems) = preProject - build\NuSpecs\UmbracoCms.nuspec = build\NuSpecs\UmbracoCms.nuspec - build\NuSpecs\UmbracoCms.SqlCe.nuspec = build\NuSpecs\UmbracoCms.SqlCe.nuspec - build\NuSpecs\UmbracoCms.StaticAssets.nuspec = build\NuSpecs\UmbracoCms.StaticAssets.nuspec - EndProjectSection -EndProject -Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Web.UI.Client", "http://localhost:3961", "{3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}" - ProjectSection(WebsiteProperties) = preProject - UseIISExpress = "true" - TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.5" - Debug.AspNetCompiler.VirtualPath = "/localhost_3961" - Debug.AspNetCompiler.PhysicalPath = "src\Umbraco.Web.UI.Client\" - Debug.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_3961\" - Debug.AspNetCompiler.Updateable = "true" - Debug.AspNetCompiler.ForceOverwrite = "true" - Debug.AspNetCompiler.FixedNames = "false" - Debug.AspNetCompiler.Debug = "True" - Release.AspNetCompiler.VirtualPath = "/localhost_3961" - Release.AspNetCompiler.PhysicalPath = "src\Umbraco.Web.UI.Client\" - Release.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_3961\" - Release.AspNetCompiler.Updateable = "true" - Release.AspNetCompiler.ForceOverwrite = "true" - Release.AspNetCompiler.FixedNames = "false" - Release.AspNetCompiler.Debug = "False" - SlnRelativePath = "src\Umbraco.Web.UI.Client\" - DefaultWebSiteLanguage = "Visual C#" - StartServerOnDebug = "false" - EndProjectSection -EndProject -Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Tests.AcceptanceTest", "http://localhost:58896", "{9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}" - ProjectSection(WebsiteProperties) = preProject - UseIISExpress = "true" - TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.5" - Debug.AspNetCompiler.VirtualPath = "/localhost_62926" - Debug.AspNetCompiler.PhysicalPath = "tests\Umbraco.Tests.AcceptanceTest\" - Debug.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_62926\" - Debug.AspNetCompiler.Updateable = "true" - Debug.AspNetCompiler.ForceOverwrite = "true" - Debug.AspNetCompiler.FixedNames = "false" - Debug.AspNetCompiler.Debug = "True" - Release.AspNetCompiler.VirtualPath = "/localhost_62926" - Release.AspNetCompiler.PhysicalPath = "tests\Umbraco.Tests.AcceptanceTest\" - Release.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_62926\" - Release.AspNetCompiler.Updateable = "true" - Release.AspNetCompiler.ForceOverwrite = "true" - Release.AspNetCompiler.FixedNames = "false" - Release.AspNetCompiler.Debug = "False" - SlnRelativePath = "tests\Umbraco.Tests.AcceptanceTest\" - DefaultWebSiteLanguage = "Visual C#" - StartServerOnDebug = "false" - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DocTools", "DocTools", "{53594E5B-64A2-4545-8367-E3627D266AE8}" - ProjectSection(SolutionItems) = preProject - src\ApiDocs\docfx.filter.yml = src\ApiDocs\docfx.filter.yml - src\ApiDocs\docfx.json = src\ApiDocs\docfx.json - src\ApiDocs\index.md = src\ApiDocs\index.md - src\ApiDocs\toc.yml = src\ApiDocs\toc.yml - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "IssueTemplates", "IssueTemplates", "{C7311C00-2184-409B-B506-52A5FAEA8736}" - ProjectSection(SolutionItems) = preProject - .github\ISSUE_TEMPLATE\1_Bug.md = .github\ISSUE_TEMPLATE\1_Bug.md - .github\ISSUE_TEMPLATE\2_Feature_request.md = .github\ISSUE_TEMPLATE\2_Feature_request.md - .github\ISSUE_TEMPLATE\3_Support_question.md = .github\ISSUE_TEMPLATE\3_Support_question.md - .github\ISSUE_TEMPLATE\4_Documentation_issue.md = .github\ISSUE_TEMPLATE\4_Documentation_issue.md - .github\ISSUE_TEMPLATE\5_Security_issue.md = .github\ISSUE_TEMPLATE\5_Security_issue.md - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Core", "src\Umbraco.Core\Umbraco.Core.csproj", "{29AA69D9-B597-4395-8D42-43B1263C240A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Infrastructure", "src\Umbraco.Infrastructure\Umbraco.Infrastructure.csproj", "{3AE7BF57-966B-45A5-910A-954D7C554441}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.PublishedCache.NuCache", "src\Umbraco.PublishedCache.NuCache\Umbraco.PublishedCache.NuCache.csproj", "{F6DE8DA0-07CC-4EF2-8A59-2BC81DBB3830}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Web.BackOffice", "src\Umbraco.Web.BackOffice\Umbraco.Web.BackOffice.csproj", "{9B95EEF7-63FE-4432-8C63-166BE9C1A929}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Web.Website", "src\Umbraco.Web.Website\Umbraco.Web.Website.csproj", "{5A246D54-3109-4D2B-BE7D-FC0787D126AE}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Tests.Common", "tests\Umbraco.Tests.Common\Umbraco.Tests.Common.csproj", "{A499779C-1B3B-48A8-B551-458E582E6E96}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Tests.UnitTests", "tests\Umbraco.Tests.UnitTests\Umbraco.Tests.UnitTests.csproj", "{9102ABDF-E537-4E46-B525-C9ED4833EED0}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Web.Common", "src\Umbraco.Web.Common\Umbraco.Web.Common.csproj", "{79E4293D-C92C-4649-AEC8-F1EFD95BDEB1}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Tests.Integration", "tests\Umbraco.Tests.Integration\Umbraco.Tests.Integration.csproj", "{1B885D2F-1599-4557-A4EC-474CC74DEB10}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Tests.Integration.SqlCe", "tests\Umbraco.Tests.Integration.SqlCe\Umbraco.Tests.Integration.SqlCe.csproj", "{7A58F7CB-786F-43D6-A946-7BFA1B70D0AA}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Examine.Lucene", "src\Umbraco.Examine.Lucene\Umbraco.Examine.Lucene.csproj", "{96EF355C-A7C8-460E-96D7-ABCE0D489762}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.TestData", "tests\Umbraco.TestData\Umbraco.TestData.csproj", "{C44B4389-6E2A-441E-9A10-7CE818CA63A9}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Tests.Benchmarks", "tests\Umbraco.Tests.Benchmarks\Umbraco.Tests.Benchmarks.csproj", "{37B6264F-A279-42A6-AB83-CC3A3950C3E2}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "buildTransitive", "buildTransitive", "{81D03F06-94BE-4246-8F52-A68B58486F94}" - ProjectSection(SolutionItems) = preProject - build\NuSpecs\buildTransitive\Umbraco.Cms.StaticAssets.props = build\NuSpecs\buildTransitive\Umbraco.Cms.StaticAssets.props - build\NuSpecs\buildTransitive\Umbraco.Cms.StaticAssets.targets = build\NuSpecs\buildTransitive\Umbraco.Cms.StaticAssets.targets - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JsonSchema", "src\JsonSchema\JsonSchema.csproj", "{B172BC0E-B535-4CEF-9204-F3F397003829}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {DCDFE97C-5630-4F6F-855D-8AEEB96556A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DCDFE97C-5630-4F6F-855D-8AEEB96556A5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DCDFE97C-5630-4F6F-855D-8AEEB96556A5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DCDFE97C-5630-4F6F-855D-8AEEB96556A5}.Release|Any CPU.Build.0 = Release|Any CPU - {3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {29AA69D9-B597-4395-8D42-43B1263C240A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {29AA69D9-B597-4395-8D42-43B1263C240A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {29AA69D9-B597-4395-8D42-43B1263C240A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {29AA69D9-B597-4395-8D42-43B1263C240A}.Release|Any CPU.Build.0 = Release|Any CPU - {3AE7BF57-966B-45A5-910A-954D7C554441}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3AE7BF57-966B-45A5-910A-954D7C554441}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3AE7BF57-966B-45A5-910A-954D7C554441}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3AE7BF57-966B-45A5-910A-954D7C554441}.Release|Any CPU.Build.0 = Release|Any CPU - {F6DE8DA0-07CC-4EF2-8A59-2BC81DBB3830}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F6DE8DA0-07CC-4EF2-8A59-2BC81DBB3830}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F6DE8DA0-07CC-4EF2-8A59-2BC81DBB3830}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F6DE8DA0-07CC-4EF2-8A59-2BC81DBB3830}.Release|Any CPU.Build.0 = Release|Any CPU - {9B95EEF7-63FE-4432-8C63-166BE9C1A929}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9B95EEF7-63FE-4432-8C63-166BE9C1A929}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9B95EEF7-63FE-4432-8C63-166BE9C1A929}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9B95EEF7-63FE-4432-8C63-166BE9C1A929}.Release|Any CPU.Build.0 = Release|Any CPU - {5A246D54-3109-4D2B-BE7D-FC0787D126AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5A246D54-3109-4D2B-BE7D-FC0787D126AE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5A246D54-3109-4D2B-BE7D-FC0787D126AE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5A246D54-3109-4D2B-BE7D-FC0787D126AE}.Release|Any CPU.Build.0 = Release|Any CPU - {A499779C-1B3B-48A8-B551-458E582E6E96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A499779C-1B3B-48A8-B551-458E582E6E96}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A499779C-1B3B-48A8-B551-458E582E6E96}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A499779C-1B3B-48A8-B551-458E582E6E96}.Release|Any CPU.Build.0 = Release|Any CPU - {9102ABDF-E537-4E46-B525-C9ED4833EED0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9102ABDF-E537-4E46-B525-C9ED4833EED0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9102ABDF-E537-4E46-B525-C9ED4833EED0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9102ABDF-E537-4E46-B525-C9ED4833EED0}.Release|Any CPU.Build.0 = Release|Any CPU - {79E4293D-C92C-4649-AEC8-F1EFD95BDEB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {79E4293D-C92C-4649-AEC8-F1EFD95BDEB1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {79E4293D-C92C-4649-AEC8-F1EFD95BDEB1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {79E4293D-C92C-4649-AEC8-F1EFD95BDEB1}.Release|Any CPU.Build.0 = Release|Any CPU - {1B885D2F-1599-4557-A4EC-474CC74DEB10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1B885D2F-1599-4557-A4EC-474CC74DEB10}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1B885D2F-1599-4557-A4EC-474CC74DEB10}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1B885D2F-1599-4557-A4EC-474CC74DEB10}.Release|Any CPU.Build.0 = Release|Any CPU - {7A58F7CB-786F-43D6-A946-7BFA1B70D0AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7A58F7CB-786F-43D6-A946-7BFA1B70D0AA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7A58F7CB-786F-43D6-A946-7BFA1B70D0AA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7A58F7CB-786F-43D6-A946-7BFA1B70D0AA}.Release|Any CPU.Build.0 = Release|Any CPU - {96EF355C-A7C8-460E-96D7-ABCE0D489762}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {96EF355C-A7C8-460E-96D7-ABCE0D489762}.Debug|Any CPU.Build.0 = Debug|Any CPU - {96EF355C-A7C8-460E-96D7-ABCE0D489762}.Release|Any CPU.ActiveCfg = Release|Any CPU - {96EF355C-A7C8-460E-96D7-ABCE0D489762}.Release|Any CPU.Build.0 = Release|Any CPU - {C44B4389-6E2A-441E-9A10-7CE818CA63A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C44B4389-6E2A-441E-9A10-7CE818CA63A9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C44B4389-6E2A-441E-9A10-7CE818CA63A9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C44B4389-6E2A-441E-9A10-7CE818CA63A9}.Release|Any CPU.Build.0 = Release|Any CPU - {37B6264F-A279-42A6-AB83-CC3A3950C3E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {37B6264F-A279-42A6-AB83-CC3A3950C3E2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {37B6264F-A279-42A6-AB83-CC3A3950C3E2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {37B6264F-A279-42A6-AB83-CC3A3950C3E2}.Release|Any CPU.Build.0 = Release|Any CPU - {B172BC0E-B535-4CEF-9204-F3F397003829}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B172BC0E-B535-4CEF-9204-F3F397003829}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B172BC0E-B535-4CEF-9204-F3F397003829}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B172BC0E-B535-4CEF-9204-F3F397003829}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {227C3B55-80E5-4E7E-A802-BE16C5128B9D} = {2849E9D4-3B4E-40A3-A309-F3CB4F0E125F} - {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} - {53594E5B-64A2-4545-8367-E3627D266AE8} = {FD962632-184C-4005-A5F3-E705D92FC645} - {C7311C00-2184-409B-B506-52A5FAEA8736} = {FD962632-184C-4005-A5F3-E705D92FC645} - {A499779C-1B3B-48A8-B551-458E582E6E96} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} - {9102ABDF-E537-4E46-B525-C9ED4833EED0} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} - {1B885D2F-1599-4557-A4EC-474CC74DEB10} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} - {7A58F7CB-786F-43D6-A946-7BFA1B70D0AA} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} - {C44B4389-6E2A-441E-9A10-7CE818CA63A9} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} - {37B6264F-A279-42A6-AB83-CC3A3950C3E2} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} - {81D03F06-94BE-4246-8F52-A68B58486F94} = {227C3B55-80E5-4E7E-A802-BE16C5128B9D} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {7A0F2E34-D2AF-4DAB-86A0-7D7764B3D0EC} - EndGlobalSection -EndGlobal diff --git a/umbraco.sln b/umbraco.sln index 497258c699..b124ff573a 100644 --- a/umbraco.sln +++ b/umbraco.sln @@ -30,7 +30,6 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NuSpecs", "NuSpecs", "{227C3B55-80E5-4E7E-A802-BE16C5128B9D}" ProjectSection(SolutionItems) = preProject build\NuSpecs\UmbracoCms.nuspec = build\NuSpecs\UmbracoCms.nuspec - build\NuSpecs\UmbracoCms.SqlCe.nuspec = build\NuSpecs\UmbracoCms.SqlCe.nuspec build\NuSpecs\UmbracoCms.StaticAssets.nuspec = build\NuSpecs\UmbracoCms.StaticAssets.nuspec EndProjectSection EndProject @@ -109,8 +108,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Core", "src\Umbraco EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Infrastructure", "src\Umbraco.Infrastructure\Umbraco.Infrastructure.csproj", "{3AE7BF57-966B-45A5-910A-954D7C554441}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Persistence.SqlCe", "src\Umbraco.Persistence.SqlCe\Umbraco.Persistence.SqlCe.csproj", "{33085570-9BF2-4065-A9B0-A29D920D13BA}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.TestData", "tests\Umbraco.TestData\Umbraco.TestData.csproj", "{FB5676ED-7A69-492C-B802-E7B24144C0FC}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.PublishedCache.NuCache", "src\Umbraco.PublishedCache.NuCache\Umbraco.PublishedCache.NuCache.csproj", "{F6DE8DA0-07CC-4EF2-8A59-2BC81DBB3830}" @@ -131,82 +128,114 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Web.Common", "src\U EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JsonSchema", "src\JsonSchema\JsonSchema.csproj", "{2A5027D9-F71D-4957-929E-F7A56AA1B95A}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Tests.Integration.SqlCe", "tests\Umbraco.Tests.Integration.SqlCe\Umbraco.Tests.Integration.SqlCe.csproj", "{1B28FC3E-3D5B-4A46-8961-5483835548D7}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Cms.Persistence.Sqlite", "src\Umbraco.Cms.Persistence.Sqlite\Umbraco.Cms.Persistence.Sqlite.csproj", "{32F6A309-EC1E-4CDB-BA80-C804CF680BEE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Cms.Persistence.SqlServer", "src\Umbraco.Cms.Persistence.SqlServer\Umbraco.Cms.Persistence.SqlServer.csproj", "{93C5910D-2E36-475D-88EB-A11BA5B50F65}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU + SkipTests|Any CPU = SkipTests|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {DCDFE97C-5630-4F6F-855D-8AEEB96556A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DCDFE97C-5630-4F6F-855D-8AEEB96556A5}.Debug|Any CPU.Build.0 = Debug|Any CPU {DCDFE97C-5630-4F6F-855D-8AEEB96556A5}.Release|Any CPU.ActiveCfg = Release|Any CPU {DCDFE97C-5630-4F6F-855D-8AEEB96556A5}.Release|Any CPU.Build.0 = Release|Any CPU + {DCDFE97C-5630-4F6F-855D-8AEEB96556A5}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU + {DCDFE97C-5630-4F6F-855D-8AEEB96556A5}.SkipTests|Any CPU.Build.0 = Debug|Any CPU {3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.Debug|Any CPU.Build.0 = Debug|Any CPU {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.Release|Any CPU.ActiveCfg = Release|Any CPU {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.Release|Any CPU.Build.0 = Release|Any CPU + {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU {29AA69D9-B597-4395-8D42-43B1263C240A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {29AA69D9-B597-4395-8D42-43B1263C240A}.Debug|Any CPU.Build.0 = Debug|Any CPU {29AA69D9-B597-4395-8D42-43B1263C240A}.Release|Any CPU.ActiveCfg = Release|Any CPU {29AA69D9-B597-4395-8D42-43B1263C240A}.Release|Any CPU.Build.0 = Release|Any CPU + {29AA69D9-B597-4395-8D42-43B1263C240A}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU + {29AA69D9-B597-4395-8D42-43B1263C240A}.SkipTests|Any CPU.Build.0 = Debug|Any CPU {3AE7BF57-966B-45A5-910A-954D7C554441}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3AE7BF57-966B-45A5-910A-954D7C554441}.Debug|Any CPU.Build.0 = Debug|Any CPU {3AE7BF57-966B-45A5-910A-954D7C554441}.Release|Any CPU.ActiveCfg = Release|Any CPU {3AE7BF57-966B-45A5-910A-954D7C554441}.Release|Any CPU.Build.0 = Release|Any CPU - {33085570-9BF2-4065-A9B0-A29D920D13BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {33085570-9BF2-4065-A9B0-A29D920D13BA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {33085570-9BF2-4065-A9B0-A29D920D13BA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {33085570-9BF2-4065-A9B0-A29D920D13BA}.Release|Any CPU.Build.0 = Release|Any CPU + {3AE7BF57-966B-45A5-910A-954D7C554441}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU + {3AE7BF57-966B-45A5-910A-954D7C554441}.SkipTests|Any CPU.Build.0 = Debug|Any CPU {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Debug|Any CPU.Build.0 = Debug|Any CPU {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Release|Any CPU.ActiveCfg = Release|Any CPU {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Release|Any CPU.Build.0 = Release|Any CPU + {FB5676ED-7A69-492C-B802-E7B24144C0FC}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU {F6DE8DA0-07CC-4EF2-8A59-2BC81DBB3830}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F6DE8DA0-07CC-4EF2-8A59-2BC81DBB3830}.Debug|Any CPU.Build.0 = Debug|Any CPU {F6DE8DA0-07CC-4EF2-8A59-2BC81DBB3830}.Release|Any CPU.ActiveCfg = Release|Any CPU {F6DE8DA0-07CC-4EF2-8A59-2BC81DBB3830}.Release|Any CPU.Build.0 = Release|Any CPU + {F6DE8DA0-07CC-4EF2-8A59-2BC81DBB3830}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU + {F6DE8DA0-07CC-4EF2-8A59-2BC81DBB3830}.SkipTests|Any CPU.Build.0 = Debug|Any CPU {0FAD7D2A-D7DD-45B1-91FD-488BB6CDACEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0FAD7D2A-D7DD-45B1-91FD-488BB6CDACEA}.Debug|Any CPU.Build.0 = Debug|Any CPU {0FAD7D2A-D7DD-45B1-91FD-488BB6CDACEA}.Release|Any CPU.ActiveCfg = Release|Any CPU {0FAD7D2A-D7DD-45B1-91FD-488BB6CDACEA}.Release|Any CPU.Build.0 = Release|Any CPU + {0FAD7D2A-D7DD-45B1-91FD-488BB6CDACEA}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU + {0FAD7D2A-D7DD-45B1-91FD-488BB6CDACEA}.SkipTests|Any CPU.Build.0 = Debug|Any CPU {9B95EEF7-63FE-4432-8C63-166BE9C1A929}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9B95EEF7-63FE-4432-8C63-166BE9C1A929}.Debug|Any CPU.Build.0 = Debug|Any CPU {9B95EEF7-63FE-4432-8C63-166BE9C1A929}.Release|Any CPU.ActiveCfg = Release|Any CPU {9B95EEF7-63FE-4432-8C63-166BE9C1A929}.Release|Any CPU.Build.0 = Release|Any CPU + {9B95EEF7-63FE-4432-8C63-166BE9C1A929}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU + {9B95EEF7-63FE-4432-8C63-166BE9C1A929}.SkipTests|Any CPU.Build.0 = Debug|Any CPU {5A246D54-3109-4D2B-BE7D-FC0787D126AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5A246D54-3109-4D2B-BE7D-FC0787D126AE}.Debug|Any CPU.Build.0 = Debug|Any CPU {5A246D54-3109-4D2B-BE7D-FC0787D126AE}.Release|Any CPU.ActiveCfg = Release|Any CPU {5A246D54-3109-4D2B-BE7D-FC0787D126AE}.Release|Any CPU.Build.0 = Release|Any CPU + {5A246D54-3109-4D2B-BE7D-FC0787D126AE}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU + {5A246D54-3109-4D2B-BE7D-FC0787D126AE}.SkipTests|Any CPU.Build.0 = Debug|Any CPU {D6319409-777A-4BD0-93ED-B2DFD805B32C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D6319409-777A-4BD0-93ED-B2DFD805B32C}.Debug|Any CPU.Build.0 = Debug|Any CPU {D6319409-777A-4BD0-93ED-B2DFD805B32C}.Release|Any CPU.ActiveCfg = Release|Any CPU {D6319409-777A-4BD0-93ED-B2DFD805B32C}.Release|Any CPU.Build.0 = Release|Any CPU + {D6319409-777A-4BD0-93ED-B2DFD805B32C}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU {A499779C-1B3B-48A8-B551-458E582E6E96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A499779C-1B3B-48A8-B551-458E582E6E96}.Debug|Any CPU.Build.0 = Debug|Any CPU {A499779C-1B3B-48A8-B551-458E582E6E96}.Release|Any CPU.ActiveCfg = Release|Any CPU {A499779C-1B3B-48A8-B551-458E582E6E96}.Release|Any CPU.Build.0 = Release|Any CPU + {A499779C-1B3B-48A8-B551-458E582E6E96}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU {9102ABDF-E537-4E46-B525-C9ED4833EED0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9102ABDF-E537-4E46-B525-C9ED4833EED0}.Debug|Any CPU.Build.0 = Debug|Any CPU {9102ABDF-E537-4E46-B525-C9ED4833EED0}.Release|Any CPU.ActiveCfg = Release|Any CPU {9102ABDF-E537-4E46-B525-C9ED4833EED0}.Release|Any CPU.Build.0 = Release|Any CPU + {9102ABDF-E537-4E46-B525-C9ED4833EED0}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU {79E4293D-C92C-4649-AEC8-F1EFD95BDEB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {79E4293D-C92C-4649-AEC8-F1EFD95BDEB1}.Debug|Any CPU.Build.0 = Debug|Any CPU {79E4293D-C92C-4649-AEC8-F1EFD95BDEB1}.Release|Any CPU.ActiveCfg = Release|Any CPU {79E4293D-C92C-4649-AEC8-F1EFD95BDEB1}.Release|Any CPU.Build.0 = Release|Any CPU + {79E4293D-C92C-4649-AEC8-F1EFD95BDEB1}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU + {79E4293D-C92C-4649-AEC8-F1EFD95BDEB1}.SkipTests|Any CPU.Build.0 = Debug|Any CPU {2A5027D9-F71D-4957-929E-F7A56AA1B95A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2A5027D9-F71D-4957-929E-F7A56AA1B95A}.Debug|Any CPU.Build.0 = Debug|Any CPU {2A5027D9-F71D-4957-929E-F7A56AA1B95A}.Release|Any CPU.ActiveCfg = Release|Any CPU {2A5027D9-F71D-4957-929E-F7A56AA1B95A}.Release|Any CPU.Build.0 = Release|Any CPU - {1B28FC3E-3D5B-4A46-8961-5483835548D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1B28FC3E-3D5B-4A46-8961-5483835548D7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1B28FC3E-3D5B-4A46-8961-5483835548D7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1B28FC3E-3D5B-4A46-8961-5483835548D7}.Release|Any CPU.Build.0 = Release|Any CPU + {2A5027D9-F71D-4957-929E-F7A56AA1B95A}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU + {2A5027D9-F71D-4957-929E-F7A56AA1B95A}.SkipTests|Any CPU.Build.0 = Debug|Any CPU + {32F6A309-EC1E-4CDB-BA80-C804CF680BEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {32F6A309-EC1E-4CDB-BA80-C804CF680BEE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {32F6A309-EC1E-4CDB-BA80-C804CF680BEE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {32F6A309-EC1E-4CDB-BA80-C804CF680BEE}.Release|Any CPU.Build.0 = Release|Any CPU + {32F6A309-EC1E-4CDB-BA80-C804CF680BEE}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU + {32F6A309-EC1E-4CDB-BA80-C804CF680BEE}.SkipTests|Any CPU.Build.0 = Debug|Any CPU + {93C5910D-2E36-475D-88EB-A11BA5B50F65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {93C5910D-2E36-475D-88EB-A11BA5B50F65}.Debug|Any CPU.Build.0 = Debug|Any CPU + {93C5910D-2E36-475D-88EB-A11BA5B50F65}.Release|Any CPU.ActiveCfg = Release|Any CPU + {93C5910D-2E36-475D-88EB-A11BA5B50F65}.Release|Any CPU.Build.0 = Release|Any CPU + {93C5910D-2E36-475D-88EB-A11BA5B50F65}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU + {93C5910D-2E36-475D-88EB-A11BA5B50F65}.SkipTests|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -222,7 +251,6 @@ Global {D6319409-777A-4BD0-93ED-B2DFD805B32C} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} {A499779C-1B3B-48A8-B551-458E582E6E96} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} {9102ABDF-E537-4E46-B525-C9ED4833EED0} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} - {1B28FC3E-3D5B-4A46-8961-5483835548D7} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7A0F2E34-D2AF-4DAB-86A0-7D7764B3D0EC}