Content search abstractions to facilitate new search in the backoffice (#19046)
This commit is contained in:
7
src/Umbraco.Core/Services/IContentSearchService.cs
Normal file
7
src/Umbraco.Core/Services/IContentSearchService.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services;
|
||||
|
||||
public interface IContentSearchService : IContentSearchService<IContent>
|
||||
{
|
||||
}
|
||||
14
src/Umbraco.Core/Services/IContentSearchServiceOfTContent.cs
Normal file
14
src/Umbraco.Core/Services/IContentSearchServiceOfTContent.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services;
|
||||
|
||||
public interface IContentSearchService<TContent>
|
||||
where TContent : class, IContentBase
|
||||
{
|
||||
Task<PagedModel<TContent>> SearchChildrenAsync(
|
||||
string? query,
|
||||
Guid? parentId,
|
||||
Ordering? ordering,
|
||||
int skip = 0,
|
||||
int take = 100);
|
||||
}
|
||||
7
src/Umbraco.Core/Services/IMediaSearchService.cs
Normal file
7
src/Umbraco.Core/Services/IMediaSearchService.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services;
|
||||
|
||||
public interface IMediaSearchService : IContentSearchService<IMedia>
|
||||
{
|
||||
}
|
||||
@@ -81,6 +81,8 @@ public static partial class UmbracoBuilderExtensions
|
||||
builder.Services.AddUnique<IContentTypeSearchService, ContentTypeSearchService>();
|
||||
builder.Services.AddUnique<IIndexedEntitySearchService, IndexedEntitySearchService>();
|
||||
builder.Services.TryAddTransient<IReservedFieldNamesService, ReservedFieldNamesService>();
|
||||
builder.Services.AddUnique<IContentSearchService, ContentSearchService>();
|
||||
builder.Services.AddUnique<IMediaSearchService, MediaSearchService>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Persistence.Querying;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Services;
|
||||
@@ -17,24 +15,17 @@ internal abstract class ContentListViewServiceBase<TContent, TContentType, TCont
|
||||
{
|
||||
private readonly TContentTypeService _contentTypeService;
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly ISqlContext _sqlContext;
|
||||
private readonly IContentSearchService<TContent> _contentSearchService;
|
||||
|
||||
protected ContentListViewServiceBase(TContentTypeService contentTypeService, IDataTypeService dataTypeService, ISqlContext sqlContext)
|
||||
protected ContentListViewServiceBase(TContentTypeService contentTypeService, IDataTypeService dataTypeService, IContentSearchService<TContent> contentSearchService)
|
||||
{
|
||||
_contentTypeService = contentTypeService;
|
||||
_dataTypeService = dataTypeService;
|
||||
_sqlContext = sqlContext;
|
||||
_contentSearchService = contentSearchService;
|
||||
}
|
||||
|
||||
protected abstract Guid DefaultListViewKey { get; }
|
||||
|
||||
protected abstract Task<PagedModel<TContent>> GetPagedChildrenAsync(
|
||||
int id,
|
||||
IQuery<TContent>? filter,
|
||||
Ordering? ordering,
|
||||
int skip,
|
||||
int take);
|
||||
|
||||
protected abstract Task<bool> HasAccessToListViewItemAsync(IUser user, Guid key);
|
||||
|
||||
protected async Task<Attempt<ListViewPagedModel<TContent>?, ContentCollectionOperationStatus>> GetListViewResultAsync(
|
||||
@@ -62,7 +53,7 @@ internal abstract class ContentListViewServiceBase<TContent, TContentType, TCont
|
||||
return Attempt.FailWithStatus<ListViewPagedModel<TContent>?, ContentCollectionOperationStatus>(orderingAttempt.Status, null);
|
||||
}
|
||||
|
||||
PagedModel<TContent> items = await GetAllowedListViewItemsAsync(user, content?.Id ?? Constants.System.Root, filter, orderingAttempt.Result, skip, take);
|
||||
PagedModel<TContent> items = await GetAllowedListViewItemsAsync(user, content?.Key, filter, orderingAttempt.Result, skip, take);
|
||||
|
||||
var result = new ListViewPagedModel<TContent>
|
||||
{
|
||||
@@ -216,20 +207,13 @@ internal abstract class ContentListViewServiceBase<TContent, TContentType, TCont
|
||||
return await _dataTypeService.GetAsync(configuredListViewKey);
|
||||
}
|
||||
|
||||
private async Task<PagedModel<TContent>> GetAllowedListViewItemsAsync(IUser user, int contentId, string? filter, Ordering? ordering, int skip, int take)
|
||||
private async Task<PagedModel<TContent>> GetAllowedListViewItemsAsync(IUser user, Guid? contentId, string? filter, Ordering? ordering, int skip, int take)
|
||||
{
|
||||
var queryFilter = ParseQueryFilter(filter);
|
||||
|
||||
var pagedChildren = await GetPagedChildrenAsync(
|
||||
contentId,
|
||||
queryFilter,
|
||||
ordering,
|
||||
skip,
|
||||
take);
|
||||
PagedModel<TContent> pagedChildren = await _contentSearchService.SearchChildrenAsync(filter, contentId, ordering, skip, take);
|
||||
|
||||
// Filtering out child nodes after getting a paged result is an active choice here, even though the pagination might get off.
|
||||
// This has been the case with this functionality in Umbraco for a long time.
|
||||
var items = await FilterItemsBasedOnAccessAsync(user, pagedChildren.Items);
|
||||
IEnumerable<TContent> items = await FilterItemsBasedOnAccessAsync(user, pagedChildren.Items);
|
||||
|
||||
var pagedResult = new PagedModel<TContent>
|
||||
{
|
||||
@@ -240,18 +224,6 @@ internal abstract class ContentListViewServiceBase<TContent, TContentType, TCont
|
||||
return pagedResult;
|
||||
}
|
||||
|
||||
private IQuery<TContent>? ParseQueryFilter(string? filter)
|
||||
{
|
||||
// Adding multiple conditions - considering key (as Guid) & name as filter param
|
||||
Guid.TryParse(filter, out Guid filterAsGuid);
|
||||
|
||||
return filter.IsNullOrWhiteSpace()
|
||||
? null
|
||||
: _sqlContext.Query<TContent>()
|
||||
.Where(c => (c.Name != null && c.Name.Contains(filter)) ||
|
||||
c.Key == filterAsGuid);
|
||||
}
|
||||
|
||||
// TODO: Optimize the way we filter out only the nodes the user is allowed to see - instead of checking one by one
|
||||
private async Task<IEnumerable<TContent>> FilterItemsBasedOnAccessAsync(IUser user, IEnumerable<TContent> items)
|
||||
{
|
||||
|
||||
@@ -21,9 +21,9 @@ internal sealed class ContentListViewService : ContentListViewServiceBase<IConte
|
||||
IContentService contentService,
|
||||
IContentTypeService contentTypeService,
|
||||
IDataTypeService dataTypeService,
|
||||
ISqlContext sqlContext,
|
||||
IContentSearchService contentSearchService,
|
||||
IContentPermissionAuthorizer contentPermissionAuthorizer)
|
||||
: base(contentTypeService, dataTypeService, sqlContext)
|
||||
: base(contentTypeService, dataTypeService, contentSearchService)
|
||||
{
|
||||
_contentService = contentService;
|
||||
_contentPermissionAuthorizer = contentPermissionAuthorizer;
|
||||
@@ -49,32 +49,6 @@ internal sealed class ContentListViewService : ContentListViewServiceBase<IConte
|
||||
return await GetListViewResultAsync(user, content, dataTypeKey, orderBy, orderCulture, orderDirection, filter, skip, take);
|
||||
}
|
||||
|
||||
protected override Task<PagedModel<IContent>> GetPagedChildrenAsync(
|
||||
int id,
|
||||
IQuery<IContent>? filter,
|
||||
Ordering? ordering,
|
||||
int skip,
|
||||
int take)
|
||||
{
|
||||
PaginationHelper.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize);
|
||||
|
||||
IEnumerable<IContent> items = _contentService.GetPagedChildren(
|
||||
id,
|
||||
pageNumber,
|
||||
pageSize,
|
||||
out var total,
|
||||
filter,
|
||||
ordering);
|
||||
|
||||
var pagedResult = new PagedModel<IContent>
|
||||
{
|
||||
Items = items,
|
||||
Total = total,
|
||||
};
|
||||
|
||||
return Task.FromResult(pagedResult);
|
||||
}
|
||||
|
||||
// We can use an authorizer here, as it already handles all the necessary checks for this filtering.
|
||||
// However, we cannot pass in all the items; we want only the ones that comply, as opposed to
|
||||
// a general response whether the user has access to all nodes.
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Persistence.Querying;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Services.Implement;
|
||||
|
||||
internal sealed class ContentSearchService : ContentSearchServiceBase<IContent>, IContentSearchService
|
||||
{
|
||||
private readonly IContentService _contentService;
|
||||
|
||||
public ContentSearchService(ISqlContext sqlContext, IIdKeyMap idKeyMap, ILogger<ContentSearchService> logger, IContentService contentService)
|
||||
: base(sqlContext, idKeyMap, logger)
|
||||
=> _contentService = contentService;
|
||||
|
||||
protected override UmbracoObjectTypes ObjectType => UmbracoObjectTypes.Document;
|
||||
|
||||
protected override Task<IEnumerable<IContent>> SearchChildrenAsync(
|
||||
IQuery<IContent>? query,
|
||||
int parentId,
|
||||
Ordering? ordering,
|
||||
long pageNumber,
|
||||
int pageSize,
|
||||
out long total)
|
||||
=> Task.FromResult(_contentService.GetPagedChildren(
|
||||
parentId,
|
||||
pageNumber,
|
||||
pageSize,
|
||||
out total,
|
||||
query,
|
||||
ordering));
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Persistence.Querying;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Services.Implement;
|
||||
|
||||
internal abstract class ContentSearchServiceBase<TContent> : IContentSearchService<TContent>
|
||||
where TContent : class, IContentBase
|
||||
{
|
||||
private readonly ISqlContext _sqlContext;
|
||||
private readonly IIdKeyMap _idKeyMap;
|
||||
private readonly ILogger<ContentSearchServiceBase<TContent>> _logger;
|
||||
|
||||
protected ContentSearchServiceBase(ISqlContext sqlContext, IIdKeyMap idKeyMap, ILogger<ContentSearchServiceBase<TContent>> logger)
|
||||
{
|
||||
_sqlContext = sqlContext;
|
||||
_idKeyMap = idKeyMap;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected abstract UmbracoObjectTypes ObjectType { get; }
|
||||
|
||||
protected abstract Task<IEnumerable<TContent>> SearchChildrenAsync(
|
||||
IQuery<TContent>? query,
|
||||
int parentId,
|
||||
Ordering? ordering,
|
||||
long pageNumber,
|
||||
int pageSize,
|
||||
out long total);
|
||||
|
||||
public async Task<PagedModel<TContent>> SearchChildrenAsync(
|
||||
string? query,
|
||||
Guid? parentId,
|
||||
Ordering? ordering,
|
||||
int skip = 0,
|
||||
int take = 100)
|
||||
{
|
||||
var parentIdAsInt = Constants.System.Root;
|
||||
if (parentId.HasValue)
|
||||
{
|
||||
Attempt<int> keyToId = _idKeyMap.GetIdForKey(parentId.Value, ObjectType);
|
||||
if (keyToId.Success is false)
|
||||
{
|
||||
_logger.LogWarning("Could not obtain an ID for parent key: {parentId} (object type: {contentType}", parentId, typeof(TContent).FullName);
|
||||
return new PagedModel<TContent>(0, []);
|
||||
}
|
||||
|
||||
parentIdAsInt = keyToId.Result;
|
||||
}
|
||||
|
||||
PaginationHelper.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize);
|
||||
IQuery<TContent>? contentQuery = ParseQuery(query);
|
||||
|
||||
IEnumerable<TContent> items = await SearchChildrenAsync(contentQuery, parentIdAsInt, ordering, pageNumber, pageSize, out var total);
|
||||
return new PagedModel<TContent>
|
||||
{
|
||||
Items = items,
|
||||
Total = total,
|
||||
};
|
||||
}
|
||||
|
||||
private IQuery<TContent>? ParseQuery(string? query)
|
||||
{
|
||||
// Adding multiple conditions - considering key (as Guid) & name as filter param
|
||||
Guid.TryParse(query, out Guid filterAsGuid);
|
||||
|
||||
return query.IsNullOrWhiteSpace()
|
||||
? null
|
||||
: _sqlContext
|
||||
.Query<TContent>()
|
||||
.Where(c => (c.Name != null && c.Name.Contains(query)) || c.Key == filterAsGuid);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Persistence.Querying;
|
||||
using Umbraco.Cms.Core.Security.Authorization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Services.Implement;
|
||||
|
||||
@@ -20,9 +18,9 @@ internal sealed class MediaListViewService : ContentListViewServiceBase<IMedia,
|
||||
IMediaService mediaService,
|
||||
IMediaTypeService mediaTypeService,
|
||||
IDataTypeService dataTypeService,
|
||||
ISqlContext sqlContext,
|
||||
IMediaSearchService mediaSearchService,
|
||||
IMediaPermissionAuthorizer mediaPermissionAuthorizer)
|
||||
: base(mediaTypeService, dataTypeService, sqlContext)
|
||||
: base(mediaTypeService, dataTypeService, mediaSearchService)
|
||||
{
|
||||
_mediaService = mediaService;
|
||||
_mediaPermissionAuthorizer = mediaPermissionAuthorizer;
|
||||
@@ -50,27 +48,6 @@ internal sealed class MediaListViewService : ContentListViewServiceBase<IMedia,
|
||||
return await GetListViewResultAsync(user, media, dataTypeKey, orderBy, null, orderDirection, filter, skip, take);
|
||||
}
|
||||
|
||||
protected override Task<PagedModel<IMedia>> GetPagedChildrenAsync(int id, IQuery<IMedia>? filter, Ordering? ordering, int skip, int take)
|
||||
{
|
||||
PaginationHelper.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize);
|
||||
|
||||
IEnumerable<IMedia> items = _mediaService.GetPagedChildren(
|
||||
id,
|
||||
pageNumber,
|
||||
pageSize,
|
||||
out var total,
|
||||
filter,
|
||||
ordering);
|
||||
|
||||
var pagedResult = new PagedModel<IMedia>
|
||||
{
|
||||
Items = items,
|
||||
Total = total,
|
||||
};
|
||||
|
||||
return Task.FromResult(pagedResult);
|
||||
}
|
||||
|
||||
// We can use an authorizer here, as it already handles all the necessary checks for this filtering.
|
||||
// However, we cannot pass in all the items; we want only the ones that comply, as opposed to
|
||||
// a general response whether the user has access to all nodes.
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Persistence.Querying;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Services.Implement;
|
||||
|
||||
internal sealed class MediaSearchService : ContentSearchServiceBase<IMedia>, IMediaSearchService
|
||||
{
|
||||
private readonly IMediaService _mediaService;
|
||||
|
||||
public MediaSearchService(ISqlContext sqlContext, IIdKeyMap idKeyMap, ILogger<MediaSearchService> logger, IMediaService mediaService)
|
||||
: base(sqlContext, idKeyMap, logger)
|
||||
=> _mediaService = mediaService;
|
||||
|
||||
protected override UmbracoObjectTypes ObjectType => UmbracoObjectTypes.Media;
|
||||
|
||||
protected override Task<IEnumerable<IMedia>> SearchChildrenAsync(
|
||||
IQuery<IMedia>? query,
|
||||
int parentId,
|
||||
Ordering? ordering,
|
||||
long pageNumber,
|
||||
int pageSize,
|
||||
out long total)
|
||||
=> Task.FromResult(_mediaService.GetPagedChildren(
|
||||
parentId,
|
||||
pageNumber,
|
||||
pageSize,
|
||||
out total,
|
||||
query,
|
||||
ordering));
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Tests.Common.Builders;
|
||||
using Umbraco.Cms.Tests.Common.Builders.Extensions;
|
||||
using Umbraco.Cms.Tests.Common.Testing;
|
||||
using Umbraco.Cms.Tests.Integration.Testing;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services;
|
||||
|
||||
// NOTE: this is not an exhaustive test suite for IContentSearchService, because the core implementation simply
|
||||
// wraps IContentService to search children (IContentService.GetPagedChildren), and that's tested elsewhere.
|
||||
// instead, these tests aim at testing the logic of the shared base between the core implementations of
|
||||
// IContentSearchService and IMediaSearchService (which does the same thing using IMediaService).
|
||||
[TestFixture]
|
||||
[UmbracoTest(
|
||||
Database = UmbracoTestOptions.Database.NewSchemaPerFixture,
|
||||
PublishedRepositoryEvents = true,
|
||||
WithApplication = true)]
|
||||
public class ContentSearchServiceTests : UmbracoIntegrationTest
|
||||
{
|
||||
private Dictionary<string, IContent> _contentByName = new ();
|
||||
|
||||
protected IContentSearchService ContentSearchService => GetRequiredService<IContentSearchService>();
|
||||
|
||||
protected IContentService ContentService => GetRequiredService<IContentService>();
|
||||
|
||||
protected IContentTypeService ContentTypeService => GetRequiredService<IContentTypeService>();
|
||||
|
||||
[Test]
|
||||
public async Task Can_Search_Children_Of_System_Root()
|
||||
{
|
||||
var result = await ContentSearchService.SearchChildrenAsync(null, null, null, 0, 1000);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.AreEqual(3, result.Total);
|
||||
Assert.AreEqual(3, result.Items.Count());
|
||||
});
|
||||
|
||||
var resultKeys = result.Items.Select(item => item.Key).ToArray();
|
||||
var expectedKeys = new[]
|
||||
{
|
||||
_contentByName["Root 1"].Key,
|
||||
_contentByName["Root 2"].Key,
|
||||
_contentByName["Root 3"].Key
|
||||
};
|
||||
CollectionAssert.AreEqual(expectedKeys, resultKeys);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Search_Children_Of_Specified_Parent()
|
||||
{
|
||||
var result = await ContentSearchService.SearchChildrenAsync(null, _contentByName["Root 1"].Key, null, 0, 1000);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.AreEqual(5, result.Total);
|
||||
Assert.AreEqual(5, result.Items.Count());
|
||||
});
|
||||
|
||||
var resultKeys = result.Items.Select(item => item.Key).ToArray();
|
||||
var expectedKeys = new[]
|
||||
{
|
||||
_contentByName["Root 1/Child 1"].Key,
|
||||
_contentByName["Root 1/Child 2"].Key,
|
||||
_contentByName["Root 1/Child 3"].Key,
|
||||
_contentByName["Root 1/Child 4"].Key,
|
||||
_contentByName["Root 1/Child 5"].Key
|
||||
};
|
||||
CollectionAssert.AreEqual(expectedKeys, resultKeys);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Apply_Pagination_With_Skip_Take()
|
||||
{
|
||||
var result = await ContentSearchService.SearchChildrenAsync(null, _contentByName["Root 2"].Key, Ordering.By("name"), 2, 2);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.AreEqual(5, result.Total);
|
||||
Assert.AreEqual(2, result.Items.Count());
|
||||
});
|
||||
|
||||
var resultKeys = result.Items.Select(item => item.Key).ToArray();
|
||||
var expectedKeys = new[]
|
||||
{
|
||||
_contentByName["Root 2/Child 3"].Key,
|
||||
_contentByName["Root 2/Child 4"].Key
|
||||
};
|
||||
CollectionAssert.AreEqual(expectedKeys, resultKeys);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Filter_By_Name()
|
||||
{
|
||||
var result = await ContentSearchService.SearchChildrenAsync("2", _contentByName["Root 3"].Key, null);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.AreEqual(1, result.Total);
|
||||
Assert.AreEqual(1, result.Items.Count());
|
||||
});
|
||||
|
||||
Assert.AreEqual(_contentByName["Root 3/Child 2"].Key, result.Items.First().Key);
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public async Task SetUpTest()
|
||||
{
|
||||
if (_contentByName.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var contentType = new ContentTypeBuilder()
|
||||
.WithAlias("theContentType")
|
||||
.Build();
|
||||
contentType.AllowedAsRoot = true;
|
||||
await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey);
|
||||
contentType.AllowedContentTypes = [new() { Alias = contentType.Alias, Key = contentType.Key }];
|
||||
await ContentTypeService.UpdateAsync(contentType, Constants.Security.SuperUserKey);
|
||||
foreach (var rootNumber in Enumerable.Range(1, 3))
|
||||
{
|
||||
var root = new ContentBuilder()
|
||||
.WithContentType(contentType)
|
||||
.WithName($"Root {rootNumber}")
|
||||
.Build();
|
||||
ContentService.Save(root);
|
||||
_contentByName[root.Name!] = root;
|
||||
|
||||
foreach (var childNumber in Enumerable.Range(1, 5))
|
||||
{
|
||||
var child = new ContentBuilder()
|
||||
.WithContentType(contentType)
|
||||
.WithParent(root)
|
||||
.WithName($"Child {childNumber}")
|
||||
.Build();
|
||||
ContentService.Save(child);
|
||||
_contentByName[$"{root.Name!}/{child.Name!}"] = child;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user