Merge branch 'main' into v17/dev

# Conflicts:
#	src/Umbraco.Web.UI.Client/package-lock.json
This commit is contained in:
Laura Neto
2025-08-11 11:32:52 +02:00
603 changed files with 6362 additions and 2676 deletions

View File

@@ -90,6 +90,9 @@ public static partial class UmbracoBuilderExtensions
builder.AddNotificationAsyncHandler<RuntimeUnattendedUpgradeNotification, UnattendedUpgrader>();
builder.AddNotificationAsyncHandler<RuntimePremigrationsUpgradeNotification, PremigrationUpgrader>();
// Database availability check.
builder.Services.AddUnique<IDatabaseAvailabilityCheck, DefaultDatabaseAvailabilityCheck>();
// Add runtime mode validation
builder.Services.AddSingleton<IRuntimeModeValidationService, RuntimeModeValidationService>();
builder.RuntimeModeValidators()
@@ -124,6 +127,7 @@ public static partial class UmbracoBuilderExtensions
builder.Services.AddSingleton<IJsonSerializer, SystemTextJsonSerializer>();
builder.Services.AddSingleton<IConfigurationEditorJsonSerializer, SystemTextConfigurationEditorJsonSerializer>();
builder.Services.AddUnique<IJsonSerializerEncoderFactory, DefaultJsonSerializerEncoderFactory>();
builder.Services.AddUnique<IWebhookJsonSerializer, SystemTextWebhookJsonSerializer>();
// register database builder

View File

@@ -0,0 +1,52 @@
using Microsoft.Extensions.Logging;
namespace Umbraco.Cms.Infrastructure.Persistence;
/// <summary>
/// Checks if a configured database is available on boot using the default method of 5 attempts with a 1 second delay between each one.
/// </summary>
internal class DefaultDatabaseAvailabilityCheck : IDatabaseAvailabilityCheck
{
private const int NumberOfAttempts = 5;
private const int DefaultAttemptDelayMilliseconds = 1000;
private readonly ILogger<DefaultDatabaseAvailabilityCheck> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="DefaultDatabaseAvailabilityCheck"/> class.
/// </summary>
/// <param name="logger"></param>
public DefaultDatabaseAvailabilityCheck(ILogger<DefaultDatabaseAvailabilityCheck> logger) => _logger = logger;
/// <summary>
/// Gets or sets the number of milliseconds to delay between attempts.
/// </summary>
/// <remarks>
/// Exposed for testing purposes, hence settable only internally.
/// </remarks>
public int AttemptDelayMilliseconds { get; internal set; } = DefaultAttemptDelayMilliseconds;
/// <inheritdoc/>
public bool IsDatabaseAvailable(IUmbracoDatabaseFactory databaseFactory)
{
bool canConnect;
for (var i = 0; ;)
{
canConnect = databaseFactory.CanConnect;
if (canConnect || ++i == NumberOfAttempts)
{
break;
}
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("Could not immediately connect to database, trying again.");
}
// Wait for the configured time before trying again.
Thread.Sleep(AttemptDelayMilliseconds);
}
return canConnect;
}
}

View File

