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