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/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md
index e97b03e7e3..1acc6b602b 100644
--- a/.github/CODE_OF_CONDUCT.md
+++ b/.github/CODE_OF_CONDUCT.md
@@ -57,6 +57,7 @@ Or alternatively, you can reach out directly to any of the team members behind t
* Sebastiaan Janssen (He, Him - Languages spoken: English, Dutch, Danish(Read)) [sebastiaan@umbraco.com](mailto:sebastiaan@umbraco.com)
* Ilham Boulghallat (She, Her - Languages spoken: English, French, Arabic) [ilham@umbraco.com](mailto:ilham@umbraco.com)
* Arnold Visser (He, Him - Languages spoken: English, Dutch) [arnold@umbraco.com](mailto:arnold@umbraco.com)
+* Emma Burstow (She, Her - Languages spoken: English) [ema@umbraco.com](mailto:ema@umbraco.com)
The review process is done with full respect for the privacy and security of the reporter of any incident.
@@ -89,4 +90,4 @@ Consequence: A permanent ban from any sort of public interaction within the comm
## Attribution
This Code of Conduct is adapted from the Contributor Covenant, version 2.0, available at [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html).
-This Code of Conduct will be maintained and reviewed by the team listed above.
\ No newline at end of file
+This Code of Conduct will be maintained and reviewed by the team listed above.
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/.globalconfig b/.globalconfig
index 8342ab4580..8c0929382d 100644
--- a/.globalconfig
+++ b/.globalconfig
@@ -49,6 +49,7 @@ dotnet_analyzer_diagnostic.category-StyleCop.CSharp.MaintainabilityRules.severit
dotnet_analyzer_diagnostic.category-StyleCop.CSharp.LayoutRules.severity = suggestion
dotnet_diagnostic.SA1636.severity = none # SA1636: File header copyright text should match
+dotnet_diagnostic.SA1101.severity = none # PrefixLocalCallsWithThis - stylecop appears to be ignoring dotnet_style_qualification_for_*
dotnet_diagnostic.SA1503.severity = warning # BracesMustNotBeOmitted
dotnet_diagnostic.SA1117.severity = warning # ParametersMustBeOnSameLineOrSeparateLines
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/ApiDocs/umbracotemplate/partials/class.tmpl.partial b/src/ApiDocs/umbracotemplate/partials/class.tmpl.partial
index 9153a863a4..aa50d597ba 100644
--- a/src/ApiDocs/umbracotemplate/partials/class.tmpl.partial
+++ b/src/ApiDocs/umbracotemplate/partials/class.tmpl.partial
@@ -15,8 +15,8 @@
{{item.name.0.value}}
{{/inheritance.0}}
-{{__global.namespace}} :{{namespace}}
-{{__global.assembly}} :{{assemblies.0}}.dll
+{{__global.namespace}} : {{{namespace.specName.0.value}}}
+{{__global.assembly}} : {{assemblies.0}}.dll
{{__global.syntax}}
{{syntax.content.0.value}}
diff --git a/src/JsonSchema/AppSettings.cs b/src/JsonSchema/AppSettings.cs
index 73c5ea18f5..f9aa6b500c 100644
--- a/src/JsonSchema/AppSettings.cs
+++ b/src/JsonSchema/AppSettings.cs
@@ -3,6 +3,7 @@
using Umbraco.Cms.Core.Configuration;
using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Deploy.Core.Configuration.DebugConfiguration;
using Umbraco.Deploy.Core.Configuration.DeployConfiguration;
using Umbraco.Deploy.Core.Configuration.DeployProjectConfiguration;
using Umbraco.Forms.Core.Configuration;
@@ -127,6 +128,8 @@ namespace JsonSchema
public DeploySettings Settings { get; set; }
public DeployProjectConfig Project { get; set; }
+
+ public DebugSettings Debug { get; set; }
}
}
}
diff --git a/src/JsonSchema/JsonSchema.csproj b/src/JsonSchema/JsonSchema.csproj
index f0652e7e98..e46fc3ee4b 100644
--- a/src/JsonSchema/JsonSchema.csproj
+++ b/src/JsonSchema/JsonSchema.csproj
@@ -13,6 +13,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 961a54bf68..65bd0b5d65 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 86%
rename from src/Umbraco.Infrastructure/Persistence/Dtos/ConstraintPerColumnDto.cs
rename to src/Umbraco.Cms.Persistence.SqlServer/Dtos/ConstraintPerColumnDto.cs
index e9aff45305..351979570c 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 82%
rename from src/Umbraco.Infrastructure/Persistence/Dtos/ConstraintPerTableDto.cs
rename to src/Umbraco.Cms.Persistence.SqlServer/Dtos/ConstraintPerTableDto.cs
index c8f9a1abbf..3a633d4e0e 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 88%
rename from src/Umbraco.Infrastructure/Persistence/Dtos/DefaultConstraintPerColumnDto.cs
rename to src/Umbraco.Cms.Persistence.SqlServer/Dtos/DefaultConstraintPerColumnDto.cs
index 1441133daa..e0e1dfbe2f 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 88%
rename from src/Umbraco.Infrastructure/Persistence/Dtos/DefinedIndexDto.cs
rename to src/Umbraco.Cms.Persistence.SqlServer/Dtos/DefinedIndexDto.cs
index 287757dc9f..e78f354e46 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 f03b60d778..e9784ce270 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 c9b05dd9a0..4856e1e117 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 17dc7937a6..8a05e78258 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 bffda81bd0..7b4feb0a98 100644
--- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs
+++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerSyntaxProvider.cs
@@ -1,19 +1,17 @@
-using System;
-using System.Collections.Generic;
using System.Data;
-using System.Diagnostics.CodeAnalysis;
-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.
@@ -21,13 +19,20 @@ 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 override string ProviderName => Constants.ProviderName;
public ServerVersionInfo? ServerVersion { get; private set; }
@@ -57,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()
@@ -88,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 "??":
@@ -267,108 +288,7 @@ 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)
+ public override string FormatColumnRename(string tableName, string oldName, string newName)
{
return string.Format(RenameColumn, tableName, oldName, newName);
}
@@ -434,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
index 85c9332b30..e69de29bb2 100644
--- a/src/Umbraco.Core/Configuration/ConfigConnectionString.cs
+++ b/src/Umbraco.Core/Configuration/ConfigConnectionString.cs
@@ -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 44568c33c0..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/ContentSettings.cs b/src/Umbraco.Core/Configuration/Models/ContentSettings.cs
index 1caa81d80a..e6e5c7006f 100644
--- a/src/Umbraco.Core/Configuration/Models/ContentSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/ContentSettings.cs
@@ -156,6 +156,9 @@ namespace Umbraco.Cms.Core.Configuration.Models
internal const bool StaticShowDeprecatedPropertyEditors = false;
internal const string StaticLoginBackgroundImage = "assets/img/login.jpg";
internal const string StaticLoginLogoImage = "assets/img/application/umbraco_logo_white.svg";
+ internal const bool StaticHideBackOfficeLogo = false;
+ internal const bool StaticDisableDeleteWhenReferenced = false;
+ internal const bool StaticDisableUnpublishWhenReferenced = false;
///
/// Gets or sets a value for the content notification settings.
@@ -219,6 +222,24 @@ namespace Umbraco.Cms.Core.Configuration.Models
[DefaultValue(StaticLoginLogoImage)]
public string LoginLogoImage { get; set; } = StaticLoginLogoImage;
+ ///
+ /// Gets or sets a value indicating whether to hide the backoffice umbraco logo or not.
+ ///
+ [DefaultValue(StaticHideBackOfficeLogo)]
+ public bool HideBackOfficeLogo { get; set; } = StaticHideBackOfficeLogo;
+
+ ///
+ /// Gets or sets a value indicating whether to disable the deletion of items referenced by other items.
+ ///
+ [DefaultValue(StaticDisableDeleteWhenReferenced)]
+ public bool DisableDeleteWhenReferenced { get; set; } = StaticDisableDeleteWhenReferenced;
+
+ ///
+ /// Gets or sets a value indicating whether to disable the unpublishing of items referenced by other items.
+ ///
+ [DefaultValue(StaticDisableUnpublishWhenReferenced)]
+ public bool DisableUnpublishWhenReferenced { get; set; } = StaticDisableUnpublishWhenReferenced;
+
///
/// Get or sets the model representing the global content version cleanup policy
///
diff --git a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs
index 0cd5ca5670..b355d81444 100644
--- a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs
@@ -27,8 +27,10 @@ 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;
///
/// Gets or sets a value for the reserved URLs (must end with a comma).
@@ -137,6 +139,26 @@ namespace Umbraco.Cms.Core.Configuration.Models
///
public string MainDomLock { get; set; } = string.Empty;
+ ///
+ /// Gets or sets a value to discriminate MainDom boundaries.
+ ///
+ /// Generally the default should suffice but useful for advanced scenarios e.g. azure deployment slot based zero downtime deployments.
+ ///
+ ///
+ public string MainDomKeyDiscriminator { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the duration (in milliseconds) for which the MainDomLock release signal polling task should sleep.
+ ///
+ ///
+ /// Doesn't apply to MainDomSemaphoreLock.
+ ///
+ /// The default value is 2000ms.
+ ///
+ ///
+ [DefaultValue(StaticMainDomReleaseSignalPollingInterval)]
+ public int MainDomReleaseSignalPollingInterval { get; set; } = StaticMainDomReleaseSignalPollingInterval;
+
///
/// Gets or sets the telemetry ID.
///
@@ -174,18 +196,32 @@ namespace Umbraco.Cms.Core.Configuration.Models
public bool IsPickupDirectoryLocationConfigured => !string.IsNullOrWhiteSpace(Smtp?.PickupDirectoryLocation);
///
- /// Gets a value indicating whether TinyMCE scripting sanitization should be applied.
+ /// Gets or sets a value indicating whether TinyMCE scripting sanitization should be applied.
///
[DefaultValue(StaticSanitizeTinyMce)]
- public bool SanitizeTinyMce => StaticSanitizeTinyMce;
+ public bool SanitizeTinyMce { get; set; } = StaticSanitizeTinyMce;
///
- /// Gets a 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; } = 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/ModelsBuilderSettings.cs b/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs
index 2cf1f770b7..73d046de32 100644
--- a/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs
@@ -49,6 +49,7 @@ namespace Umbraco.Cms.Core.Configuration.Models
if (!ModelsMode.IsAuto())
{
_flagOutOfDateModels = false;
+ return;
}
_flagOutOfDateModels = value;
diff --git a/src/Umbraco.Core/Configuration/Models/RichTextEditorSettings.cs b/src/Umbraco.Core/Configuration/Models/RichTextEditorSettings.cs
index 4bcbd19a1b..cd82376c57 100644
--- a/src/Umbraco.Core/Configuration/Models/RichTextEditorSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/RichTextEditorSettings.cs
@@ -8,7 +8,7 @@ namespace Umbraco.Cms.Core.Configuration.Models
[UmbracoOptions(Constants.Configuration.ConfigRichTextEditor)]
public class RichTextEditorSettings
{
- internal const string StaticValidElements = "+a[id|style|rel|data-id|data-udi|rev|charset|hreflang|dir|lang|tabindex|accesskey|type|name|href|target|title|class|onfocus|onblur|onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup],-strong/-b[class|style],-em/-i[class|style],-strike[class|style],-u[class|style],#p[id|style|dir|class|align],-ol[class|reversed|start|style|type],-ul[class|style],-li[class|style],br[class],img[id|dir|lang|longdesc|usemap|style|class|src|onmouseover|onmouseout|border|alt=|title|hspace|vspace|width|height|align|umbracoorgwidth|umbracoorgheight|onresize|onresizestart|onresizeend|rel|data-id],-sub[style|class],-sup[style|class],-blockquote[dir|style|class],-table[border=0|cellspacing|cellpadding|width|height|class|align|summary|style|dir|id|lang|bgcolor|background|bordercolor],-tr[id|lang|dir|class|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor],tbody[id|class],thead[id|class],tfoot[id|class],#td[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor|scope],-th[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|scope],caption[id|lang|dir|class|style],-div[id|dir|class|align|style],-span[class|align|style],-pre[class|align|style],address[class|align|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align|style],hr[class|style],small[class|style],dd[id|class|title|style|dir|lang],dl[id|class|title|style|dir|lang],dt[id|class|title|style|dir|lang],object[class|id|width|height|codebase|*],param[name|value|_value|class],embed[type|width|height|src|class|*],map[name|class],area[shape|coords|href|alt|target|class],bdo[class],button[class],iframe[*]";
+ internal const string StaticValidElements = "+a[id|style|rel|data-id|data-udi|rev|charset|hreflang|dir|lang|tabindex|accesskey|type|name|href|target|title|class|onfocus|onblur|onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup],-strong/-b[class|style],-em/-i[class|style],-strike[class|style],-u[class|style],#p[id|style|dir|class|align],-ol[class|reversed|start|style|type],-ul[class|style],-li[class|style],br[class],img[id|dir|lang|longdesc|usemap|style|class|src|onmouseover|onmouseout|border|alt=|title|hspace|vspace|width|height|align|umbracoorgwidth|umbracoorgheight|onresize|onresizestart|onresizeend|rel|data-id],-sub[style|class],-sup[style|class],-blockquote[dir|style|class],-table[border=0|cellspacing|cellpadding|width|height|class|align|summary|style|dir|id|lang|bgcolor|background|bordercolor],-tr[id|lang|dir|class|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor],tbody[id|class],thead[id|class],tfoot[id|class],#td[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor|scope],-th[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|scope],caption[id|lang|dir|class|style],-div[id|dir|class|align|style],-span[class|align|style],-pre[class|align|style],address[class|align|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align|style],hr[class|style],small[class|style],dd[id|class|title|style|dir|lang],dl[id|class|title|style|dir|lang],dt[id|class|title|style|dir|lang],object[class|id|width|height|codebase|*],param[name|value|_value|class],embed[type|width|height|src|class|*],map[name|class],area[shape|coords|href|alt|target|class],bdo[class],button[class],iframe[*],figure,figcaption";
internal const string StaticInvalidElements = "font";
private static readonly string[] s_default_plugins = new[]
diff --git a/src/Umbraco.Core/Configuration/Models/Validation/GlobalSettingsValidator.cs b/src/Umbraco.Core/Configuration/Models/Validation/GlobalSettingsValidator.cs
index 40b82ee09d..31d0779626 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-HttpClients.cs b/src/Umbraco.Core/Constants-HttpClients.cs
new file mode 100644
index 0000000000..474ec49a50
--- /dev/null
+++ b/src/Umbraco.Core/Constants-HttpClients.cs
@@ -0,0 +1,19 @@
+namespace Umbraco.Cms.Core
+{
+ ///
+ /// Defines constants.
+ ///
+ public static partial class Constants
+ {
+ ///
+ /// Defines constants for named http clients.
+ ///
+ public static class HttpClients
+ {
+ ///
+ /// Name for http client which ignores certificate errors.
+ ///
+ public const string IgnoreCertificateErrors = "Umbraco:HttpClients:IgnoreCertificateErrors";
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Constants-Icons.cs b/src/Umbraco.Core/Constants-Icons.cs
index 62e19008dd..39980f116a 100644
--- a/src/Umbraco.Core/Constants-Icons.cs
+++ b/src/Umbraco.Core/Constants-Icons.cs
@@ -1,4 +1,4 @@
-namespace Umbraco.Cms.Core
+namespace Umbraco.Cms.Core
{
public static partial class Constants
{
@@ -9,6 +9,11 @@
///
public const string DefaultIcon = Content;
+ ///
+ /// System blueprint icon
+ ///
+ public const string Blueprint = "icon-blueprint";
+
///
/// System content icon
///
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/Constants-SystemDirectories.cs b/src/Umbraco.Core/Constants-SystemDirectories.cs
index bf34aab989..f70dd199fc 100644
--- a/src/Umbraco.Core/Constants-SystemDirectories.cs
+++ b/src/Umbraco.Core/Constants-SystemDirectories.cs
@@ -1,3 +1,5 @@
+using System;
+
namespace Umbraco.Cms.Core
{
public static partial class Constants
@@ -42,9 +44,11 @@ namespace Umbraco.Cms.Core
public const string Install = "~/install";
public const string AppPlugins = "/App_Plugins";
- public static string AppPluginIcons => "/Backoffice/Icons";
- public const string CreatedPackages = "/created-packages";
+ [Obsolete("Use PluginIcons instead")]
+ public static string AppPluginIcons => "/Backoffice/Icons";
+
+ public const string PluginIcons = "/backoffice/icons";
public const string MvcViews = "~/Views";
@@ -54,6 +58,8 @@ namespace Umbraco.Cms.Core
public const string Packages = Data + "/packages";
+ public const string CreatedPackages = Data + "/CreatedPackages";
+
public const string Preview = Data + "/preview";
///
diff --git a/src/Umbraco.Core/ContentApps/DictionaryContentAppFactory.cs b/src/Umbraco.Core/ContentApps/DictionaryContentAppFactory.cs
new file mode 100644
index 0000000000..b1fb31d2aa
--- /dev/null
+++ b/src/Umbraco.Core/ContentApps/DictionaryContentAppFactory.cs
@@ -0,0 +1,32 @@
+using System.Collections.Generic;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models.ContentEditing;
+using Umbraco.Cms.Core.Models.Membership;
+
+namespace Umbraco.Cms.Core.ContentApps
+{
+ internal class DictionaryContentAppFactory : IContentAppFactory
+ {
+ private const int Weight = -100;
+
+ private ContentApp _dictionaryApp;
+
+ public ContentApp GetContentAppFor(object source, IEnumerable userGroups)
+ {
+ switch (source)
+ {
+ case IDictionaryItem _:
+ return _dictionaryApp ??= new ContentApp
+ {
+ Alias = "dictionaryContent",
+ Name = "Content",
+ Icon = "icon-document",
+ View = "views/dictionary/views/content/content.html",
+ Weight = Weight
+ };
+ default:
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/Umbraco.Core/DependencyInjection/IUmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/IUmbracoBuilder.cs
index ec6669b177..9351bb52cf 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.Collections.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs
index 811ee35c14..a0ff6104a7 100644
--- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs
+++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs
@@ -46,7 +46,8 @@ namespace Umbraco.Cms.Core.DependencyInjection
.Append()
.Append()
.Append()
- .Append();
+ .Append()
+ .Append();
// all built-in finders in the correct order,
// devs can then modify this list on application startup
diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs
index 58c57eb668..a3dc8b0e58 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 ac5137e9ab..94b8b70c1b 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;
}
@@ -265,6 +280,7 @@ namespace Umbraco.Cms.Core.DependencyInjection
Services.AddSingleton();
// Register telemetry service used to gather data about installed packages
+ Services.AddUnique();
Services.AddUnique();
Services.AddUnique();
@@ -291,6 +307,7 @@ namespace Umbraco.Cms.Core.DependencyInjection
Services.AddUnique();
Services.AddUnique();
Services.AddUnique();
+ Services.AddUnique();
Services.AddUnique(factory => new ExternalLoginService(
factory.GetRequiredService(),
factory.GetRequiredService(),
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/Events/RelateOnCopyNotificationHandler.cs b/src/Umbraco.Core/Events/RelateOnCopyNotificationHandler.cs
index 44d2eb6ee7..f37d8723a7 100644
--- a/src/Umbraco.Core/Events/RelateOnCopyNotificationHandler.cs
+++ b/src/Umbraco.Core/Events/RelateOnCopyNotificationHandler.cs
@@ -33,7 +33,8 @@ namespace Umbraco.Cms.Core.Events
Constants.Conventions.RelationTypes.RelateDocumentOnCopyName,
true,
Constants.ObjectTypes.Document,
- Constants.ObjectTypes.Document);
+ Constants.ObjectTypes.Document,
+ false);
_relationService.Save(relationType);
}
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/Extensions/ObjectExtensions.cs b/src/Umbraco.Core/Extensions/ObjectExtensions.cs
index 1265052308..1ba7e0fc4d 100644
--- a/src/Umbraco.Core/Extensions/ObjectExtensions.cs
+++ b/src/Umbraco.Core/Extensions/ObjectExtensions.cs
@@ -624,7 +624,6 @@ namespace Umbraco.Extensions
if (type == typeof(sbyte)) return XmlConvert.ToString((sbyte)value);
if (type == typeof(short)) return XmlConvert.ToString((short)value);
if (type == typeof(TimeSpan)) return XmlConvert.ToString((TimeSpan)value);
- if (type == typeof(bool)) return XmlConvert.ToString((bool)value);
if (type == typeof(uint)) return XmlConvert.ToString((uint)value);
if (type == typeof(ulong)) return XmlConvert.ToString((ulong)value);
if (type == typeof(ushort)) return XmlConvert.ToString((ushort)value);
diff --git a/src/Umbraco.Core/Install/InstallSteps/TelemetryIdentifierStep.cs b/src/Umbraco.Core/Install/InstallSteps/TelemetryIdentifierStep.cs
index 6f4cd87bd9..5263ac935f 100644
--- a/src/Umbraco.Core/Install/InstallSteps/TelemetryIdentifierStep.cs
+++ b/src/Umbraco.Core/Install/InstallSteps/TelemetryIdentifierStep.cs
@@ -1,10 +1,13 @@
using System;
using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Install.Models;
+using Umbraco.Cms.Core.Telemetry;
+using Umbraco.Cms.Web.Common.DependencyInjection;
namespace Umbraco.Cms.Core.Install.InstallSteps
{
@@ -13,32 +16,30 @@ namespace Umbraco.Cms.Core.Install.InstallSteps
PerformsAppRestart = false)]
public class TelemetryIdentifierStep : InstallSetupStep
{
- private readonly ILogger _logger;
private readonly IOptions _globalSettings;
- private readonly IConfigManipulator _configManipulator;
+ private readonly ISiteIdentifierService _siteIdentifierService;
- public TelemetryIdentifierStep(ILogger logger, IOptions globalSettings, IConfigManipulator configManipulator)
+ public TelemetryIdentifierStep(
+ IOptions globalSettings,
+ ISiteIdentifierService siteIdentifierService)
{
- _logger = logger;
_globalSettings = globalSettings;
- _configManipulator = configManipulator;
+ _siteIdentifierService = siteIdentifierService;
+ }
+
+ [Obsolete("Use constructor that takes GlobalSettings and ISiteIdentifierService")]
+ public TelemetryIdentifierStep(
+ ILogger logger,
+ IOptions globalSettings,
+ IConfigManipulator configManipulator)
+ : this(globalSettings, StaticServiceProvider.Instance.GetRequiredService())
+ {
}
public override Task ExecuteAsync(object model)
{
- // Generate GUID
- var telemetrySiteIdentifier = Guid.NewGuid();
-
- try
- {
- _configManipulator.SetGlobalId(telemetrySiteIdentifier.ToString());
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Couldn't update config files with a telemetry site identifier");
- }
-
- return Task.FromResult(null);
+ _siteIdentifierService.TryCreateSiteIdentifier(out _);
+ return Task.FromResult(null);
}
public override bool RequiresExecution(object model)
diff --git a/src/Umbraco.Core/Install/Models/DatabaseModel.cs b/src/Umbraco.Core/Install/Models/DatabaseModel.cs
index f321e8fc89..37cdcb9610 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; } = null!;
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/Manifest/ManifestContentAppFactory.cs b/src/Umbraco.Core/Manifest/ManifestContentAppFactory.cs
index c52bd81e21..c4bc87e9a2 100644
--- a/src/Umbraco.Core/Manifest/ManifestContentAppFactory.cs
+++ b/src/Umbraco.Core/Manifest/ManifestContentAppFactory.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
@@ -70,6 +70,10 @@ namespace Umbraco.Cms.Core.Manifest
partA = "contentType";
partB = contentType?.Alias;
break;
+ case IDictionaryItem _:
+ partA = "dictionary";
+ partB = "*"; //Not really a different type for dictionary items
+ break;
default:
return null;
diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs
index d3b02e30a1..bc77e52624 100644
--- a/src/Umbraco.Core/Models/Content.cs
+++ b/src/Umbraco.Core/Models/Content.cs
@@ -198,7 +198,7 @@ namespace Umbraco.Cms.Core.Models
public bool IsCulturePublished(string culture)
// just check _publishInfos
// a non-available culture could not become published anyways
- => _publishInfos != null && _publishInfos.ContainsKey(culture);
+ => !culture.IsNullOrWhiteSpace() && _publishInfos != null && _publishInfos.ContainsKey(culture);
///
public bool IsCultureEdited(string culture)
diff --git a/src/Umbraco.Core/Models/ContentEditing/DictionaryDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/DictionaryDisplay.cs
index 41e49ba34d..d8cfaf1104 100644
--- a/src/Umbraco.Core/Models/ContentEditing/DictionaryDisplay.cs
+++ b/src/Umbraco.Core/Models/ContentEditing/DictionaryDisplay.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
@@ -17,6 +17,7 @@ namespace Umbraco.Cms.Core.Models.ContentEditing
{
Notifications = new List();
Translations = new List();
+ ContentApps = new List();
}
///
@@ -37,5 +38,11 @@ namespace Umbraco.Cms.Core.Models.ContentEditing
///
[DataMember(Name = "translations")]
public List Translations { get; private set; }
+
+ ///
+ /// Apps for the dictionary item
+ ///
+ [DataMember(Name = "apps")]
+ public List ContentApps { get; private set; }
}
}
diff --git a/src/Umbraco.Core/Models/ContentEditing/HistoryCleanup.cs b/src/Umbraco.Core/Models/ContentEditing/HistoryCleanup.cs
index b7bfb32808..a0d9bbbcb3 100644
--- a/src/Umbraco.Core/Models/ContentEditing/HistoryCleanup.cs
+++ b/src/Umbraco.Core/Models/ContentEditing/HistoryCleanup.cs
@@ -1,17 +1,34 @@
using System.Runtime.Serialization;
+using Umbraco.Cms.Core.Models.Entities;
namespace Umbraco.Cms.Core.Models.ContentEditing
{
[DataContract(Name = "historyCleanup", Namespace = "")]
- public class HistoryCleanup
+ public class HistoryCleanup : BeingDirtyBase
{
+ private bool _preventCleanup;
+ private int? _keepAllVersionsNewerThanDays;
+ private int? _keepLatestVersionPerDayForDays;
+
[DataMember(Name = "preventCleanup")]
- public bool PreventCleanup { get; set; }
+ public bool PreventCleanup
+ {
+ get => _preventCleanup;
+ set => SetPropertyValueAndDetectChanges(value, ref _preventCleanup, nameof(PreventCleanup));
+ }
[DataMember(Name = "keepAllVersionsNewerThanDays")]
- public int? KeepAllVersionsNewerThanDays { get; set; }
+ public int? KeepAllVersionsNewerThanDays
+ {
+ get => _keepAllVersionsNewerThanDays;
+ set => SetPropertyValueAndDetectChanges(value, ref _keepAllVersionsNewerThanDays, nameof(KeepAllVersionsNewerThanDays));
+ }
[DataMember(Name = "keepLatestVersionPerDayForDays")]
- public int? KeepLatestVersionPerDayForDays { get; set; }
+ public int? KeepLatestVersionPerDayForDays
+ {
+ get => _keepLatestVersionPerDayForDays;
+ set => SetPropertyValueAndDetectChanges(value, ref _keepLatestVersionPerDayForDays, nameof(KeepLatestVersionPerDayForDays));
+ }
}
}
diff --git a/src/Umbraco.Core/Models/ContentEditing/RelationTypeDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/RelationTypeDisplay.cs
index 0c649c092f..906fdf3a40 100644
--- a/src/Umbraco.Core/Models/ContentEditing/RelationTypeDisplay.cs
+++ b/src/Umbraco.Core/Models/ContentEditing/RelationTypeDisplay.cs
@@ -55,5 +55,11 @@ namespace Umbraco.Cms.Core.Models.ContentEditing
///
[DataMember(Name = "notifications")]
public List Notifications { get; private set; }
+
+ ///
+ /// Gets or sets a boolean indicating whether the RelationType should be returned in "Used by"-queries.
+ ///
+ [DataMember(Name = "isDependency", IsRequired = true)]
+ public bool IsDependency { get; set; }
}
}
diff --git a/src/Umbraco.Core/Models/ContentEditing/RelationTypeSave.cs b/src/Umbraco.Core/Models/ContentEditing/RelationTypeSave.cs
index b72a03eec4..f541158095 100644
--- a/src/Umbraco.Core/Models/ContentEditing/RelationTypeSave.cs
+++ b/src/Umbraco.Core/Models/ContentEditing/RelationTypeSave.cs
@@ -23,5 +23,11 @@ namespace Umbraco.Cms.Core.Models.ContentEditing
///
[DataMember(Name = "childObjectType", IsRequired = false)]
public Guid? ChildObjectType { get; set; }
+
+ ///
+ /// Gets or sets a boolean indicating whether the RelationType should be returned in "Used by"-queries.
+ ///
+ [DataMember(Name = "isDependency", IsRequired = true)]
+ public bool IsDependency { get; set; }
}
}
diff --git a/src/Umbraco.Core/Models/ContentType.cs b/src/Umbraco.Core/Models/ContentType.cs
index 07cc46c79b..92c1b6cfe6 100644
--- a/src/Umbraco.Core/Models/ContentType.cs
+++ b/src/Umbraco.Core/Models/ContentType.cs
@@ -64,8 +64,8 @@ namespace Umbraco.Cms.Core.Models
/// we should not store direct entity
///
[IgnoreDataMember]
- public ITemplate? DefaultTemplate =>
- AllowedTemplates?.FirstOrDefault(x => x != null && x.Id == DefaultTemplateId);
+ public ITemplate DefaultTemplate =>
+ AllowedTemplates.FirstOrDefault(x => x != null && x.Id == DefaultTemplateId);
[DataMember]
@@ -82,21 +82,27 @@ namespace Umbraco.Cms.Core.Models
/// we should not store direct entity
///
[DataMember]
- public IEnumerable? AllowedTemplates
+ public IEnumerable AllowedTemplates
{
get => _allowedTemplates;
set
{
SetPropertyValueAndDetectChanges(value, ref _allowedTemplates, nameof(AllowedTemplates), TemplateComparer);
- if (_allowedTemplates?.Any(x => x.Id == _defaultTemplate) == false)
+ if (_allowedTemplates.Any(x => x.Id == _defaultTemplate) == false)
{
DefaultTemplateId = 0;
}
}
}
- public HistoryCleanup? HistoryCleanup { get; set; }
+ private HistoryCleanup? _historyCleanup;
+
+ public HistoryCleanup? HistoryCleanup
+ {
+ get => _historyCleanup;
+ set => SetPropertyValueAndDetectChanges(value, ref _historyCleanup, nameof(HistoryCleanup));
+ }
///
/// Determines if AllowedTemplates contains templateId
@@ -162,5 +168,8 @@ namespace Umbraco.Cms.Core.Models
///
IContentType IContentType.DeepCloneWithResetIdentities(string newAlias) =>
(IContentType)DeepCloneWithResetIdentities(newAlias);
+
+ ///
+ public override bool IsDirty() => base.IsDirty() || HistoryCleanup.IsDirty();
}
}
diff --git a/src/Umbraco.Core/Models/IRelationType.cs b/src/Umbraco.Core/Models/IRelationType.cs
index c48e13fdc3..cbc485f64b 100644
--- a/src/Umbraco.Core/Models/IRelationType.cs
+++ b/src/Umbraco.Core/Models/IRelationType.cs
@@ -4,6 +4,15 @@ using Umbraco.Cms.Core.Models.Entities;
namespace Umbraco.Cms.Core.Models
{
+ public interface IRelationTypeWithIsDependency : IRelationType
+ {
+ ///
+ /// Gets or sets a boolean indicating whether the RelationType should be returned in "Used by"-queries.
+ ///
+ [DataMember]
+ bool IsDependency { get; set; }
+ }
+
public interface IRelationType : IEntity, IRememberBeingDirty
{
///
diff --git a/src/Umbraco.Core/Models/Mapping/CommonMapper.cs b/src/Umbraco.Core/Models/Mapping/CommonMapper.cs
index ae96b57afe..3832654f45 100644
--- a/src/Umbraco.Core/Models/Mapping/CommonMapper.cs
+++ b/src/Umbraco.Core/Models/Mapping/CommonMapper.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Cms.Core.ContentApps;
@@ -48,6 +48,11 @@ namespace Umbraco.Cms.Core.Models.Mapping
}
public IEnumerable GetContentApps(IUmbracoEntity source)
+ {
+ return GetContentAppsForEntity(source);
+ }
+
+ public IEnumerable GetContentAppsForEntity(IEntity source)
{
var apps = _contentAppDefinitions.GetContentAppsFor(source).ToArray();
diff --git a/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs
index d93420ace6..6a2dd0d305 100644
--- a/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs
+++ b/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs
@@ -133,7 +133,7 @@ namespace Umbraco.Cms.Core.Models.Mapping
if (target is IContentTypeWithHistoryCleanup targetWithHistoryCleanup)
{
- targetWithHistoryCleanup.HistoryCleanup = source.HistoryCleanup;
+ MapHistoryCleanup(source, targetWithHistoryCleanup);
}
target.AllowedTemplates = source.AllowedTemplates?
@@ -147,6 +147,34 @@ namespace Umbraco.Cms.Core.Models.Mapping
: _fileService.GetTemplate(source.DefaultTemplate));
}
+ private static void MapHistoryCleanup(DocumentTypeSave source, IContentTypeWithHistoryCleanup target)
+ {
+ // If source history cleanup is null we don't have to map all properties
+ if (source.HistoryCleanup is null)
+ {
+ target.HistoryCleanup = null;
+ return;
+ }
+
+ // We need to reset the dirty properties, because it is otherwise true, just because the json serializer has set properties
+ target.HistoryCleanup.ResetDirtyProperties(false);
+ if (target.HistoryCleanup.PreventCleanup != source.HistoryCleanup.PreventCleanup)
+ {
+ target.HistoryCleanup.PreventCleanup = source.HistoryCleanup.PreventCleanup;
+ }
+
+ if (target.HistoryCleanup.KeepAllVersionsNewerThanDays != source.HistoryCleanup.KeepAllVersionsNewerThanDays)
+ {
+ target.HistoryCleanup.KeepAllVersionsNewerThanDays = source.HistoryCleanup.KeepAllVersionsNewerThanDays;
+ }
+
+ if (target.HistoryCleanup.KeepLatestVersionPerDayForDays !=
+ source.HistoryCleanup.KeepLatestVersionPerDayForDays)
+ {
+ target.HistoryCleanup.KeepLatestVersionPerDayForDays = source.HistoryCleanup.KeepLatestVersionPerDayForDays;
+ }
+ }
+
// no MapAll - take care
private void Map(MediaTypeSave source, IMediaType target, MapperContext context)
{
@@ -196,7 +224,7 @@ namespace Umbraco.Cms.Core.Models.Mapping
target.AllowCultureVariant = source.VariesByCulture();
target.AllowSegmentVariant = source.VariesBySegment();
- target.ContentApps = _commonMapper.GetContentApps(source);
+ target.ContentApps = _commonMapper.GetContentAppsForEntity(source);
//sync templates
if (source.AllowedTemplates is not null)
@@ -331,7 +359,10 @@ namespace Umbraco.Cms.Core.Models.Mapping
if (source.GroupId > 0)
{
- target.PropertyGroupId = new Lazy(() => source.GroupId, false);
+ if (target.PropertyGroupId?.Value != source.GroupId)
+ {
+ target.PropertyGroupId = new Lazy(() => source.GroupId, false);
+ }
}
target.Alias = source.Alias;
@@ -526,7 +557,15 @@ namespace Umbraco.Cms.Core.Models.Mapping
target.Thumbnail = source.Thumbnail;
target.AllowedAsRoot = source.AllowAsRoot;
- target.AllowedContentTypes = source.AllowedContentTypes.Select((t, i) => new ContentTypeSort(t, i));
+
+ bool allowedContentTypesUnchanged = target.AllowedContentTypes.Select(x => x.Id.Value)
+ .SequenceEqual(source.AllowedContentTypes);
+
+ if (allowedContentTypesUnchanged is false)
+ {
+ target.AllowedContentTypes = source.AllowedContentTypes.Select((t, i) => new ContentTypeSort(t, i));
+ }
+
if (!(target is IMemberType))
{
@@ -578,7 +617,7 @@ namespace Umbraco.Cms.Core.Models.Mapping
// ensure no duplicate alias, then assign the group properties collection
EnsureUniqueAliases(destProperties);
- if (destGroup is not null)
+ if (destGroup is not null && (destGroup.PropertyTypes.SupportsPublishing != isPublishing || destGroup.PropertyTypes.SequenceEqual(destProperties) is false))
{
destGroup.PropertyTypes = new PropertyTypeCollection(isPublishing, destProperties);
destGroups.Add(destGroup);
@@ -587,7 +626,11 @@ namespace Umbraco.Cms.Core.Models.Mapping
// ensure no duplicate name, then assign the groups collection
EnsureUniqueAliases(destGroups);
- target.PropertyGroups = new PropertyGroupCollection(destGroups);
+
+ if (target.PropertyGroups.SequenceEqual(destGroups) is false)
+ {
+ target.PropertyGroups = new PropertyGroupCollection(destGroups);
+ }
// because the property groups collection was rebuilt, there is no need to remove
// the old groups - they are just gone and will be cleared by the repository
diff --git a/src/Umbraco.Core/Models/Mapping/DictionaryMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/DictionaryMapDefinition.cs
index 290742eb1a..f355cba47d 100644
--- a/src/Umbraco.Core/Models/Mapping/DictionaryMapDefinition.cs
+++ b/src/Umbraco.Core/Models/Mapping/DictionaryMapDefinition.cs
@@ -1,6 +1,7 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
+using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Services;
@@ -14,12 +15,20 @@ namespace Umbraco.Cms.Core.Models.Mapping
public class DictionaryMapDefinition : IMapDefinition
{
private readonly ILocalizationService _localizationService;
+ private readonly CommonMapper _commonMapper;
+ [Obsolete("Use the constructor with the CommonMapper")]
public DictionaryMapDefinition(ILocalizationService localizationService)
{
_localizationService = localizationService;
}
+ public DictionaryMapDefinition(ILocalizationService localizationService, CommonMapper commonMapper)
+ {
+ _localizationService = localizationService;
+ _commonMapper = commonMapper;
+ }
+
public void DefineMaps(IUmbracoMapper mapper)
{
mapper.Define((source, context) => new EntityBasic(), Map);
@@ -44,6 +53,10 @@ namespace Umbraco.Cms.Core.Models.Mapping
target.Name = source.ItemKey;
target.ParentId = source.ParentId ?? Guid.Empty;
target.Udi = Udi.Create(Constants.UdiEntityType.DictionaryItem, source.Key);
+ if (_commonMapper != null)
+ {
+ target.ContentApps.AddRange(_commonMapper.GetContentAppsForEntity(source));
+ }
// build up the path to make it possible to set active item in tree
// TODO: check if there is a better way
diff --git a/src/Umbraco.Core/Models/Mapping/RelationMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/RelationMapDefinition.cs
index 714fb8431f..b0aaab9537 100644
--- a/src/Umbraco.Core/Models/Mapping/RelationMapDefinition.cs
+++ b/src/Umbraco.Core/Models/Mapping/RelationMapDefinition.cs
@@ -30,6 +30,11 @@ namespace Umbraco.Cms.Core.Models.Mapping
target.ChildObjectType = source.ChildObjectType;
target.Id = source.Id;
target.IsBidirectional = source.IsBidirectional;
+
+ if (source is IRelationTypeWithIsDependency sourceWithIsDependency)
+ {
+ target.IsDependency = sourceWithIsDependency.IsDependency;
+ }
target.Key = source.Key;
target.Name = source.Name;
target.Alias = source.Alias;
@@ -77,6 +82,11 @@ namespace Umbraco.Cms.Core.Models.Mapping
target.ChildObjectType = source.ChildObjectType;
target.Id = source.Id.TryConvertTo().Result;
target.IsBidirectional = source.IsBidirectional;
+ if (target is IRelationTypeWithIsDependency targetWithIsDependency)
+ {
+ targetWithIsDependency.IsDependency = source.IsDependency;
+ }
+
target.Key = source.Key;
target.Name = source.Name;
target.ParentObjectType = source.ParentObjectType;
diff --git a/src/Umbraco.Core/Models/RelationItem.cs b/src/Umbraco.Core/Models/RelationItem.cs
new file mode 100644
index 0000000000..cebbc20951
--- /dev/null
+++ b/src/Umbraco.Core/Models/RelationItem.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Runtime.Serialization;
+using Umbraco.Cms.Core.Models.Entities;
+
+namespace Umbraco.Cms.Core.Models
+{
+ [DataContract(Name = "relationItem", Namespace = "")]
+ public class RelationItem
+ {
+ [DataMember(Name = "id")]
+ public int NodeId { get; set; }
+
+ [DataMember(Name = "key")]
+ public Guid NodeKey { get; set; }
+
+ [DataMember(Name = "name")]
+ public string NodeName { get; set; }
+
+ [DataMember(Name = "type")]
+ public string NodeType { get; set; }
+
+ [DataMember(Name = "udi")]
+ public Udi NodeUdi => Udi.Create(NodeType, NodeKey);
+
+ [DataMember(Name = "icon")]
+ public string ContentTypeIcon { get; set; }
+
+ [DataMember(Name = "alias")]
+ public string ContentTypeAlias { get; set; }
+
+ [DataMember(Name = "contentTypeName")]
+ public string ContentTypeName { get; set; }
+
+ [DataMember(Name = "relationTypeName")]
+ public string RelationTypeName { get; set; }
+
+ [DataMember(Name = "relationTypeIsBidirectional")]
+ public bool RelationTypeIsBidirectional { get; set; }
+
+ [DataMember(Name = "relationTypeIsDependency")]
+ public bool RelationTypeIsDependency { get; set; }
+
+ }
+}
diff --git a/src/Umbraco.Core/Models/RelationType.cs b/src/Umbraco.Core/Models/RelationType.cs
index 2b59b52262..fcc5ab699e 100644
--- a/src/Umbraco.Core/Models/RelationType.cs
+++ b/src/Umbraco.Core/Models/RelationType.cs
@@ -9,20 +9,28 @@ namespace Umbraco.Cms.Core.Models
///
[Serializable]
[DataContract(IsReference = true)]
- public class RelationType : EntityBase, IRelationType
+ public class RelationType : EntityBase, IRelationType, IRelationTypeWithIsDependency
{
private string _name;
private string _alias;
private bool _isBidirectional;
+ private bool _isDependency;
private Guid? _parentObjectType;
private Guid? _childObjectType;
public RelationType(string alias, string name)
- : this(name: name, alias: alias, false, null, null)
+ : this(name: name, alias: alias, false, null, null, false)
{
}
+ [Obsolete("Use ctor with isDependency parameter")]
public RelationType(string name, string alias, bool isBidrectional, Guid? parentObjectType, Guid? childObjectType)
+ :this(name,alias,isBidrectional, parentObjectType, childObjectType, false)
+ {
+
+ }
+
+ public RelationType(string name, string alias, bool isBidrectional, Guid? parentObjectType, Guid? childObjectType, bool isDependency)
{
if (name == null) throw new ArgumentNullException(nameof(name));
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name));
@@ -32,6 +40,7 @@ namespace Umbraco.Cms.Core.Models
_name = name;
_alias = alias;
_isBidirectional = isBidrectional;
+ _isDependency = isDependency;
_parentObjectType = parentObjectType;
_childObjectType = childObjectType;
}
@@ -88,5 +97,11 @@ namespace Umbraco.Cms.Core.Models
set => SetPropertyValueAndDetectChanges(value, ref _childObjectType, nameof(ChildObjectType));
}
+
+ public bool IsDependency
+ {
+ get => _isDependency;
+ set => SetPropertyValueAndDetectChanges(value, ref _isDependency, nameof(IsDependency));
+ }
}
}
diff --git a/src/Umbraco.Core/Packaging/PackagesRepository.cs b/src/Umbraco.Core/Packaging/PackagesRepository.cs
index 4183575070..4e68cf9f2e 100644
--- a/src/Umbraco.Core/Packaging/PackagesRepository.cs
+++ b/src/Umbraco.Core/Packaging/PackagesRepository.cs
@@ -5,7 +5,6 @@ using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
-using System.Text;
using System.Xml.Linq;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
@@ -33,7 +32,7 @@ namespace Umbraco.Cms.Core.Packaging
private readonly IEntityXmlSerializer _serializer;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly string _packageRepositoryFileName;
- private readonly string _mediaFolderPath;
+ private readonly string _createdPackagesFolderPath;
private readonly string _packagesFolderPath;
private readonly string _tempFolderPath;
private readonly PackageDefinitionXmlParser _parser;
@@ -93,7 +92,7 @@ namespace Umbraco.Cms.Core.Packaging
_tempFolderPath = tempFolderPath ?? Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "PackageFiles";
_packagesFolderPath = packagesFolderPath ?? Constants.SystemDirectories.Packages;
- _mediaFolderPath = mediaFolderPath ?? Path.Combine(globalSettings.Value.UmbracoMediaPhysicalRootPath, Constants.SystemDirectories.CreatedPackages);
+ _createdPackagesFolderPath = mediaFolderPath ?? Constants.SystemDirectories.CreatedPackages;
_parser = new PackageDefinitionXmlParser();
_mediaService = mediaService;
@@ -250,15 +249,8 @@ namespace Umbraco.Cms.Core.Packaging
}
}
-
-
- var directoryName =
- _hostingEnvironment.MapPathWebRoot(Path.Combine(_mediaFolderPath, definition.Name.Replace(' ', '_')));
-
- if (Directory.Exists(directoryName) == false)
- {
- Directory.CreateDirectory(directoryName);
- }
+ var directoryName = _hostingEnvironment.MapPathContentRoot(Path.Combine(_createdPackagesFolderPath, definition.Name.Replace(' ', '_')));
+ Directory.CreateDirectory(directoryName);
var finalPackagePath = Path.Combine(directoryName, fileName);
@@ -276,14 +268,14 @@ namespace Umbraco.Cms.Core.Packaging
}
finally
{
- //Clean up
+ // Clean up
Directory.Delete(temporaryPath, true);
}
}
private void ValidatePackage(PackageDefinition definition)
{
- //ensure it's valid
+ // ensure it's valid
var context = new ValidationContext(definition, serviceProvider: null, items: null);
var results = new List();
var isValid = Validator.TryValidateObject(definition, context, results);
@@ -740,7 +732,6 @@ namespace Umbraco.Cms.Core.Packaging
private XDocument EnsureStorage(out string packagesFile)
{
var packagesFolder = _hostingEnvironment.MapPathContentRoot(_packagesFolderPath);
- //ensure it exists
Directory.CreateDirectory(packagesFolder);
packagesFile = _hostingEnvironment.MapPathContentRoot(CreatedPackagesFile);
@@ -748,6 +739,8 @@ namespace Umbraco.Cms.Core.Packaging
{
var xml = new XDocument(new XElement("packages"));
xml.Save(packagesFile);
+
+ return xml;
}
var packagesXml = XDocument.Load(packagesFile);
@@ -757,9 +750,16 @@ namespace Umbraco.Cms.Core.Packaging
public void DeleteLocalRepositoryFiles()
{
var packagesFile = _hostingEnvironment.MapPathContentRoot(CreatedPackagesFile);
- File.Delete(packagesFile);
+ if (File.Exists(packagesFile))
+ {
+ File.Delete(packagesFile);
+ }
+
var packagesFolder = _hostingEnvironment.MapPathContentRoot(_packagesFolderPath);
- Directory.Delete(packagesFolder);
+ if (Directory.Exists(packagesFolder))
+ {
+ Directory.Delete(packagesFolder);
+ }
}
}
}
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.Core/Persistence/Constants-Locks.cs b/src/Umbraco.Core/Persistence/Constants-Locks.cs
index 5312bf6886..3c0b2c4d28 100644
--- a/src/Umbraco.Core/Persistence/Constants-Locks.cs
+++ b/src/Umbraco.Core/Persistence/Constants-Locks.cs
@@ -65,6 +65,11 @@ namespace Umbraco.Cms.Core
/// All languages.
///
public const int Languages = -340;
+
+ ///
+ /// ScheduledPublishing job.
+ ///
+ public const int ScheduledPublishing = -341;
}
}
}
diff --git a/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs
index 484cd1917b..0165b9eb39 100644
--- a/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
diff --git a/src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs
new file mode 100644
index 0000000000..42746a9565
--- /dev/null
+++ b/src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models.Entities;
+
+namespace Umbraco.Cms.Core.Persistence.Repositories
+{
+ public interface ITrackedReferencesRepository
+ {
+ IEnumerable GetPagedRelationsForItems(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency,out long totalRecords);
+ IEnumerable GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency,out long totalRecords);
+ IEnumerable GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency,out long totalRecords);
+ }
+}
diff --git a/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs b/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs
index ab84f7131b..b8e3e597a4 100644
--- a/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs
+++ b/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs
@@ -6,7 +6,6 @@ using System.Linq;
using System.Runtime.Serialization;
using System.Xml.Linq;
using Microsoft.Extensions.Logging;
-using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Editors;
@@ -158,17 +157,15 @@ namespace Umbraco.Cms.Core.PropertyEditors
/// ValueType was out of range.
internal Attempt TryConvertValueToCrlType(object? value)
{
- // Ensure empty string values are converted to null
- if (value is string s && string.IsNullOrWhiteSpace(s))
+ // Ensure empty string and JSON values are converted to null
+ if (value is string stringValue && string.IsNullOrWhiteSpace(stringValue))
{
value = null;
}
-
- // Ensure JSON is serialized properly (without indentation or converted to null when empty)
- if (value is not null && ValueType.InvariantEquals(ValueTypes.Json))
+ else if (value is not string && ValueType.InvariantEquals(ValueTypes.Json))
{
+ // Only serialize value when it's not already a string
var jsonValue = _jsonSerializer?.Serialize(value);
-
if (jsonValue?.DetectIsEmptyJson() ?? false)
{
value = null;
diff --git a/src/Umbraco.Core/Routing/DefaultUrlProvider.cs b/src/Umbraco.Core/Routing/DefaultUrlProvider.cs
index 1b75eb1aeb..7e3a46f7a8 100644
--- a/src/Umbraco.Core/Routing/DefaultUrlProvider.cs
+++ b/src/Umbraco.Core/Routing/DefaultUrlProvider.cs
@@ -161,7 +161,7 @@ namespace Umbraco.Cms.Core.Routing
: DomainUtilities.DomainForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, int.Parse(route.Substring(0, pos), CultureInfo.InvariantCulture), current, culture);
var defaultCulture = _localizationService.GetDefaultLanguageIsoCode();
- if (domainUri is not null || culture is null || culture.Equals(defaultCulture, StringComparison.InvariantCultureIgnoreCase))
+ if (domainUri is not null || string.IsNullOrEmpty(culture) || culture.Equals(defaultCulture, StringComparison.InvariantCultureIgnoreCase))
{
var url = AssembleUrl(domainUri, path, current, mode).ToString();
return UrlInfo.Url(url, culture);
diff --git a/src/Umbraco.Core/Routing/UrlProviderExtensions.cs b/src/Umbraco.Core/Routing/UrlProviderExtensions.cs
index 07878be7f5..7686b1a49a 100644
--- a/src/Umbraco.Core/Routing/UrlProviderExtensions.cs
+++ b/src/Umbraco.Core/Routing/UrlProviderExtensions.cs
@@ -269,16 +269,10 @@ namespace Umbraco.Extensions
if (!pcr.HasPublishedContent())
{
- var logMsg = nameof(DetectCollisionAsync) +
+ const string logMsg = nameof(DetectCollisionAsync) +
" did not resolve a content item for original url: {Url}, translated to {TranslatedUrl} and culture: {Culture}";
- if (pcr.IgnorePublishedContentCollisions)
- {
- logger.LogDebug(logMsg, url, uri, culture);
- }
- else
- {
- logger.LogDebug(logMsg, url, uri, culture);
- }
+
+ logger.LogDebug(logMsg, url, uri, culture);
var urlInfo = UrlInfo.Message(textService.Localize("content", "routeErrorCannotRoute"), culture);
return Attempt.Succeed(urlInfo);
diff --git a/src/Umbraco.Core/Runtime/IMainDomKeyGenerator.cs b/src/Umbraco.Core/Runtime/IMainDomKeyGenerator.cs
new file mode 100644
index 0000000000..5b8fb819e6
--- /dev/null
+++ b/src/Umbraco.Core/Runtime/IMainDomKeyGenerator.cs
@@ -0,0 +1,13 @@
+namespace Umbraco.Cms.Core.Runtime
+{
+ ///
+ /// Defines a class which can generate a distinct key for a MainDom boundary.
+ ///
+ public interface IMainDomKeyGenerator
+ {
+ ///
+ /// Returns a key that signifies a MainDom boundary.
+ ///
+ string GenerateKey();
+ }
+}
diff --git a/src/Umbraco.Core/Runtime/MainDom.cs b/src/Umbraco.Core/Runtime/MainDom.cs
index e09a8d2205..0198382b2a 100644
--- a/src/Umbraco.Core/Runtime/MainDom.cs
+++ b/src/Umbraco.Core/Runtime/MainDom.cs
@@ -87,7 +87,7 @@ namespace Umbraco.Cms.Core.Runtime
if (_isMainDom.HasValue == false)
{
- throw new InvalidOperationException("Register called when MainDom has not been acquired");
+ throw new InvalidOperationException("Register called before IsMainDom has been established");
}
else if (_isMainDom == false)
{
@@ -225,7 +225,7 @@ namespace Umbraco.Cms.Core.Runtime
{
if (!_isMainDom.HasValue)
{
- throw new InvalidOperationException("MainDom has not been acquired yet");
+ throw new InvalidOperationException("IsMainDom has not been established yet");
}
return _isMainDom.Value;
}
diff --git a/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs b/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs
index cd205a0542..5d2248906e 100644
--- a/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs
+++ b/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs
@@ -1,4 +1,5 @@
using System;
+using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
@@ -22,6 +23,11 @@ namespace Umbraco.Cms.Core.Runtime
public MainDomSemaphoreLock(ILogger logger, IHostingEnvironment hostingEnvironment)
{
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ throw new PlatformNotSupportedException("MainDomSemaphoreLock is only supported on Windows.");
+ }
+
var mainDomId = MainDom.GetMainDomId(hostingEnvironment);
var lockName = "UMBRACO-" + mainDomId + "-MAINDOM-LCK";
_systemLock = new SystemLock(lockName);
diff --git a/src/Umbraco.Core/Security/LegacyPasswordSecurity.cs b/src/Umbraco.Core/Security/LegacyPasswordSecurity.cs
index 8f6813e7ba..35528a48ca 100644
--- a/src/Umbraco.Core/Security/LegacyPasswordSecurity.cs
+++ b/src/Umbraco.Core/Security/LegacyPasswordSecurity.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.ComponentModel;
using System.Security.Cryptography;
using System.Text;
@@ -144,7 +144,7 @@ namespace Umbraco.Cms.Core.Security
var saltBytes = Convert.FromBase64String(salt);
byte[] inArray;
- var hashAlgorithm = GetHashAlgorithm(algorithmType);
+ using var hashAlgorithm = GetHashAlgorithm(algorithmType);
var algorithm = hashAlgorithm as KeyedHashAlgorithm;
if (algorithm != null)
{
@@ -209,11 +209,21 @@ namespace Umbraco.Cms.Core.Security
{
// This is for the v6-v8 hashing algorithm
if (algorithm.InvariantEquals(Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName))
+ {
return true;
+ }
+
+ // Default validation value for old machine keys (switched to HMACSHA256 aspnet 4 https://docs.microsoft.com/en-us/aspnet/whitepapers/aspnet4/breaking-changes)
+ if (algorithm.InvariantEquals("SHA1"))
+ {
+ return true;
+ }
// This is for the <= v4 hashing algorithm
if (IsLegacySHA1Algorithm(algorithm))
+ {
return true;
+ }
return false;
}
@@ -227,7 +237,7 @@ namespace Umbraco.Cms.Core.Security
/// The encoded password.
private string HashLegacySHA1Password(string password)
{
- var hashAlgorithm = GetLegacySHA1Algorithm(password);
+ using var hashAlgorithm = GetLegacySHA1Algorithm(password);
var hash = Convert.ToBase64String(hashAlgorithm.ComputeHash(Encoding.Unicode.GetBytes(password)));
return hash;
}
diff --git a/src/Umbraco.Core/Services/IRelationService.cs b/src/Umbraco.Core/Services/IRelationService.cs
index 3f303eb12f..c4060cd695 100644
--- a/src/Umbraco.Core/Services/IRelationService.cs
+++ b/src/Umbraco.Core/Services/IRelationService.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
@@ -215,7 +215,7 @@ namespace Umbraco.Cms.Core.Services
///
///
///
- ///
+ /// An enumerable list of
IEnumerable GetPagedParentEntitiesByChildId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes);
///
@@ -225,7 +225,7 @@ namespace Umbraco.Cms.Core.Services
///
///
///
- ///
+ /// An enumerable list of
IEnumerable GetPagedChildEntitiesByParentId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes);
///
diff --git a/src/Umbraco.Core/Services/IServerRegistrationService.cs b/src/Umbraco.Core/Services/IServerRegistrationService.cs
index f9aaf7e33f..e469de9a06 100644
--- a/src/Umbraco.Core/Services/IServerRegistrationService.cs
+++ b/src/Umbraco.Core/Services/IServerRegistrationService.cs
@@ -37,6 +37,17 @@ namespace Umbraco.Cms.Core.Services
/// from the database.
IEnumerable? GetActiveServers(bool refresh = false);
+ ///
+ /// Return all servers (active and inactive).
+ ///
+ /// A value indicating whether to force-refresh the cache.
+ /// All servers.
+ /// By default this method will rely on the repository's cache, which is updated each
+ /// time the current server is touched, and the period depends on the configuration. Use the
+ /// parameter to force a cache refresh and reload all servers
+ /// from the database.
+ IEnumerable GetServers(bool refresh = false);
+
///
/// Gets the role of the current server.
///
diff --git a/src/Umbraco.Core/Services/ITrackedReferencesService.cs b/src/Umbraco.Core/Services/ITrackedReferencesService.cs
new file mode 100644
index 0000000000..eee8a324df
--- /dev/null
+++ b/src/Umbraco.Core/Services/ITrackedReferencesService.cs
@@ -0,0 +1,15 @@
+using Umbraco.Cms.Core.Models;
+
+namespace Umbraco.Cms.Core.Services
+{
+ public interface ITrackedReferencesService
+ {
+ PagedResult GetPagedRelationsForItems(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency);
+
+
+ PagedResult GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency);
+
+
+ PagedResult GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency);
+ }
+}
diff --git a/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs b/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs
index 9c9132d9bb..f26843b028 100644
--- a/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs
+++ b/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
diff --git a/src/Umbraco.Core/Services/RelationService.cs b/src/Umbraco.Core/Services/RelationService.cs
index 3122e6e1f8..667622c5c1 100644
--- a/src/Umbraco.Core/Services/RelationService.cs
+++ b/src/Umbraco.Core/Services/RelationService.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging;
diff --git a/src/Umbraco.Core/Services/TrackedReferencesService.cs b/src/Umbraco.Core/Services/TrackedReferencesService.cs
new file mode 100644
index 0000000000..c43d2ca57b
--- /dev/null
+++ b/src/Umbraco.Core/Services/TrackedReferencesService.cs
@@ -0,0 +1,50 @@
+using System.Linq;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Persistence.Repositories;
+using Umbraco.Cms.Core.Scoping;
+
+namespace Umbraco.Cms.Core.Services
+{
+ public class TrackedReferencesService : ITrackedReferencesService
+ {
+ private readonly ITrackedReferencesRepository _trackedReferencesRepository;
+ private readonly IScopeProvider _scopeProvider;
+ private readonly IEntityService _entityService;
+
+ public TrackedReferencesService(ITrackedReferencesRepository trackedReferencesRepository, IScopeProvider scopeProvider, IEntityService entityService)
+ {
+ _trackedReferencesRepository = trackedReferencesRepository;
+ _scopeProvider = scopeProvider;
+ _entityService = entityService;
+ }
+
+ public PagedResult GetPagedRelationsForItems(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency)
+ {
+ using IScope scope = _scopeProvider.CreateScope(autoComplete: true);
+ var items = _trackedReferencesRepository.GetPagedRelationsForItems(ids, pageIndex, pageSize, filterMustBeIsDependency, out var totalItems);
+
+ return new PagedResult(totalItems, pageIndex+1, pageSize) { Items = items };
+ }
+
+ public PagedResult GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency)
+ {
+ using IScope scope = _scopeProvider.CreateScope(autoComplete: true);
+ var items = _trackedReferencesRepository.GetPagedItemsWithRelations(ids, pageIndex, pageSize, filterMustBeIsDependency, out var totalItems);
+
+ return new PagedResult(totalItems, pageIndex+1, pageSize) { Items = items };
+ }
+
+ public PagedResult GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency)
+ {
+ using IScope scope = _scopeProvider.CreateScope(autoComplete: true);
+
+ var items = _trackedReferencesRepository.GetPagedDescendantsInReferences(
+ parentId,
+ pageIndex,
+ pageSize,
+ filterMustBeIsDependency,
+ out var totalItems);
+ return new PagedResult(totalItems, pageIndex+1, pageSize) { Items = items };
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Telemetry/ISiteIdentifierService.cs b/src/Umbraco.Core/Telemetry/ISiteIdentifierService.cs
new file mode 100644
index 0000000000..7fd0ee5a85
--- /dev/null
+++ b/src/Umbraco.Core/Telemetry/ISiteIdentifierService.cs
@@ -0,0 +1,31 @@
+using System;
+
+namespace Umbraco.Cms.Core.Telemetry
+{
+ ///
+ /// Used to get and create the site identifier
+ ///
+ public interface ISiteIdentifierService
+ {
+
+ ///
+ /// Tries to get the site identifier
+ ///
+ /// True if success.
+ bool TryGetSiteIdentifier(out Guid siteIdentifier);
+
+ ///
+ /// Creates the site identifier and writes it to config.
+ ///
+ /// asd.
+ /// True if success.
+ bool TryCreateSiteIdentifier(out Guid createdGuid);
+
+ ///
+ /// Tries to get the site identifier or otherwise create it if it doesn't exist.
+ ///
+ /// The out parameter for the existing or create site identifier.
+ /// True if success.
+ bool TryGetOrCreateSiteIdentifier(out Guid siteIdentifier);
+ }
+}
diff --git a/src/Umbraco.Core/Telemetry/SiteIdentifierService.cs b/src/Umbraco.Core/Telemetry/SiteIdentifierService.cs
new file mode 100644
index 0000000000..b6e40665c1
--- /dev/null
+++ b/src/Umbraco.Core/Telemetry/SiteIdentifierService.cs
@@ -0,0 +1,81 @@
+using System;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core.Configuration;
+using Umbraco.Cms.Core.Configuration.Models;
+
+namespace Umbraco.Cms.Core.Telemetry
+{
+ ///
+ internal class SiteIdentifierService : ISiteIdentifierService
+ {
+ private GlobalSettings _globalSettings;
+ private readonly IConfigManipulator _configManipulator;
+ private readonly ILogger _logger;
+
+ public SiteIdentifierService(
+ IOptionsMonitor optionsMonitor,
+ IConfigManipulator configManipulator,
+ ILogger logger)
+ {
+ _globalSettings = optionsMonitor.CurrentValue;
+ optionsMonitor.OnChange(globalSettings => _globalSettings = globalSettings);
+ _configManipulator = configManipulator;
+ _logger = logger;
+ }
+
+ ///
+ public bool TryGetSiteIdentifier(out Guid siteIdentifier)
+ {
+ // Parse telemetry string as a GUID & verify its a GUID and not some random string
+ // since users may have messed with or decided to empty the app setting or put in something random
+ if (Guid.TryParse(_globalSettings.Id, out var parsedTelemetryId) is false
+ || parsedTelemetryId == Guid.Empty)
+ {
+ siteIdentifier = Guid.Empty;
+ return false;
+ }
+
+ siteIdentifier = parsedTelemetryId;
+ return true;
+ }
+
+ ///
+ public bool TryGetOrCreateSiteIdentifier(out Guid siteIdentifier)
+ {
+ if (TryGetSiteIdentifier(out Guid existingId))
+ {
+ siteIdentifier = existingId;
+ return true;
+ }
+
+ if (TryCreateSiteIdentifier(out Guid createdId))
+ {
+ siteIdentifier = createdId;
+ return true;
+ }
+
+ siteIdentifier = Guid.Empty;
+ return false;
+ }
+
+ ///
+ public bool TryCreateSiteIdentifier(out Guid createdGuid)
+ {
+ createdGuid = Guid.NewGuid();
+
+ try
+ {
+ _configManipulator.SetGlobalId(createdGuid.ToString());
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Couldn't update config files with a telemetry site identifier");
+ createdGuid = Guid.Empty;
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Telemetry/TelemetryService.cs b/src/Umbraco.Core/Telemetry/TelemetryService.cs
index 7927772e0c..ce3696fca4 100644
--- a/src/Umbraco.Core/Telemetry/TelemetryService.cs
+++ b/src/Umbraco.Core/Telemetry/TelemetryService.cs
@@ -3,9 +3,7 @@
using System;
using System.Collections.Generic;
-using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration;
-using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Manifest;
using Umbraco.Cms.Core.Telemetry.Models;
using Umbraco.Extensions;
@@ -15,27 +13,27 @@ namespace Umbraco.Cms.Core.Telemetry
///
internal class TelemetryService : ITelemetryService
{
- private readonly IOptionsMonitor _globalSettings;
private readonly IManifestParser _manifestParser;
private readonly IUmbracoVersion _umbracoVersion;
+ private readonly ISiteIdentifierService _siteIdentifierService;
///
/// Initializes a new instance of the class.
///
public TelemetryService(
- IOptionsMonitor globalSettings,
IManifestParser manifestParser,
- IUmbracoVersion umbracoVersion)
+ IUmbracoVersion umbracoVersion,
+ ISiteIdentifierService siteIdentifierService)
{
_manifestParser = manifestParser;
_umbracoVersion = umbracoVersion;
- _globalSettings = globalSettings;
+ _siteIdentifierService = siteIdentifierService;
}
///
public bool TryGetTelemetryReportData(out TelemetryReportData? telemetryReportData)
{
- if (TryGetTelemetryId(out Guid telemetryId) is false)
+ if (_siteIdentifierService.TryGetOrCreateSiteIdentifier(out Guid telemetryId) is false)
{
telemetryReportData = null;
return false;
@@ -45,28 +43,14 @@ namespace Umbraco.Cms.Core.Telemetry
{
Id = telemetryId,
Version = _umbracoVersion.SemanticVersion.ToSemanticStringWithoutBuild(),
- Packages = GetPackageTelemetry()
+ Packages = GetPackageTelemetry(),
};
return true;
}
- private bool TryGetTelemetryId(out Guid telemetryId)
- {
- // Parse telemetry string as a GUID & verify its a GUID and not some random string
- // since users may have messed with or decided to empty the app setting or put in something random
- if (Guid.TryParse(_globalSettings.CurrentValue.Id, out Guid parsedTelemetryId) is false)
- {
- telemetryId = Guid.Empty;
- return false;
- }
-
- telemetryId = parsedTelemetryId;
- return true;
- }
-
private IEnumerable GetPackageTelemetry()
{
- List packages = new ();
+ List packages = new();
IEnumerable manifests = _manifestParser.GetManifests();
foreach (PackageManifest manifest in manifests)
@@ -79,7 +63,7 @@ namespace Umbraco.Cms.Core.Telemetry
packages.Add(new PackageTelemetry
{
Name = manifest.PackageName,
- Version = manifest.Version ?? string.Empty
+ Version = manifest.Version ?? string.Empty,
});
}
diff --git a/src/Umbraco.Examine.Lucene/LuceneIndexDiagnostics.cs b/src/Umbraco.Examine.Lucene/LuceneIndexDiagnostics.cs
index ba3945c6e5..33230a296d 100644
--- a/src/Umbraco.Examine.Lucene/LuceneIndexDiagnostics.cs
+++ b/src/Umbraco.Examine.Lucene/LuceneIndexDiagnostics.cs
@@ -27,7 +27,11 @@ namespace Umbraco.Cms.Infrastructure.Examine
IOptionsMonitor indexOptions)
{
_hostingEnvironment = hostingEnvironment;
- _indexOptions = indexOptions.Get(index.Name);
+ if (indexOptions != null)
+ {
+ _indexOptions = indexOptions.Get(index.Name);
+
+ }
Index = index;
Logger = logger;
}
@@ -35,7 +39,7 @@ namespace Umbraco.Cms.Infrastructure.Examine
public LuceneIndex Index { get; }
public ILogger Logger { get; }
-
+
public Attempt IsHealthy()
{
@@ -72,12 +76,12 @@ namespace Umbraco.Cms.Infrastructure.Examine
{
d[nameof(LuceneDirectoryIndexOptions.DirectoryFactory)] = _indexOptions.DirectoryFactory.GetType();
}
-
+
if (_indexOptions.IndexDeletionPolicy != null)
{
d[nameof(LuceneDirectoryIndexOptions.IndexDeletionPolicy)] = _indexOptions.IndexDeletionPolicy.GetType();
- }
-
+ }
+
}
return d;
diff --git a/src/Umbraco.Infrastructure/Configuration/JsonConfigManipulator.cs b/src/Umbraco.Infrastructure/Configuration/JsonConfigManipulator.cs
index 0bec8bc580..48c0540c67 100644
--- a/src/Umbraco.Infrastructure/Configuration/JsonConfigManipulator.cs
+++ b/src/Umbraco.Infrastructure/Configuration/JsonConfigManipulator.cs
@@ -2,26 +2,46 @@ using System;
using System.IO;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.Json;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
-using Umbraco.Cms.Core.Configuration;
+using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Web.Common.DependencyInjection;
namespace Umbraco.Cms.Core.Configuration
{
public class JsonConfigManipulator : IConfigManipulator
{
private readonly IConfiguration _configuration;
+ private readonly ILogger _logger;
private readonly object _locker = new object();
- public JsonConfigManipulator(IConfiguration configuration) => _configuration = configuration;
+ [Obsolete]
+ public JsonConfigManipulator(IConfiguration configuration)
+ : this(configuration, StaticServiceProvider.Instance.GetRequiredService>())
+ { }
- public string UmbracoConnectionPath { get; } = $"ConnectionStrings:{ Cms.Core.Constants.System.UmbracoConnectionName}";
+ public JsonConfigManipulator(
+ IConfiguration configuration,
+ ILogger logger)
+ {
+ _configuration = configuration;
+ _logger = logger;
+ }
+
+ public string UmbracoConnectionPath { get; } = $"ConnectionStrings:{Cms.Core.Constants.System.UmbracoConnectionName}";
public void RemoveConnectionString()
{
var provider = GetJsonConfigurationProvider(UmbracoConnectionPath);
var json = GetJson(provider);
+ if (json is null)
+ {
+ _logger.LogWarning("Failed to remove connection string from JSON configuration.");
+ return;
+ }
RemoveJsonKey(json, UmbracoConnectionPath);
@@ -33,6 +53,11 @@ namespace Umbraco.Cms.Core.Configuration
var provider = GetJsonConfigurationProvider();
var json = GetJson(provider);
+ if (json is null)
+ {
+ _logger.LogWarning("Failed to save connection string in JSON configuration.");
+ return;
+ }
var item = GetConnectionItem(connectionString, providerName);
@@ -50,6 +75,11 @@ namespace Umbraco.Cms.Core.Configuration
var provider = GetJsonConfigurationProvider();
var json = GetJson(provider);
+ if (json is null)
+ {
+ _logger.LogWarning("Failed to save configuration key \"{Key}\" in JSON configuration.", key);
+ return;
+ }
JToken? token = json;
foreach (var propertyName in key.Split(new[] { ':' }))
@@ -79,6 +109,11 @@ namespace Umbraco.Cms.Core.Configuration
var provider = GetJsonConfigurationProvider();
var json = GetJson(provider);
+ if (json is null)
+ {
+ _logger.LogWarning("Failed to save enabled/disabled state for redirect URL tracking in JSON configuration.");
+ return;
+ }
var item = GetDisableRedirectUrlItem(disable);
@@ -95,6 +130,11 @@ namespace Umbraco.Cms.Core.Configuration
var provider = GetJsonConfigurationProvider();
var json = GetJson(provider);
+ if (json is null)
+ {
+ _logger.LogWarning("Failed to save global identifier in JSON configuration.");
+ return;
+ }
var item = GetGlobalIdItem(id);
@@ -155,8 +195,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();
@@ -182,13 +224,20 @@ namespace Umbraco.Cms.Core.Configuration
{
var jsonFilePath = Path.Combine(physicalFileProvider.Root, provider.Source.Path);
- using (var sw = new StreamWriter(jsonFilePath, false))
- using (var jsonTextWriter = new JsonTextWriter(sw)
+ try
{
- Formatting = Formatting.Indented,
- })
+ using (var sw = new StreamWriter(jsonFilePath, false))
+ using (var jsonTextWriter = new JsonTextWriter(sw)
+ {
+ Formatting = Formatting.Indented,
+ })
+ {
+ json?.WriteTo(jsonTextWriter);
+ }
+ }
+ catch (IOException exception)
{
- json?.WriteTo(jsonTextWriter);
+ _logger.LogWarning(exception, "JSON configuration could not be written: {path}", jsonFilePath);
}
}
}
@@ -198,19 +247,25 @@ namespace Umbraco.Cms.Core.Configuration
{
lock (_locker)
{
- if (provider.Source.FileProvider is PhysicalFileProvider physicalFileProvider)
+ if (provider.Source.FileProvider is not PhysicalFileProvider physicalFileProvider)
{
- var jsonFilePath = Path.Combine(physicalFileProvider.Root, provider.Source.Path);
-
- var serializer = new JsonSerializer();
- using (var sr = new StreamReader(jsonFilePath))
- using (var jsonTextReader = new JsonTextReader(sr))
- {
- return serializer.Deserialize(jsonTextReader);
- }
+ return null;
}
- return null;
+ var jsonFilePath = Path.Combine(physicalFileProvider.Root, provider.Source.Path);
+
+ try
+ {
+ var serializer = new JsonSerializer();
+ using var sr = new StreamReader(jsonFilePath);
+ using var jsonTextReader = new JsonTextReader(sr);
+ return serializer.Deserialize(jsonTextReader);
+ }
+ catch (IOException exception)
+ {
+ _logger.LogWarning(exception, "JSON configuration could not be read: {path}", jsonFilePath);
+ return null;
+ }
}
}
diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs
index 5a4faa6c43..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();
@@ -220,6 +223,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection
private static IUmbracoBuilder AddMainDom(this IUmbracoBuilder builder)
{
+ builder.Services.AddSingleton();
builder.Services.AddSingleton(factory =>
{
var globalSettings = factory.GetRequiredService>();
@@ -228,21 +232,29 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection
var dbCreator = factory.GetRequiredService();
var databaseSchemaCreatorFactory = factory.GetRequiredService();
- var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
var loggerFactory = factory.GetRequiredService();
var npocoMappers = factory.GetRequiredService();
+ var mainDomKeyGenerator = factory.GetRequiredService();
- return globalSettings.Value.MainDomLock.Equals("SqlMainDomLock") || isWindows == false
- ? (IMainDomLock)new SqlMainDomLock(
- loggerFactory.CreateLogger(),
+ switch (globalSettings.Value.MainDomLock)
+ {
+ case "SqlMainDomLock":
+ return new SqlMainDomLock(
loggerFactory,
globalSettings,
connectionStrings,
dbCreator,
- hostingEnvironment,
+ mainDomKeyGenerator,
databaseSchemaCreatorFactory,
- npocoMappers)
- : new MainDomSemaphoreLock(loggerFactory.CreateLogger(), hostingEnvironment);
+ npocoMappers);
+
+ case "MainDomSemaphoreLock":
+ return new MainDomSemaphoreLock(loggerFactory.CreateLogger(), hostingEnvironment);
+
+ case "FileSystemMainDomLock":
+ default:
+ return new FileSystemMainDomLock(loggerFactory.CreateLogger(), mainDomKeyGenerator, hostingEnvironment, factory.GetRequiredService>());
+ }
});
return builder;
diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs
index ccb515182e..fbb32671a1 100644
--- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs
+++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs
@@ -1,3 +1,4 @@
+using System.IO;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -49,7 +50,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection
ILogger logger = factory.GetRequiredService>();
GlobalSettings globalSettings = factory.GetRequiredService>().Value;
- var rootPath = hostingEnvironment.MapPathWebRoot(globalSettings.UmbracoMediaPhysicalRootPath);
+ var rootPath = Path.IsPathRooted(globalSettings.UmbracoMediaPhysicalRootPath) ? globalSettings.UmbracoMediaPhysicalRootPath : hostingEnvironment.MapPathWebRoot(globalSettings.UmbracoMediaPhysicalRootPath);
var rootUrl = hostingEnvironment.ToAbsolute(globalSettings.UmbracoMediaPath);
return new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, rootPath, rootUrl);
});
diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs
index e0958bfdb7..d750eb15e0 100644
--- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs
+++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs
@@ -1,7 +1,10 @@
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Install.InstallSteps;
using Umbraco.Cms.Core.Install.Models;
+using Umbraco.Cms.Core.Telemetry;
using Umbraco.Cms.Infrastructure.Install;
using Umbraco.Cms.Infrastructure.Install.InstallSteps;
using Umbraco.Extensions;
@@ -19,7 +22,12 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection
builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
- builder.Services.AddScoped();
+ builder.Services.AddScoped(provider =>
+ {
+ return new TelemetryIdentifierStep(
+ provider.GetRequiredService>(),
+ provider.GetRequiredService());
+ });
builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs
index 511c09304d..734fcb5661 100644
--- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs
+++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs
@@ -48,6 +48,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection
builder.Services.AddUnique();
builder.Services.AddUnique();
builder.Services.AddUnique();
+ builder.Services.AddUnique();
builder.Services.AddUnique();
builder.Services.AddUnique();
builder.Services.AddUnique();
diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs
index 80d4dd9b3f..7c72b1ddce 100644
--- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs
+++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs
@@ -42,10 +42,6 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection
builder.Services.AddUnique();
builder.Services.AddUnique();
builder.Services.AddUnique();
- builder.Services.AddUnique();
- builder.Services.AddUnique();
- builder.Services.AddUnique();
- builder.Services.AddUnique();
builder.Services.AddUnique();
builder.Services.AddTransient(SourcesFactory);
builder.Services.AddUnique(factory => CreatePackageRepository(factory, "createdPackages.config"));
@@ -84,7 +80,8 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection
var pluginLangFolders = appPlugins.Exists == false
? Enumerable.Empty()
: appPlugins.GetDirectories()
- .SelectMany(x => x.GetDirectories("Lang", SearchOption.AllDirectories))
+ // Check for both Lang & lang to support case sensitive file systems.
+ .SelectMany(x => x.GetDirectories("?ang", SearchOption.AllDirectories).Where(x => x.Name.InvariantEquals("lang")))
.SelectMany(x => x.GetFiles("*.xml", SearchOption.TopDirectoryOnly))
.Select(x => new LocalizedTextServiceSupplementaryFileSource(x, false));
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/Events/RelateOnTrashNotificationHandler.cs b/src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs
index 4222ac800e..b06248c79e 100644
--- a/src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs
+++ b/src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs
@@ -65,7 +65,7 @@ namespace Umbraco.Cms.Core.Events
var documentObjectType = Constants.ObjectTypes.Document;
const string relationTypeName = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteName;
- relationType = new RelationType(relationTypeName, relationTypeAlias, false, documentObjectType, documentObjectType);
+ relationType = new RelationType(relationTypeName, relationTypeAlias, false, documentObjectType, documentObjectType, false);
_relationService.Save(relationType);
}
@@ -123,7 +123,7 @@ namespace Umbraco.Cms.Core.Events
{
var documentObjectType = Constants.ObjectTypes.Document;
const string relationTypeName = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName;
- relationType = new RelationType(relationTypeName, relationTypeAlias, false, documentObjectType, documentObjectType);
+ relationType = new RelationType(relationTypeName, relationTypeAlias, false, documentObjectType, documentObjectType, false);
_relationService.Save(relationType);
}
diff --git a/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs b/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs
index 9f0a82eb78..550cb56674 100644
--- a/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs
+++ b/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs
@@ -7,6 +7,7 @@ using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Logging;
@@ -101,7 +102,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
try
{
var request = new HttpRequestMessage(HttpMethod.Get, keepAlivePingUrl);
- HttpClient httpClient = _httpClientFactory.CreateClient();
+ HttpClient httpClient = _httpClientFactory.CreateClient(Constants.HttpClients.IgnoreCertificateErrors);
_ = await httpClient.SendAsync(request);
}
catch (Exception ex)
diff --git a/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs b/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs
index ad21c2982b..34b13c6e5a 100644
--- a/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs
+++ b/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs
@@ -24,6 +24,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
private TimeSpan _period;
private readonly TimeSpan _delay;
private Timer? _timer;
+ private bool _disposedValue;
///
/// Initializes a new instance of the class.
@@ -84,7 +85,24 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
return Task.CompletedTask;
}
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposedValue)
+ {
+ if (disposing)
+ {
+ _timer?.Dispose();
+ }
+
+ _disposedValue = true;
+ }
+ }
+
///
- public void Dispose() => _timer?.Dispose();
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
}
}
diff --git a/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs b/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs
index 753dc542b7..431409a240 100644
--- a/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs
+++ b/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs
@@ -59,9 +59,6 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
// Send data to LIVE telemetry
s_httpClient.BaseAddress = new Uri("https://telemetry.umbraco.com/");
- // Set a low timeout - no need to use a larger default timeout for this POST request
- s_httpClient.Timeout = new TimeSpan(0, 0, 1);
-
#if DEBUG
// Send data to DEBUG telemetry service
s_httpClient.BaseAddress = new Uri("https://telemetry.rainbowsrock.net/");
diff --git a/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs b/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs
index d806bab5ad..7b8c87ceaa 100644
--- a/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs
+++ b/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs
@@ -5,13 +5,15 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Runtime;
-using Umbraco.Cms.Core.Security;
+using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Sync;
using Umbraco.Cms.Core.Web;
+using Umbraco.Cms.Web.Common.DependencyInjection;
namespace Umbraco.Cms.Infrastructure.HostedServices
{
@@ -27,20 +29,13 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
private readonly IMainDom _mainDom;
private readonly IRuntimeState _runtimeState;
private readonly IServerMessenger _serverMessenger;
+ private readonly IScopeProvider _scopeProvider;
private readonly IServerRoleAccessor _serverRegistrar;
private readonly IUmbracoContextFactory _umbracoContextFactory;
- ///
+ ///
/// Initializes a new instance of the class.
///
- /// Representation of the state of the Umbraco runtime.
- /// Representation of the main application domain.
- /// Provider of server registrations to the distributed cache.
- /// Service for handling content operations.
- /// Service for creating and managing Umbraco context.
- /// The typed logger.
- /// Service broadcasting cache notifications to registered servers.
- /// Creates and manages instances.
public ScheduledPublishing(
IRuntimeState runtimeState,
IMainDom mainDom,
@@ -48,7 +43,8 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
IContentService contentService,
IUmbracoContextFactory umbracoContextFactory,
ILogger logger,
- IServerMessenger serverMessenger)
+ IServerMessenger serverMessenger,
+ IScopeProvider scopeProvider)
: base(TimeSpan.FromMinutes(1), DefaultDelay)
{
_runtimeState = runtimeState;
@@ -58,6 +54,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
_umbracoContextFactory = umbracoContextFactory;
_logger = logger;
_serverMessenger = serverMessenger;
+ _scopeProvider = scopeProvider;
}
public override Task PerformExecuteAsync(object? state)
@@ -93,8 +90,6 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
try
{
- // We don't need an explicit scope here because PerformScheduledPublish creates it's own scope
- // so it's safe as it will create it's own ambient scope.
// Ensure we run with an UmbracoContext, because this will run in a background task,
// and developers may be using the UmbracoContext in the event handlers.
@@ -105,6 +100,14 @@ namespace Umbraco.Cms.Infrastructure.HostedServices
// - and we should definitively *not* have to flush it here (should be auto)
using UmbracoContextReference contextReference = _umbracoContextFactory.EnsureUmbracoContext();
+ using IScope scope = _scopeProvider.CreateScope(autoComplete: true);
+
+ /* We used to assume that there will never be two instances running concurrently where (IsMainDom && ServerRole == SchedulingPublisher)
+ * However this is possible during an azure deployment slot swap for the SchedulingPublisher instance when trying to achieve zero downtime deployments.
+ * If we take a distributed write lock, we are certain that the multiple instances of the job will not run in parallel.
+ * It's possible that during the swapping process we may run this job more frequently than intended but this is not of great concern and it's
+ * only until the old SchedulingPublisher shuts down. */
+ scope.EagerWriteLock(Constants.Locks.ScheduledPublishing);
try
{
// Run
diff --git a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs
index 54214ba08c..77a7e0226f 100644
--- a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs
+++ b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs
@@ -20,6 +20,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration
private readonly IRuntimeState _runtimeState;
private readonly IServerMessenger _messenger;
private readonly ILogger _logger;
+ private bool _disposedValue;
///
/// Initializes a new instance of the class.
@@ -54,5 +55,20 @@ namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration
return Task.CompletedTask;
}
+
+ protected override void Dispose(bool disposing)
+ {
+ if (!_disposedValue)
+ {
+ if (disposing && _messenger is IDisposable disposable)
+ {
+ disposable.Dispose();
+ }
+
+ _disposedValue = true;
+ }
+
+ base.Dispose(disposing);
+ }
}
}
diff --git a/src/Umbraco.Infrastructure/IPublishedContentQuery.cs b/src/Umbraco.Infrastructure/IPublishedContentQuery.cs
index d5f41c0a02..b244cb93de 100644
--- a/src/Umbraco.Infrastructure/IPublishedContentQuery.cs
+++ b/src/Umbraco.Infrastructure/IPublishedContentQuery.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Xml.XPath;
using Examine.Search;
@@ -8,31 +8,46 @@ using Umbraco.Cms.Core.Xml;
namespace Umbraco.Cms.Core
{
///
- /// Query methods used for accessing strongly typed content in templates
+ /// Query methods used for accessing strongly typed content in templates.
///
public interface IPublishedContentQuery
{
IPublishedContent? Content(int id);
+
IPublishedContent Content(Guid id);
+
IPublishedContent? Content(Udi id);
+
IPublishedContent? Content(object id);
+
IPublishedContent ContentSingleAtXPath(string xpath, params XPathVariable[] vars);
+
IEnumerable Content(IEnumerable ids);
+
IEnumerable Content(IEnumerable ids);
IEnumerable Content(IEnumerable ids);
+
IEnumerable ContentAtXPath(string xpath, params XPathVariable[] vars);
+
IEnumerable ContentAtXPath(XPathExpression xpath, params XPathVariable[] vars);
+
IEnumerable ContentAtRoot();
IPublishedContent? Media(int id);
+
IPublishedContent Media(Guid id);
+
IPublishedContent? Media(Udi id);
IPublishedContent? Media(object id);
+
IEnumerable Media(IEnumerable ids);
+
IEnumerable Media(IEnumerable ids);
+
IEnumerable Media(IEnumerable ids);
+
IEnumerable MediaAtRoot();
///
@@ -44,7 +59,7 @@ namespace Umbraco.Cms.Core
/// The total amount of records.
/// The culture (defaults to a culture insensitive search).
/// The name of the index to search (defaults to ).
- /// The fields to load in the results of the search (defaults to all fields loaded).
+ /// This parameter is no longer used, because the results are loaded from the published snapshot using the single item ID field.
///
/// The search results.
///
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 d211253ae2..44879f31b8 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 c0e272a6aa..25494ff925 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 11372c89ae..38ac000452 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/Logging/Viewer/ExpressionFilter.cs b/src/Umbraco.Infrastructure/Logging/Viewer/ExpressionFilter.cs
index ddf18a12ce..70c05dc2a4 100644
--- a/src/Umbraco.Infrastructure/Logging/Viewer/ExpressionFilter.cs
+++ b/src/Umbraco.Infrastructure/Logging/Viewer/ExpressionFilter.cs
@@ -1,7 +1,8 @@
-using System;
+using System;
using System.Linq;
using Serilog.Events;
-using Serilog.Filters.Expressions;
+using Serilog.Expressions;
+using Umbraco.Cms.Infrastructure.Logging.Viewer;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Logging.Viewer
@@ -10,32 +11,43 @@ namespace Umbraco.Cms.Core.Logging.Viewer
internal class ExpressionFilter : ILogFilter
{
private readonly Func? _filter;
- private const string ExpressionOperators = "()+=*<>%-";
+ private const string s_expressionOperators = "()+=*<>%-";
public ExpressionFilter(string? filterExpression)
{
Func? filter;
+ // Our custom Serilog Functions to extend Serilog.Expressions
+ // In this case we are plugging the gap for the missing Has()
+ // function from porting away from Serilog.Filters.Expressions to Serilog.Expressions
+ // Along with patching support for the more verbose built in property names
+ var customSerilogFunctions = new SerilogLegacyNameResolver(typeof(SerilogExpressionsFunctions));
+
if (string.IsNullOrEmpty(filterExpression))
{
return;
}
// If the expression is one word and doesn't contain a serilog operator then we can perform a like search
- if (!filterExpression.Contains(" ") && !filterExpression.ContainsAny(ExpressionOperators.Select(c => c)))
+ if (!filterExpression.Contains(" ") && !filterExpression.ContainsAny(s_expressionOperators.Select(c => c)))
{
filter = PerformMessageLikeFilter(filterExpression);
}
else // check if it's a valid expression
{
// If the expression evaluates then make it into a filter
- if (FilterLanguage.TryCreateFilter(filterExpression, out var eval, out _))
+ if (SerilogExpression.TryCompile(filterExpression, null, customSerilogFunctions, out CompiledExpression compiled, out var error))
{
- filter = evt => true.Equals(eval(evt));
+ filter = evt =>
+ {
+ LogEventPropertyValue result = compiled(evt);
+ return ExpressionResult.IsTrue(result);
+ };
}
else
{
- //Assume the expression was a search string and make a Like filter from that
+ // 'error' describes a syntax error, where it was unable to compile an expression
+ // Assume the expression was a search string and make a Like filter from that
filter = PerformMessageLikeFilter(filterExpression);
}
}
@@ -50,10 +62,15 @@ namespace Umbraco.Cms.Core.Logging.Viewer
private Func? PerformMessageLikeFilter(string filterExpression)
{
- var filterSearch = $"@Message like '%{FilterLanguage.EscapeLikeExpressionContent(filterExpression)}%'";
- if (FilterLanguage.TryCreateFilter(filterSearch, out var eval, out _))
+ var filterSearch = $"@Message like '%{SerilogExpression.EscapeLikeExpressionContent(filterExpression)}%'";
+ if (SerilogExpression.TryCompile(filterSearch, out CompiledExpression compiled, out var error))
{
- return evt => true.Equals(eval(evt));
+ // `compiled` is a function that can be executed against `LogEvent`s:
+ return evt =>
+ {
+ LogEventPropertyValue result = compiled(evt);
+ return ExpressionResult.IsTrue(result);
+ };
}
return null;
diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogExpressionsFunctions.cs b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogExpressionsFunctions.cs
new file mode 100644
index 0000000000..92b16b9729
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogExpressionsFunctions.cs
@@ -0,0 +1,14 @@
+using Serilog.Events;
+
+namespace Umbraco.Cms.Infrastructure.Logging.Viewer
+{
+ public class SerilogExpressionsFunctions
+ {
+ // This Has() code is the same as the renamed IsDefined() function
+ // Added this to help backport and ensure saved queries continue to work if using Has()
+ public static LogEventPropertyValue? Has(LogEventPropertyValue? value)
+ {
+ return new ScalarValue(value != null);
+ }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLegacyNameResolver.cs b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLegacyNameResolver.cs
new file mode 100644
index 0000000000..0472a8ea16
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLegacyNameResolver.cs
@@ -0,0 +1,38 @@
+using System;
+using Serilog.Expressions;
+
+namespace Umbraco.Cms.Infrastructure.Logging.Viewer
+{
+ ///
+ /// Inherits Serilog's StaticMemberNameResolver to ensure we get same functionality
+ /// Of easily allowing any static methods definied in the passed in class/type
+ /// To extend as functions to use for filtering logs such as Has() and any other custom ones
+ ///
+ public class SerilogLegacyNameResolver : StaticMemberNameResolver
+ {
+ public SerilogLegacyNameResolver(Type type) : base(type)
+ {
+ }
+
+ ///
+ /// Allows us to fix the gap from migrating away from Serilog.Filters.Expressions
+ /// So we can still support the more verbose built in property names such as
+ /// Exception, Level, MessageTemplate etc
+ ///
+ public override bool TryResolveBuiltInPropertyName(string alias, out string target)
+ {
+ target = alias switch
+ {
+ "Exception" => "x",
+ "Level" => "l",
+ "Message" => "m",
+ "MessageTemplate" => "mt",
+ "Properties" => "p",
+ "Timestamp" => "t",
+ _ => null
+ };
+
+ return target != null;
+ }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableOfDtoBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableOfDtoBuilder.cs
index f0362e42a3..5aac9e90f7 100644
--- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableOfDtoBuilder.cs
+++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableOfDtoBuilder.cs
@@ -33,15 +33,8 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table
}
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 c7411b3b69..09c6e04fe1 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 30c881511d..cf06d38672 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;
@@ -83,32 +90,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 +131,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 +192,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 fc6f669f7d..fb4ac3ec7e 100644
--- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs
+++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs
@@ -79,7 +79,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()
@@ -173,6 +173,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install
_database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.Domains, Name = "Domains" });
_database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.KeyValues, Name = "KeyValues" });
_database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.Languages, Name = "Languages" });
+ _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.ScheduledPublishing, Name = "ScheduledPublishing" });
_database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.MainDom, Name = "MainDom" });
}
@@ -418,21 +419,21 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install
private void CreateRelationTypeData()
{
- var relationType = new RelationTypeDto { Id = 1, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias, ChildObjectType = Cms.Core.Constants.ObjectTypes.Document, ParentObjectType = Cms.Core.Constants.ObjectTypes.Document, Dual = true, Name = Cms.Core.Constants.Conventions.RelationTypes.RelateDocumentOnCopyName };
+ var relationType = new RelationTypeDto { Id = 1, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias, ChildObjectType = Cms.Core.Constants.ObjectTypes.Document, ParentObjectType = Cms.Core.Constants.ObjectTypes.Document, Dual = true, Name = Cms.Core.Constants.Conventions.RelationTypes.RelateDocumentOnCopyName, IsDependency = false};
relationType.UniqueId = CreateUniqueRelationTypeId(relationType.Alias, relationType.Name);
_database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType);
- relationType = new RelationTypeDto { Id = 2, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias, ChildObjectType = Cms.Core.Constants.ObjectTypes.Document, ParentObjectType = Cms.Core.Constants.ObjectTypes.Document, Dual = false, Name = Cms.Core.Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteName };
+ relationType = new RelationTypeDto { Id = 2, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias, ChildObjectType = Cms.Core.Constants.ObjectTypes.Document, ParentObjectType = Cms.Core.Constants.ObjectTypes.Document, Dual = false, Name = Cms.Core.Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteName, IsDependency = false };
relationType.UniqueId = CreateUniqueRelationTypeId(relationType.Alias, relationType.Name);
_database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType);
- relationType = new RelationTypeDto { Id = 3, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias, ChildObjectType = Cms.Core.Constants.ObjectTypes.Media, ParentObjectType = Cms.Core.Constants.ObjectTypes.Media, Dual = false, Name = Cms.Core.Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName };
+ relationType = new RelationTypeDto { Id = 3, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias, ChildObjectType = Cms.Core.Constants.ObjectTypes.Media, ParentObjectType = Cms.Core.Constants.ObjectTypes.Media, Dual = false, Name = Cms.Core.Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName, IsDependency = false };
relationType.UniqueId = CreateUniqueRelationTypeId(relationType.Alias, relationType.Name);
_database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType);
- relationType = new RelationTypeDto { Id = 4, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelatedMediaAlias, ChildObjectType = null, ParentObjectType = null, Dual = false, Name = Cms.Core.Constants.Conventions.RelationTypes.RelatedMediaName };
+ relationType = new RelationTypeDto { Id = 4, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelatedMediaAlias, ChildObjectType = null, ParentObjectType = null, Dual = false, Name = Cms.Core.Constants.Conventions.RelationTypes.RelatedMediaName, IsDependency = true };
relationType.UniqueId = CreateUniqueRelationTypeId(relationType.Alias, relationType.Name);
_database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType);
- relationType = new RelationTypeDto { Id = 5, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelatedDocumentAlias, ChildObjectType = null, ParentObjectType = null, Dual = false, Name = Cms.Core.Constants.Conventions.RelationTypes.RelatedDocumentName };
+ relationType = new RelationTypeDto { Id = 5, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelatedDocumentAlias, ChildObjectType = null, ParentObjectType = null, Dual = false, Name = Cms.Core.Constants.Conventions.RelationTypes.RelatedDocumentName, IsDependency = true };
relationType.UniqueId = CreateUniqueRelationTypeId(relationType.Alias, relationType.Name);
_database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType);
}
diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs
index 3058fbfdf0..6134d45803 100644
--- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs
+++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs
@@ -90,8 +90,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));
@@ -154,8 +158,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);
@@ -458,12 +461,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 (string.IsNullOrEmpty(tableName))
{
@@ -485,18 +482,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 "));
}
@@ -510,20 +500,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 1154d6151e..22f7771685 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 e89b91690f..4838467197 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/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs
index f2c5662a74..34077943c8 100644
--- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs
+++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs
@@ -17,6 +17,7 @@ using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0;
using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_1_0;
using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_2_0;
using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_3_0;
+using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_4_0;
using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade
@@ -243,6 +244,9 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade
// to 8.17.0
To("{153865E9-7332-4C2A-9F9D-F20AEE078EC7}");
+ // Hack to support migration from 8.18
+ To("{03482BB0-CF13-475C-845E-ECB8319DBE3C}");
+
// This should be safe to execute again. We need it with a new name to ensure updates from all the following has executed this step.
// - 8.15.0 RC - Current state: {4695D0C9-0729-4976-985B-048D503665D8}
// - 8.15.0 Final - Current state: {5C424554-A32D-4852-8ED1-A13508187901}
@@ -278,6 +282,9 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade
To("{CA7A1D9D-C9D4-4914-BC0A-459E7B9C3C8C}");
To("{0828F206-DCF7-4F73-ABBB-6792275532EB}");
+ // TO 9.4.0
+ To("{DBBA1EA0-25A1-4863-90FB-5D306FB6F1E1}");
+ To("{DED98755-4059-41BB-ADBD-3FEAB12D1D7B}");
}
}
}
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_0_0/ExternalLoginTableIndexes.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexes.cs
index f350ed633c..db7f17eee3 100644
--- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexes.cs
+++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexes.cs
@@ -1,6 +1,10 @@
+using System;
using System.Collections.Generic;
using System.Linq;
+using NPoco;
using Umbraco.Cms.Core;
+using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations;
+using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0
@@ -20,14 +24,14 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0
{
// Before adding these indexes we need to remove duplicate data.
// Get all logins by latest
- var logins = Database.Fetch()
+ var logins = Database.Fetch()
.OrderByDescending(x => x.CreateDate)
.ToList();
var toDelete = new List();
// used to track duplicates so they can be removed
var keys = new HashSet<(string, string)>();
- foreach(ExternalLoginDto login in logins)
+ foreach(ExternalLoginTokenTable.LegacyExternalLoginDto login in logins)
{
if (!keys.Add((login.ProviderKey, login.LoginProvider)))
{
@@ -37,16 +41,16 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0
}
if (toDelete.Count > 0)
{
- Database.DeleteMany().Where(x => toDelete.Contains(x.Id)).Execute();
- }
+ Database.DeleteMany().Where(x => toDelete.Contains(x.Id)).Execute();
+ }
- var indexName1 = "IX_" + ExternalLoginDto.TableName + "_LoginProvider";
+ var indexName1 = "IX_" + ExternalLoginTokenTable.LegacyExternalLoginDto.TableName + "_LoginProvider";
if (!IndexExists(indexName1))
{
Create
.Index(indexName1)
- .OnTable(ExternalLoginDto.TableName)
+ .OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName)
.OnColumn("loginProvider")
.Ascending()
.WithOptions()
@@ -56,13 +60,13 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0
.Do();
}
- var indexName2 = "IX_" + ExternalLoginDto.TableName + "_ProviderKey";
+ var indexName2 = "IX_" + ExternalLoginTokenTable.LegacyExternalLoginDto.TableName + "_ProviderKey";
if (!IndexExists(indexName2))
{
Create
.Index(indexName2)
- .OnTable(ExternalLoginDto.TableName)
+ .OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName)
.OnColumn("loginProvider").Ascending()
.OnColumn("providerKey").Ascending()
.WithOptions()
@@ -70,5 +74,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0
.Do();
}
}
+
+
}
}
diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexesFixup.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexesFixup.cs
index 5efb914eb7..2c77b301ce 100644
--- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexesFixup.cs
+++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexesFixup.cs
@@ -14,29 +14,29 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0
protected override void Migrate()
{
- var indexName1 = "IX_" + ExternalLoginDto.TableName + "_LoginProvider";
- var indexName2 = "IX_" + ExternalLoginDto.TableName + "_ProviderKey";
+ var indexName1 = "IX_" + ExternalLoginTokenTable.LegacyExternalLoginDto.TableName + "_LoginProvider";
+ var indexName2 = "IX_" + ExternalLoginTokenTable.LegacyExternalLoginDto.TableName + "_ProviderKey";
if (IndexExists(indexName1))
{
// drop it since the previous migration index was wrong, and we
// need to modify a column that belons to it
- Delete.Index(indexName1).OnTable(ExternalLoginDto.TableName).Do();
+ Delete.Index(indexName1).OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName).Do();
}
if (IndexExists(indexName2))
{
// drop since it's using a column we're about to modify
- Delete.Index(indexName2).OnTable(ExternalLoginDto.TableName).Do();
+ Delete.Index(indexName2).OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName).Do();
}
// then fixup the length of the loginProvider column
- AlterColumn(ExternalLoginDto.TableName, "loginProvider");
+ AlterColumn(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName, "loginProvider");
// create it with the correct definition
Create
.Index(indexName1)
- .OnTable(ExternalLoginDto.TableName)
+ .OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName)
.OnColumn("loginProvider").Ascending()
.OnColumn("userId").Ascending()
.WithOptions()
@@ -48,9 +48,9 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0
// re-create the original
Create
.Index(indexName2)
- .OnTable(ExternalLoginDto.TableName)
+ .OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName)
.OnColumn("loginProvider").Ascending()
- .OnColumn("providerKey").Ascending()
+ .OnColumn("providerKey").Ascending()
.WithOptions()
.NonClustered()
.Do();
diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTokenTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTokenTable.cs
index 8dd43f1834..851d986c7c 100644
--- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTokenTable.cs
+++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTokenTable.cs
@@ -1,10 +1,14 @@
+using System;
using System.Collections.Generic;
+using NPoco;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations;
+using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0
{
-
public class ExternalLoginTokenTable : MigrationBase
{
public ExternalLoginTokenTable(IMigrationContext context)
@@ -13,7 +17,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0
}
///
- /// Adds new External Login token table
+ /// Adds new External Login token table
///
protected override void Migrate()
{
@@ -25,5 +29,53 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0
Create.Table().Do();
}
+
+ [TableName(TableName)]
+ [ExplicitColumns]
+ [PrimaryKey("Id")]
+ internal class LegacyExternalLoginDto
+ {
+ public const string TableName = Constants.DatabaseSchema.Tables.ExternalLogin;
+
+ [Column("id")] [PrimaryKeyColumn] public int Id { get; set; }
+
+ [Obsolete(
+ "This only exists to ensure you can upgrade using external logins from umbraco version where this was used to the new where it is not used")]
+ [Column("userId")]
+ public int? UserId { get; set; }
+
+
+ ///
+ /// Used to store the name of the provider (i.e. Facebook, Google)
+ ///
+ [Column("loginProvider")]
+ [Length(400)]
+ [NullSetting(NullSetting = NullSettings.NotNull)]
+ [Index(IndexTypes.UniqueNonClustered, ForColumns = "loginProvider,userOrMemberKey",
+ Name = "IX_" + TableName + "_LoginProvider")]
+ public string LoginProvider { get; set; }
+
+ ///
+ /// Stores the key the provider uses to lookup the login
+ ///
+ [Column("providerKey")]
+ [Length(4000)]
+ [NullSetting(NullSetting = NullSettings.NotNull)]
+ [Index(IndexTypes.NonClustered, ForColumns = "loginProvider,providerKey",
+ Name = "IX_" + TableName + "_ProviderKey")]
+ public string ProviderKey { get; set; }
+
+ [Column("createDate")]
+ [Constraint(Default = SystemMethods.CurrentDateTime)]
+ public DateTime CreateDate { get; set; }
+
+ ///
+ /// Used to store any arbitrary data for the user and external provider - like user tokens returned from the provider
+ ///
+ [Column("userData")]
+ [NullSetting(NullSetting = NullSettings.Null)]
+ [SpecialDbType(SpecialDbTypes.NTEXT)]
+ public string UserData { get; set; }
+ }
}
}
diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_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/Migrations/Upgrade/V_9_4_0/AddScheduledPublishingLock.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/AddScheduledPublishingLock.cs
new file mode 100644
index 0000000000..01cfb22a3d
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/AddScheduledPublishingLock.cs
@@ -0,0 +1,15 @@
+using Umbraco.Cms.Infrastructure.Persistence.Dtos;
+
+namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_4_0
+{
+ internal class AddScheduledPublishingLock : MigrationBase
+ {
+ public AddScheduledPublishingLock(IMigrationContext context)
+ : base(context)
+ {
+ }
+
+ protected override void Migrate() =>
+ Database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.ScheduledPublishing, Name = "ScheduledPublishing" });
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/UpdateRelationTypesToHandleDependencies.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/UpdateRelationTypesToHandleDependencies.cs
new file mode 100644
index 0000000000..1c8fe7ed72
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/UpdateRelationTypesToHandleDependencies.cs
@@ -0,0 +1,34 @@
+using System.Linq;
+using Umbraco.Cms.Infrastructure.Persistence.Dtos;
+using Umbraco.Extensions;
+
+
+namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_4_0
+{
+ internal class UpdateRelationTypesToHandleDependencies : MigrationBase
+ {
+ public UpdateRelationTypesToHandleDependencies(IMigrationContext context)
+ : base(context)
+ {
+ }
+
+ protected override void Migrate()
+ {
+ var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList();
+
+ AddColumnIfNotExists(columns, "isDependency");
+
+ var aliasesWithDependencies = new[]
+ {
+ Core.Constants.Conventions.RelationTypes.RelatedDocumentAlias,
+ Core.Constants.Conventions.RelationTypes.RelatedMediaAlias
+ };
+
+ Database.Execute(
+ Sql()
+ .Update(u => u.Set(x => x.IsDependency, true))
+ .WhereIn(x => x.Alias, aliasesWithDependencies));
+
+ }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/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 a2507a17f2..9e26c2722a 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 f3f0902456..59e8e9c386 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/Dtos/RelationTypeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/RelationTypeDto.cs
index 28cbcaed2d..d1cb5cc278 100644
--- a/src/Umbraco.Infrastructure/Persistence/Dtos/RelationTypeDto.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Dtos/RelationTypeDto.cs
@@ -39,6 +39,10 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Dtos
[NullSetting(NullSetting = NullSettings.NotNull)]
[Length(100)]
[Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_alias")]
- public string Alias { get; set; } = null!;
+ public string Alias { get; set; } = null!;
+
+ [Constraint(Default = "0")]
+ [Column("isDependency")]
+ public bool IsDependency { get; set; }
}
}
diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/RelationTypeFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/RelationTypeFactory.cs
index 0e304f1fcb..57b1831c9d 100644
--- a/src/Umbraco.Infrastructure/Persistence/Factories/RelationTypeFactory.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Factories/RelationTypeFactory.cs
@@ -9,7 +9,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories
public static IRelationType BuildEntity(RelationTypeDto dto)
{
- var entity = new RelationType(dto.Name, dto.Alias, dto.Dual, dto.ParentObjectType, dto.ChildObjectType);
+ var entity = new RelationType(dto.Name, dto.Alias, dto.Dual, dto.ParentObjectType, dto.ChildObjectType, dto.IsDependency);
try
{
@@ -30,11 +30,17 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories
public static RelationTypeDto BuildDto(IRelationType entity)
{
+ var isDependency = false;
+ if (entity is IRelationTypeWithIsDependency relationTypeWithIsDependency)
+ {
+ isDependency = relationTypeWithIsDependency.IsDependency;
+ }
var dto = new RelationTypeDto
{
Alias = entity.Alias,
ChildObjectType = entity.ChildObjectType,
Dual = entity.IsBidirectional,
+ IsDependency = isDependency,
Name = entity.Name ?? string.Empty,
ParentObjectType = entity.ParentObjectType,
UniqueId = entity.Key
@@ -47,6 +53,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories
return dto;
}
+
+
#endregion
}
}
diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryDbConnection.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryDbConnection.cs
index bd03c44c7a..57b14bf0a9 100644
--- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryDbConnection.cs
+++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryDbConnection.cs
@@ -6,7 +6,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 52daf7e351..f9bdf00f8f 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 382eda81b0..43d7c461ca 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/Mappers/RelationTypeMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/RelationTypeMapper.cs
index 965a659631..732563fef7 100644
--- a/src/Umbraco.Infrastructure/Persistence/Mappers/RelationTypeMapper.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Mappers/RelationTypeMapper.cs
@@ -22,6 +22,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Mappers
DefineMap(nameof(RelationType.Alias), nameof(RelationTypeDto.Alias));
DefineMap(nameof(RelationType.ChildObjectType), nameof(RelationTypeDto.ChildObjectType));
DefineMap(nameof(RelationType.IsBidirectional), nameof(RelationTypeDto.Dual));
+ DefineMap(nameof(RelationType.IsDependency), nameof(RelationTypeDto.IsDependency));
DefineMap(nameof(RelationType.Name), nameof(RelationTypeDto.Name));
DefineMap(nameof(RelationType.ParentObjectType), nameof(RelationTypeDto.ParentObjectType));
}
diff --git a/src/Umbraco.Infrastructure/Persistence/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 7c4498292b..2d2df7fe4d 100644
--- a/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs
+++ b/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs
@@ -72,7 +72,27 @@ namespace Umbraco.Extensions
/// The Sql statement.
public static Sql WhereIn(this Sql sql, Expression> field, Sql? values)
{
- return sql.WhereIn(field, values, false);
+ return WhereIn(sql, field, values, false, null);
+ }
+
+ public static Sql WhereIn(this Sql sql, Expression> field, Sql values, string tableAlias)
+ {
+ return sql.WhereIn(field, values, false, tableAlias);
+ }
+
+
+ public static Sql WhereLike(this Sql sql, Expression> fieldSelector, Sql valuesSql)
+ {
+ var fieldName = sql.SqlContext.SqlSyntax.GetFieldName(fieldSelector);
+ sql.Where(fieldName + " LIKE (" + valuesSql.SQL + ")", valuesSql.Arguments);
+ return sql;
+ }
+
+ public static Sql WhereLike(this Sql sql, Expression> fieldSelector, string likeValue)
+ {
+ var fieldName = sql.SqlContext.SqlSyntax.GetFieldName(fieldSelector);
+ sql.Where(fieldName + " LIKE ('" + likeValue + "')");
+ return sql;
}
///
@@ -130,7 +150,12 @@ namespace Umbraco.Extensions
private static Sql WhereIn(this Sql sql, Expression> fieldSelector, Sql? valuesSql, bool not)
{
- var fieldName = sql.SqlContext.SqlSyntax.GetFieldName(fieldSelector);
+ return WhereIn(sql, fieldSelector, valuesSql, not, null);
+ }
+
+ private static Sql WhereIn(this Sql sql, Expression> fieldSelector, Sql? valuesSql, bool not, string tableAlias)
+ {
+ var fieldName = sql.SqlContext.SqlSyntax.GetFieldName(fieldSelector, tableAlias);
sql.Where(fieldName + (not ? " NOT" : "") +" IN (" + valuesSql?.SQL + ")", valuesSql?.Arguments);
return sql;
}
@@ -252,7 +277,7 @@ namespace Umbraco.Extensions
/// The Sql statement.
public static Sql OrderByDescending(this Sql sql, Expression> field)
{
- return sql.OrderBy("(" + sql.SqlContext.SqlSyntax.GetFieldName(field) + ") DESC");
+ return sql.OrderByDescending(sql.SqlContext.SqlSyntax.GetFieldName(field));
}
///
@@ -268,7 +293,7 @@ namespace Umbraco.Extensions
var columns = fields.Length == 0
? sql.GetColumns(withAlias: false)
: fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray();
- return sql.OrderBy(columns.Select(x => x + " DESC"));
+ return sql.OrderByDescending(columns);
}
///
@@ -406,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