@@ -4,7 +4,7 @@ using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations;
namespace Umbraco.Cms.Infrastructure.Persistence.Dtos;
[TableName(Constants.DatabaseSchema.Tables.ElementTypeTree)]
[TableName(Constants.DatabaseSchema.Tables.ContentTypeTree)]
[ExplicitColumns]
internal sealed class ContentType2ContentTypeDto
{

View File

@@ -0,0 +1,16 @@
namespace Umbraco.Cms.Infrastructure.Persistence;
/// <summary>
/// Checks if a configured database is available on boot.
/// </summary>
public interface IDatabaseAvailabilityCheck
{
/// <summary>
/// Checks if the database is available for Umbraco to boot.
/// </summary>
/// <param name="databaseFactory">The <see cref="IUmbracoDatabaseFactory"/>.</param>
/// <returns>
/// A value indicating whether the database is available.
/// </returns>
bool IsDatabaseAvailable(IUmbracoDatabaseFactory databaseFactory);
}

View File

@@ -438,7 +438,7 @@ AND umbracoNode.id <> @id",
IEnumerable<int> propertyTypeToDeleteIds = dbPropertyTypeIds.Except(entityPropertyTypes);
foreach (var propertyTypeId in propertyTypeToDeleteIds)
{
DeletePropertyType(entity.Id, propertyTypeId);
DeletePropertyType(entity, propertyTypeId);
}
}
@@ -647,7 +647,7 @@ AND umbracoNode.id <> @id",
{
foreach (var id in orphanPropertyTypeIds)
{
DeletePropertyType(entity.Id, id);
DeletePropertyType(entity, id);
}
}
@@ -1410,16 +1410,27 @@ AND umbracoNode.id <> @id",
}
}
private void DeletePropertyType(int contentTypeId, int propertyTypeId)
private void DeletePropertyType(IContentTypeComposition contentType, int propertyTypeId)
{
// first clear dependencies
// First clear dependencies.
Database.Delete<TagRelationshipDto>("WHERE propertyTypeId = @Id", new { Id = propertyTypeId });
Database.Delete<PropertyDataDto>("WHERE propertyTypeId = @Id", new { Id = propertyTypeId });
// then delete the property type
// Clear the property value permissions, which aren't a hard dependency with a foreign key, but we want to ensure
// that any for removed property types are cleared.
var uniqueIdAsString = string.Format(SqlContext.SqlSyntax.ConvertUniqueIdentifierToString, "uniqueId");
var permissionSearchString = SqlContext.SqlSyntax.GetConcat(
"(SELECT " + uniqueIdAsString + " FROM " + Constants.DatabaseSchema.Tables.PropertyType + " WHERE id = @PropertyTypeId)",
"'|%'");
Database.Delete<UserGroup2GranularPermissionDto>(
"WHERE uniqueId = @ContentTypeKey AND permission LIKE " + permissionSearchString,
new { ContentTypeKey = contentType.Key, PropertyTypeId = propertyTypeId });
// Finally delete the property type.
Database.Delete<PropertyTypeDto>(
"WHERE contentTypeId = @Id AND id = @PropertyTypeId",
new { Id = contentTypeId, PropertyTypeId = propertyTypeId });
new { contentType.Id, PropertyTypeId = propertyTypeId });
}
protected void ValidateAlias(TEntity entity)
@@ -1555,20 +1566,16 @@ WHERE {Constants.DatabaseSchema.Tables.Content}.nodeId IN (@ids) AND cmsContentT
// is included here just to be 100% sure since it has a FK on cmsPropertyType.
var list = new List<string>
{
"DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @id",
"DELETE FROM umbracoUserGroup2Permission WHERE userGroupKey IN (SELECT [umbracoUserGroup].[Key] FROM umbracoUserGroup WHERE Id = @id)",
"DELETE FROM umbracoUserGroup2GranularPermission WHERE userGroupKey IN (SELECT [umbracoUserGroup].[Key] FROM umbracoUserGroup WHERE Id = @id)",
"DELETE FROM cmsTagRelationship WHERE nodeId = @id",
"DELETE FROM cmsContentTypeAllowedContentType WHERE Id = @id",
"DELETE FROM cmsContentTypeAllowedContentType WHERE AllowedId = @id",
"DELETE FROM cmsContentType2ContentType WHERE parentContentTypeId = @id",
"DELETE FROM cmsContentType2ContentType WHERE childContentTypeId = @id",
"DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyData +
" WHERE propertyTypeId IN (SELECT id FROM cmsPropertyType WHERE contentTypeId = @id)",
"DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyType +
" WHERE contentTypeId = @id",
"DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyTypeGroup +
" WHERE contenttypeNodeId = @id",
"DELETE FROM " + Constants.DatabaseSchema.Tables.User2NodeNotify + " WHERE nodeId = @id",
"DELETE FROM " + Constants.DatabaseSchema.Tables.UserGroup2GranularPermission + " WHERE uniqueId IN (SELECT uniqueId FROM umbracoNode WHERE id = @id)",
"DELETE FROM " + Constants.DatabaseSchema.Tables.TagRelationship + " WHERE nodeId = @id",
"DELETE FROM " + Constants.DatabaseSchema.Tables.ContentChildType + " WHERE Id = @id",
"DELETE FROM " + Constants.DatabaseSchema.Tables.ContentChildType + " WHERE AllowedId = @id",
"DELETE FROM " + Constants.DatabaseSchema.Tables.ContentTypeTree + " WHERE parentContentTypeId = @id",
"DELETE FROM " + Constants.DatabaseSchema.Tables.ContentTypeTree + " WHERE childContentTypeId = @id",
"DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyData + " WHERE propertyTypeId IN (SELECT id FROM cmsPropertyType WHERE contentTypeId = @id)",
"DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyType + " WHERE contentTypeId = @id",
"DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyTypeGroup + " WHERE contenttypeNodeId = @id",
};
return list;
}

