Relations: Exclude the relate parent on delete relation type from checks for related documents and media on delete, when disable delete with references is enabled (closes #20803) (#20811)

* Exclude the relate parent on delete relation type from checks for related documents and media on delete, when disable delete with references is enabled.

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Applied suggestions from code review.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Andy Butland
2025-11-13 09:32:18 +01:00
committed by GitHub
parent d4d4b8a50a
commit f075223412
9 changed files with 110 additions and 11 deletions

View File

@@ -30,6 +30,14 @@ public interface IQuery<T>
/// <returns>This instance so calls to this method are chainable</returns>
IQuery<T> WhereIn(Expression<Func<T, object>> fieldSelector, IEnumerable? values);
/// <summary>
/// Adds a where-not-in clause to the query
/// </summary>
/// <param name="fieldSelector"></param>
/// <param name="values"></param>
/// <returns>This instance so calls to this method are chainable</returns>
IQuery<T> WhereNotIn(Expression<Func<T, object>> fieldSelector, IEnumerable? values) => throw new NotImplementedException(); // TODO (V18): Remove default implementation.
/// <summary>
/// Adds a set of OR-ed where clauses to the query.
/// </summary>

View File

@@ -62,6 +62,9 @@ internal sealed class ContentEditingService
_languageService = languageService;
}
/// <inheritdoc/>
protected override string? RelateParentOnDeleteAlias => Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias;
public Task<IContent?> GetAsync(Guid key)
{
IContent? content = ContentService.GetById(key);

View File

@@ -75,6 +75,11 @@ internal abstract class ContentEditingServiceBase<TContent, TContentType, TConte
protected TContentTypeService ContentTypeService { get; }
/// <summary>
/// Gets the alias used to relate the parent entity when handling content (document or media) delete operations.
/// </summary>
protected virtual string? RelateParentOnDeleteAlias => null;
protected async Task<Attempt<TContentCreateResult, ContentEditingOperationStatus>> MapCreate<TContentCreateResult>(ContentCreationModelBase contentCreationModelBase)
where TContentCreateResult : ContentCreateResultBase<TContent>, new()
{
@@ -202,9 +207,27 @@ internal abstract class ContentEditingServiceBase<TContent, TContentType, TConte
return Attempt.FailWithStatus<TContent?, ContentEditingOperationStatus>(status, content);
}
if (disabledWhenReferenced && _relationService.IsRelated(content.Id, RelationDirectionFilter.Child))
if (disabledWhenReferenced)
{
return Attempt.FailWithStatus<TContent?, ContentEditingOperationStatus>(referenceFailStatus, content);
// When checking if an item is related, we may need to exclude the "relate parent on delete" relation type, as this prevents
// deleting from the recycle bin.
int[]? excludeRelationTypeIds = null;
if (string.IsNullOrWhiteSpace(RelateParentOnDeleteAlias) is false)
{
IRelationType? relateParentOnDeleteRelationType = _relationService.GetRelationTypeByAlias(RelateParentOnDeleteAlias);
if (relateParentOnDeleteRelationType is not null)
{
excludeRelationTypeIds = [relateParentOnDeleteRelationType.Id];
}
}
if (_relationService.IsRelated(
content.Id,
RelationDirectionFilter.Child,
excludeRelationTypeIds: excludeRelationTypeIds))
{
return Attempt.FailWithStatus<TContent?, ContentEditingOperationStatus>(referenceFailStatus, content);
}
}
var userId = await GetUserIdAsync(userKey);

View File

@@ -297,9 +297,24 @@ public interface IRelationService : IService
/// </summary>
/// <param name="id">Id of an object to check relations for</param>
/// <param name="directionFilter">Indicates whether to check for relations as parent, child or in either direction.</param>
/// <returns>Returns <c>True</c> if any relations exists with the given Id, otherwise <c>False</c></returns>
/// <returns>Returns <c>True</c> if any relations exists with the given Id, otherwise <c>False</c>.</returns>
[Obsolete("Please use the overload taking all parameters. Scheduled for removal in Umbraco 18.")]
bool IsRelated(int id, RelationDirectionFilter directionFilter);
/// <summary>
/// Checks whether any relations exists for the passed in Id and direction.
/// </summary>
/// <param name="id">Id of an object to check relations for</param>
/// <param name="directionFilter">Indicates whether to check for relations as parent, child or in either direction.</param>
/// <param name="includeRelationTypeIds">A collection of relation type Ids to include consideration in the relation checks.</param>
/// <param name="excludeRelationTypeIds">A collection of relation type Ids to exclude from consideration in the relation checks.</param>
/// <remarks>If no relation type Ids are provided in includeRelationTypeIds or excludeRelationTypeIds, all relation type Ids are considered.</remarks>
/// <returns>Returns <c>True</c> if any relations exists with the given Id, otherwise <c>False</c>.</returns>
bool IsRelated(int id, RelationDirectionFilter directionFilter, int[]? includeRelationTypeIds = null, int[]? excludeRelationTypeIds = null)
#pragma warning disable CS0618 // Type or member is obsolete
=> IsRelated(id, directionFilter);
#pragma warning restore CS0618 // Type or member is obsolete
/// <summary>
/// Checks whether two items are related
/// </summary>

View File

@@ -43,6 +43,9 @@ internal sealed class MediaEditingService
contentTypeFilters)
=> _logger = logger;
/// <inheritdoc/>
protected override string? RelateParentOnDeleteAlias => Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias;
public Task<IMedia?> GetAsync(Guid key)
{
IMedia? media = ContentService.GetById(key);

View File

@@ -485,11 +485,14 @@ public class RelationService : RepositoryService, IRelationService
return _relationRepository.Get(query).Any();
}
/// <inheritdoc />
public bool IsRelated(int id) => IsRelated(id, RelationDirectionFilter.Any);
[Obsolete("No longer used in Umbraco, please the overload taking all parameters. Scheduled for removal in Umbraco 19.")]
public bool IsRelated(int id) => IsRelated(id, RelationDirectionFilter.Any, null, null);
[Obsolete("Please the overload taking all parameters. Scheduled for removal in Umbraco 18.")]
public bool IsRelated(int id, RelationDirectionFilter directionFilter) => IsRelated(id, directionFilter, null, null);
/// <inheritdoc />
public bool IsRelated(int id, RelationDirectionFilter directionFilter)
public bool IsRelated(int id, RelationDirectionFilter directionFilter, int[]? includeRelationTypeIds = null, int[]? excludeRelationTypeIds = null)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
IQuery<IRelation> query = Query<IRelation>();
@@ -502,6 +505,16 @@ public class RelationService : RepositoryService, IRelationService
_ => throw new ArgumentOutOfRangeException(nameof(directionFilter)),
};
if (includeRelationTypeIds is not null && includeRelationTypeIds.Length > 0)
{
query = query.WhereIn(x => x.RelationTypeId, includeRelationTypeIds);
}
if (excludeRelationTypeIds is not null && excludeRelationTypeIds.Length > 0)
{
query = query.WhereNotIn(x => x.RelationTypeId, excludeRelationTypeIds);
}
return _relationRepository.Get(query).Any();
}