View File

@@ -149,7 +149,7 @@ internal class EntityContainerRepository : EntityRepositoryBase<int, EntityConta
{
NodeDto nodeDto = Database.FirstOrDefault<NodeDto>(Sql().SelectAll()
.From<NodeDto>()
.Where<NodeDto>(dto => dto.Text == name && dto.NodeObjectType == NodeObjectTypeId && dto.ParentId == parentId));
.Where<NodeDto>(dto => dto.Text == name && dto.NodeObjectType == NodeObjectTypeId && dto.ParentId == parentId));
return nodeDto is not null;
}

View File

@@ -95,10 +95,6 @@ internal sealed class EntityRepository : RepositoryBase, IEntityRepositoryExtend
ApplyOrdering(ref sql, ordering);
}
// TODO: we should be able to do sql = sql.OrderBy(x => Alias(x.NodeId, "NodeId")); but we can't because the OrderBy extension don't support Alias currently
// no matter what we always must have node id ordered at the end
sql = ordering.Direction == Direction.Ascending ? sql.OrderBy("NodeId") : sql.OrderByDescending("NodeId");
// for content we must query for ContentEntityDto entities to produce the correct culture variant entity names
var pageIndexToFetch = pageIndex + 1;
IEnumerable<BaseDto> dtos;
@@ -146,7 +142,15 @@ internal sealed class EntityRepository : RepositoryBase, IEntityRepositoryExtend
}
/// <inheritdoc/>
public IEnumerable<IEntitySlim> GetSiblings(Guid objectType, Guid targetKey, int before, int after, Ordering ordering)
public IEnumerable<IEntitySlim> GetSiblings(
ISet<Guid> objectTypes,
Guid targetKey,
int before,
int after,
IQuery<IUmbracoEntity>? filter,
Ordering ordering,
out long totalBefore,
out long totalAfter)
{
// Ideally we don't want to have to do a second query for the parent ID, but the siblings query is already messy enough
// without us also having to do a nested query for the parent ID too.
@@ -159,13 +163,28 @@ internal sealed class EntityRepository : RepositoryBase, IEntityRepositoryExtend
Sql<ISqlContext> orderingSql = Sql();
ApplyOrdering(ref orderingSql, ordering);
// Get all children of the parent node which is not trashed, ordered by SortOrder, and assign each a row number.
// Get all children of the parent node which are not trashed and match the provided object types.
// Order by SortOrder, and assign each a row number.
// These row numbers are important, we need them to select the "before" and "after" siblings of the target node.
Sql<ISqlContext> rowNumberSql = Sql()
.Select($"ROW_NUMBER() OVER ({orderingSql.SQL}) AS rn")
.AndSelect<NodeDto>(n => n.UniqueId)
.From<NodeDto>()
.Where<NodeDto>(x => x.ParentId == parentId && x.Trashed == false);
.Where<NodeDto>(x => x.ParentId == parentId && x.Trashed == false)
.WhereIn<NodeDto>(x => x.NodeObjectType, objectTypes);
// Apply the filter if provided.
if (filter != null)
{
foreach (Tuple<string, object[]> filterClause in filter.GetWhereClauses())
{
rowNumberSql.Where(filterClause.Item1, filterClause.Item2);
}
}
// By applying additional where clauses with parameters containing an unknown number of elements, the position of the parameters in
// the final query for before and after positions will increase. So we need to calculate the offset based on the provided values.
int beforeAfterParameterIndexOffset = GetBeforeAfterParameterOffset(objectTypes, filter);
// Find the specific row number of the target node.
// We need this to determine the bounds of the row numbers to select.
@@ -180,21 +199,66 @@ internal sealed class EntityRepository : RepositoryBase, IEntityRepositoryExtend
IEnumerable<object> afterArguments = targetRowSql.Arguments.Concat([after]);
// Select the UniqueId of nodes which row number is within the specified range of the target node's row number.
const int BeforeAfterParameterIndex = 3;
var beforeAfterParameterIndex = BeforeAfterParameterIndex + beforeAfterParameterIndexOffset;
var beforeArgumentsArray = beforeArguments.ToArray();
var afterArgumentsArray = afterArguments.ToArray();
Sql<ISqlContext>? mainSql = Sql()
.Select("UniqueId")
.From().AppendSubQuery(rowNumberSql, "NumberedNodes")
.Where($"rn >= ({targetRowSql.SQL}) - @3", beforeArguments.ToArray())
.Where($"rn <= ({targetRowSql.SQL}) + @3", afterArguments.ToArray())
.Where($"rn >= ({targetRowSql.SQL}) - @{beforeAfterParameterIndex}", beforeArgumentsArray)
.Where($"rn <= ({targetRowSql.SQL}) + @{beforeAfterParameterIndex}", afterArgumentsArray)
.OrderBy("rn");
List<Guid>? keys = Database.Fetch<Guid>(mainSql);
totalBefore = GetNumberOfSiblingsOutsideSiblingRange(rowNumberSql, targetRowSql, beforeAfterParameterIndex, beforeArgumentsArray, true);
totalAfter = GetNumberOfSiblingsOutsideSiblingRange(rowNumberSql, targetRowSql, beforeAfterParameterIndex, afterArgumentsArray, false);
if (keys is null || keys.Count == 0)
{
return [];
}
return PerformGetAll(objectType, ordering, sql => sql.WhereIn<NodeDto>(x => x.UniqueId, keys));
// To re-use this method we need to provide a single object type. By convention for folder based trees, we provide the primary object type last.
return PerformGetAll(objectTypes.ToArray(), ordering, sql => sql.WhereIn<NodeDto>(x => x.UniqueId, keys));
}
private static int GetBeforeAfterParameterOffset(ISet<Guid> objectTypes, IQuery<IUmbracoEntity>? filter)
{
int beforeAfterParameterIndexOffset = 0;
// Increment for each object type.
beforeAfterParameterIndexOffset += objectTypes.Count;
// Increment for the provided filter.
if (filter != null)
{
foreach (Tuple<string, object[]> filterClause in filter.GetWhereClauses())
{
// We need to offset by one for each non-array parameter in the filter clause.
// If a query is created using Contains or some other set based operation, we'll get both the array and the
// items in the array provided in the where clauses. It's only the latter that count for applying parameters
// to the SQL statement, and hence we should only offset by them.
beforeAfterParameterIndexOffset += filterClause.Item2.Count(x => !x.GetType().IsArray);
}
}
return beforeAfterParameterIndexOffset;
}
private long GetNumberOfSiblingsOutsideSiblingRange(
Sql<ISqlContext> rowNumberSql,
Sql<ISqlContext> targetRowSql,
int parameterIndex,
object[] arguments,
bool getBefore)
{
Sql<ISqlContext>? sql = Sql()
.SelectCount()
.From().AppendSubQuery(rowNumberSql, "NumberedNodes")
.Where($"rn {(getBefore ? "<" : ">")} ({targetRowSql.SQL}) {(getBefore ? "-" : "+")} @{parameterIndex}", arguments);
return Database.ExecuteScalar<long>(sql);
}
@@ -270,16 +334,16 @@ internal sealed class EntityRepository : RepositoryBase, IEntityRepositoryExtend
}
private IEnumerable<IEntitySlim> PerformGetAll(
Guid objectType,
Guid[] objectTypes,
Ordering ordering,
Action<Sql<ISqlContext>>? filter = null)
{
var isContent = objectType == Constants.ObjectTypes.Document ||
objectType == Constants.ObjectTypes.DocumentBlueprint;
var isMedia = objectType == Constants.ObjectTypes.Media;
var isMember = objectType == Constants.ObjectTypes.Member;
var isContent = objectTypes.Contains(Constants.ObjectTypes.Document) ||
objectTypes.Contains(Constants.ObjectTypes.DocumentBlueprint);
var isMedia = objectTypes.Contains(Constants.ObjectTypes.Media);
var isMember = objectTypes.Contains(Constants.ObjectTypes.Member);
Sql<ISqlContext> sql = GetFullSqlForEntityType(isContent, isMedia, isMember, objectType, ordering, filter);
Sql<ISqlContext> sql = GetFullSqlForEntityType(isContent, isMedia, isMember, objectTypes, ordering, filter);
return GetEntities(sql, isContent, isMedia, isMember);
}
@@ -526,8 +590,17 @@ internal sealed class EntityRepository : RepositoryBase, IEntityRepositoryExtend
Guid objectType,
Ordering ordering,
Action<Sql<ISqlContext>>? filter)
=> GetFullSqlForEntityType(isContent, isMedia, isMember, [objectType], ordering, filter);
protected Sql<ISqlContext> GetFullSqlForEntityType(
bool isContent,
bool isMedia,
bool isMember,
Guid[] objectTypes,
Ordering ordering,
Action<Sql<ISqlContext>>? filter)
{
Sql<ISqlContext> sql = GetBaseWhere(isContent, isMedia, isMember, false, filter, new[] { objectType });
Sql<ISqlContext> sql = GetBaseWhere(isContent, isMedia, isMember, false, filter, objectTypes);
AddGroupBy(isContent, isMedia, isMember, sql, false);
ApplyOrdering(ref sql, ordering);
@@ -742,6 +815,8 @@ internal sealed class EntityRepository : RepositoryBase, IEntityRepositoryExtend
Ordering? runner = ordering;
Direction lastDirection = Direction.Ascending;
bool orderingIncludesNodeId = false;
do
{
@@ -753,7 +828,10 @@ internal sealed class EntityRepository : RepositoryBase, IEntityRepositoryExtend
case "PATH":
orderBy = SqlSyntax.GetQuotedColumn(NodeDto.TableName, "path");
break;
case "NODEID":
orderBy = runner.OrderBy;
orderingIncludesNodeId = true;
break;
default:
orderBy = runner.OrderBy ?? string.Empty;
break;
@@ -768,11 +846,25 @@ internal sealed class EntityRepository : RepositoryBase, IEntityRepositoryExtend
sql.OrderByDescending(orderBy);
}
lastDirection = runner.Direction;
runner = runner.Next;
}
while (runner is not null);
// If we haven't already included the node Id in the order by clause, order by node Id as well to ensure consistent results
// when the provided sort yields entities with the same value.
if (orderingIncludesNodeId is false)
{
if (lastDirection == Direction.Ascending)
{
sql.OrderBy<NodeDto>(x => x.NodeId);
}
else
{
sql.OrderByDescending<NodeDto>(x => x.NodeId);
}
}
}
#endregion

View File

@@ -72,6 +72,8 @@ public interface ISqlSyntaxProvider
string ConvertDecimalToOrderableString { get; }
string ConvertUniqueIdentifierToString => throw new NotImplementedException();
/// <summary>
/// Returns the default isolation level for the database
/// </summary>

View File

@@ -469,6 +469,8 @@ public abstract class SqlSyntaxProviderBase<TSyntax> : ISqlSyntaxProvider
public virtual string ConvertDecimalToOrderableString => "REPLACE(STR({0}, 20, 9), SPACE(1), '0')";
public virtual string ConvertUniqueIdentifierToString => "CONVERT(nvarchar(36), {0})";
private DbTypes InitColumnTypeMap()
{
var dbTypeMap = new DbTypesFactory();

View File

@@ -31,6 +31,7 @@ public class RuntimeState : IRuntimeState
private readonly IConflictingRouteService _conflictingRouteService = null!;
private readonly IEnumerable<IDatabaseProviderMetadata> _databaseProviderMetadata = null!;
private readonly IRuntimeModeValidationService _runtimeModeValidationService = null!;
private readonly IDatabaseAvailabilityCheck _databaseAvailabilityCheck = null!;
/// <summary>
/// The initial <see cref="RuntimeState"/>
@@ -46,6 +47,7 @@ public class RuntimeState : IRuntimeState
/// <summary>
/// Initializes a new instance of the <see cref="RuntimeState" /> class.
/// </summary>
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
public RuntimeState(
IOptions<GlobalSettings> globalSettings,
IOptions<UnattendedSettings> unattendedSettings,
@@ -56,6 +58,34 @@ public class RuntimeState : IRuntimeState
IConflictingRouteService conflictingRouteService,
IEnumerable<IDatabaseProviderMetadata> databaseProviderMetadata,
IRuntimeModeValidationService runtimeModeValidationService)
: this(
globalSettings,
unattendedSettings,
umbracoVersion,
databaseFactory,
logger,
packageMigrationState,
conflictingRouteService,
databaseProviderMetadata,
runtimeModeValidationService,
StaticServiceProvider.Instance.GetRequiredService<IDatabaseAvailabilityCheck>())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="RuntimeState" /> class.
/// </summary>
public RuntimeState(
IOptions<GlobalSettings> globalSettings,
IOptions<UnattendedSettings> unattendedSettings,
IUmbracoVersion umbracoVersion,
IUmbracoDatabaseFactory databaseFactory,
ILogger<RuntimeState> logger,
PendingPackageMigrations packageMigrationState,
IConflictingRouteService conflictingRouteService,
IEnumerable<IDatabaseProviderMetadata> databaseProviderMetadata,
IRuntimeModeValidationService runtimeModeValidationService,
IDatabaseAvailabilityCheck databaseAvailabilityCheck)
{
_globalSettings = globalSettings;
_unattendedSettings = unattendedSettings;
@@ -66,6 +96,7 @@ public class RuntimeState : IRuntimeState
_conflictingRouteService = conflictingRouteService;
_databaseProviderMetadata = databaseProviderMetadata;
_runtimeModeValidationService = runtimeModeValidationService;
_databaseAvailabilityCheck = databaseAvailabilityCheck;
}
/// <inheritdoc />
@@ -242,7 +273,7 @@ public class RuntimeState : IRuntimeState
{
try
{
if (!TryDbConnect(databaseFactory))
if (_databaseAvailabilityCheck.IsDatabaseAvailable(databaseFactory) is false)
{
return UmbracoDatabaseState.CannotConnect;
}
@@ -305,27 +336,4 @@ public class RuntimeState : IRuntimeState
}
return CurrentMigrationState != FinalMigrationState;
}
private bool TryDbConnect(IUmbracoDatabaseFactory databaseFactory)
{
// anything other than install wants a database - see if we can connect
// (since this is an already existing database, assume localdb is ready)
bool canConnect;
var tries = 5;
for (var i = 0; ;)
{
canConnect = databaseFactory.CanConnect;
if (canConnect || ++i == tries)
{
break;
}
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
{
_logger.LogDebug("Could not immediately connect to database, trying again.");
}
Thread.Sleep(1000);
}
return canConnect;
}
}

View File

@@ -0,0 +1,14 @@
using System.Text.Encodings.Web;
using System.Text.Unicode;
using Umbraco.Cms.Core.Serialization;
namespace Umbraco.Cms.Infrastructure.Serialization;
/// <inheritdoc />
public sealed class DefaultJsonSerializerEncoderFactory : IJsonSerializerEncoderFactory
{
/// <inheritdoc />
public JavaScriptEncoder CreateEncoder<TSerializer>()
where TSerializer : IJsonSerializer
=> JavaScriptEncoder.Create(UnicodeRanges.BasicLatin);
}

View File

@@ -1,6 +1,8 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Serialization;
@@ -14,11 +16,24 @@ public sealed class SystemTextConfigurationEditorJsonSerializer : SystemTextJson
/// <summary>
/// Initializes a new instance of the <see cref="SystemTextConfigurationEditorJsonSerializer" /> class.
/// </summary>
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
public SystemTextConfigurationEditorJsonSerializer()
: this(
StaticServiceProvider.Instance.GetRequiredService<IJsonSerializerEncoderFactory>())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SystemTextConfigurationEditorJsonSerializer" /> class.
/// </summary>
public SystemTextConfigurationEditorJsonSerializer(IJsonSerializerEncoderFactory jsonSerializerEncoderFactory)
: base(jsonSerializerEncoderFactory)
=> _jsonSerializerOptions = new JsonSerializerOptions()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Encoder = jsonSerializerEncoderFactory.CreateEncoder<SystemTextConfigurationEditorJsonSerializer>(),
// In some cases, configs aren't camel cased in the DB, so we have to resort to case insensitive
// property name resolving when creating configuration objects (deserializing DB configs).
PropertyNameCaseInsensitive = true,
@@ -40,6 +55,7 @@ public sealed class SystemTextConfigurationEditorJsonSerializer : SystemTextJson
.WithAddedModifier(UseAttributeConfiguredPropertyNames()),
};
/// <inheritdoc/>
protected override JsonSerializerOptions JsonSerializerOptions => _jsonSerializerOptions;
/// <summary>

View File

@@ -1,5 +1,8 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Serialization;
namespace Umbraco.Cms.Infrastructure.Serialization;
@@ -8,13 +11,27 @@ public sealed class SystemTextJsonSerializer : SystemTextJsonSerializerBase
{
private readonly JsonSerializerOptions _jsonSerializerOptions;
/// <summary>
/// Initializes a new instance of the <see cref="SystemTextConfigurationEditorJsonSerializer" /> class.
/// </summary>
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
public SystemTextJsonSerializer()
: this(
StaticServiceProvider.Instance.GetRequiredService<IJsonSerializerEncoderFactory>())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SystemTextJsonSerializer" /> class.
/// </summary>
public SystemTextJsonSerializer()
public SystemTextJsonSerializer(IJsonSerializerEncoderFactory jsonSerializerEncoderFactory)
: base(jsonSerializerEncoderFactory)
=> _jsonSerializerOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Encoder = jsonSerializerEncoderFactory.CreateEncoder<SystemTextJsonSerializer>(),
Converters =
{
new JsonStringEnumConverter(),
@@ -25,5 +42,6 @@ public sealed class SystemTextJsonSerializer : SystemTextJsonSerializerBase
}
};
/// <inheritdoc/>
protected override JsonSerializerOptions JsonSerializerOptions => _jsonSerializerOptions;
}

View File

@@ -1,6 +1,9 @@
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.CodeAnalysis;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Extensions;
@@ -8,6 +11,28 @@ namespace Umbraco.Cms.Infrastructure.Serialization;
public abstract class SystemTextJsonSerializerBase : IJsonSerializer
{
private readonly IJsonSerializerEncoderFactory _jsonSerializerEncoderFactory;
/// <summary>
/// Initializes a new instance of the <see cref="SystemTextJsonSerializerBase" /> class.
/// </summary>
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
protected SystemTextJsonSerializerBase()
: this(
StaticServiceProvider.Instance.GetRequiredService<IJsonSerializerEncoderFactory>())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SystemTextJsonSerializerBase"/> class.
/// </summary>
/// <param name="jsonSerializerEncoderFactory">The <see cref="IJsonSerializerEncoderFactory"/> for creating the <see cref="JavaScriptEncoder"/>.</param>
protected SystemTextJsonSerializerBase(IJsonSerializerEncoderFactory jsonSerializerEncoderFactory)
=> _jsonSerializerEncoderFactory = jsonSerializerEncoderFactory;
/// <summary>
/// Gets the <see cref="System.Text.Json.JsonSerializerOptions"/>.
/// </summary>
protected abstract JsonSerializerOptions JsonSerializerOptions { get; }
/// <inheritdoc />

View File

@@ -1,5 +1,7 @@
using System.Text.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Serialization;
namespace Umbraco.Cms.Infrastructure.Serialization;
@@ -9,23 +11,38 @@ public sealed class SystemTextWebhookJsonSerializer : SystemTextJsonSerializerBa
{
private readonly JsonSerializerOptions _jsonSerializerOptions;
/// <summary>
/// Initializes a new instance of the <see cref="SystemTextConfigurationEditorJsonSerializer" /> class.
/// </summary>
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 18.")]
public SystemTextWebhookJsonSerializer()
: this(
StaticServiceProvider.Instance.GetRequiredService<IJsonSerializerEncoderFactory>())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SystemTextWebhookJsonSerializer" /> class.
/// </summary>
public SystemTextWebhookJsonSerializer()
public SystemTextWebhookJsonSerializer(IJsonSerializerEncoderFactory jsonSerializerEncoderFactory)
: base(jsonSerializerEncoderFactory)
=> _jsonSerializerOptions = new JsonSerializerOptions()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Encoder = jsonSerializerEncoderFactory.CreateEncoder<SystemTextWebhookJsonSerializer>(),
Converters =
{
new JsonStringEnumConverter(),
new JsonUdiConverter(),
new JsonUdiRangeConverter(),
new JsonObjectConverter(), // Required for block editor values
new JsonBlockValueConverter()
new JsonBlockValueConverter(),
},
TypeInfoResolver = new WebhookJsonTypeResolver(),
};
/// <inheritdoc/>
protected override JsonSerializerOptions JsonSerializerOptions => _jsonSerializerOptions;
}