diff --git a/src/XperienceCommunity.DataRepository/BaseRepository.cs b/src/XperienceCommunity.DataRepository/BaseRepository.cs index e7eda92..fdfbc94 100644 --- a/src/XperienceCommunity.DataRepository/BaseRepository.cs +++ b/src/XperienceCommunity.DataRepository/BaseRepository.cs @@ -3,6 +3,7 @@ using CMS.Websites; using CMS.Websites.Routing; +using XperienceCommunity.DataRepository.Interfaces; using XperienceCommunity.DataRepository.Models; namespace XperienceCommunity.DataRepository; @@ -32,6 +33,11 @@ public abstract class BaseRepository /// protected readonly IWebsiteChannelContext WebsiteChannelContext; + /// + /// Gets the cache dependency builder instance. + /// + protected readonly ICacheDependencyBuilder CacheDependencyBuilder; + /// /// Initializes a new instance of the class. /// @@ -39,12 +45,15 @@ public abstract class BaseRepository /// The content query executor instance. /// The website channel context instance. /// The repository options. + /// The Cache Dependency Builder. protected BaseRepository(IProgressiveCache cache, - IContentQueryExecutor executor, IWebsiteChannelContext websiteChannelContext, RepositoryOptions options) + IContentQueryExecutor executor, IWebsiteChannelContext websiteChannelContext, RepositoryOptions options, ICacheDependencyBuilder cacheDependencyBuilder) { Cache = cache ?? throw new ArgumentNullException(nameof(cache)); Executor = executor ?? throw new ArgumentNullException(nameof(executor)); WebsiteChannelContext = websiteChannelContext ?? throw new ArgumentNullException(nameof(websiteChannelContext)); + CacheDependencyBuilder = + cacheDependencyBuilder ?? throw new ArgumentNullException(nameof(cacheDependencyBuilder)); CacheMinutes = options?.CacheMinutes ?? 10; } @@ -70,7 +79,7 @@ protected ContentQueryExecutionOptions GetQueryExecutionOptions() /// The cancellation token. /// The parts of the cache name. /// The result of the query. - protected async Task> ExecutePageQuery(ContentItemQueryBuilder builder, Func dependencyFunc, CancellationToken cancellationToken = default, + protected async Task> ExecutePageQuery(ContentItemQueryBuilder builder, Func? dependencyFunc = null, CancellationToken cancellationToken = default, params object[] cacheNameParts) { var queryOptions = GetQueryExecutionOptions(); @@ -95,7 +104,24 @@ protected async Task> ExecutePageQuery(ContentItemQueryBuilder return result; } - cs.CacheDependency = dependencyFunc.Invoke(); + + if (dependencyFunc is not null) + { + cs.CacheDependency = dependencyFunc.Invoke(); + } + else + { + var dependency = CacheDependencyBuilder.Create(result); + + if (dependency is not null) + { + cs.CacheDependency = dependency; + } + else + { + cs.BoolCondition = false; + } + } return result; }, cacheSettings, cancellationToken); @@ -110,7 +136,7 @@ protected async Task> ExecutePageQuery(ContentItemQueryBuilder /// The cancellation token. /// The parts of the cache name. /// The result of the query. - protected async Task> ExecuteContentQuery(ContentItemQueryBuilder builder, Func dependencyFunc, CancellationToken cancellationToken = default, + protected async Task> ExecuteContentQuery(ContentItemQueryBuilder builder, Func? dependencyFunc = null, CancellationToken cancellationToken = default, params object[] cacheNameParts) { var queryOptions = GetQueryExecutionOptions(); @@ -135,7 +161,23 @@ protected async Task> ExecuteContentQuery(ContentItemQueryBuil return result; } - cs.CacheDependency = dependencyFunc.Invoke(); + if (dependencyFunc is not null) + { + cs.CacheDependency = dependencyFunc.Invoke(); + } + else + { + var dependency = CacheDependencyBuilder.Create(result); + + if (dependency is not null) + { + cs.CacheDependency = dependency; + } + else + { + cs.BoolCondition = false; + } + } return result; }, cacheSettings, cancellationToken); diff --git a/src/XperienceCommunity.DataRepository/Builders/CacheDependencyBuilder.cs b/src/XperienceCommunity.DataRepository/Builders/CacheDependencyBuilder.cs new file mode 100644 index 0000000..e40f727 --- /dev/null +++ b/src/XperienceCommunity.DataRepository/Builders/CacheDependencyBuilder.cs @@ -0,0 +1,99 @@ +using System.Reflection; + +using CMS.ContentEngine; +using CMS.Helpers; +using CMS.Websites; + +using XperienceCommunity.DataRepository.Interfaces; + +namespace XperienceCommunity.DataRepository.Builders +{ + public sealed class CacheDependencyBuilder : ICacheDependencyBuilder + { + /// + public CMSCacheDependency? Create(IEnumerable items) + { + var keys = ExtractCacheDependencyKeys(items); + + if (keys.Count == 0) + { + return null; + } + + return new CMSCacheDependency() { CacheKeys = [.. keys] }; + } + + + private static void AddDependencyKeys(T value, HashSet dependencyKeys) + { + switch (value) + { + case null: + return; + case ContentItemReference reference: + dependencyKeys.Add($"contentitem|byguid|{reference.Identifier}"); + break; + case WebPageRelatedItem webPageReference: + dependencyKeys.Add($"webpageitem|byguid|{webPageReference.WebPageGuid}"); + break; + case IWebPageFieldsSource webPageFieldSource: + dependencyKeys.Add($"webpageitem|byid|{webPageFieldSource.SystemFields.WebPageItemID}"); + break; + case IContentItemFieldsSource contentItemFieldSource: + dependencyKeys.Add($"contentitem|byid|{contentItemFieldSource.SystemFields.ContentItemID}"); + break; + case IEnumerable webPageFieldSources: + { + foreach (var source in webPageFieldSources) + { + dependencyKeys.Add($"webpageitem|byid|{source.SystemFields.WebPageItemID}"); + } + } + break; + case IEnumerable contentItemFieldSources: + { + foreach (var source in contentItemFieldSources) + { + dependencyKeys.Add($"contentitem|byid|{source.SystemFields.ContentItemID}"); + } + } + break; + default: + break; + } + } + + private static IReadOnlyList ExtractCacheDependencyKeys(in IEnumerable items) + { + var dependencyKeys = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var item in items) + { + if (item is null) + { + continue; + } + + AddDependencyKeys(item, dependencyKeys); + + var properties = item + .GetType() + .GetProperties(BindingFlags.Public | BindingFlags.Instance); + + foreach (var property in properties) + { + object? value = property.GetValue(item); + + if (value is null) + { + continue; + } + + AddDependencyKeys(value, dependencyKeys); + } + } + + return dependencyKeys.ToList(); + } + } +} diff --git a/src/XperienceCommunity.DataRepository/ContentTypeRepository.cs b/src/XperienceCommunity.DataRepository/ContentTypeRepository.cs index e38590c..bb4222a 100644 --- a/src/XperienceCommunity.DataRepository/ContentTypeRepository.cs +++ b/src/XperienceCommunity.DataRepository/ContentTypeRepository.cs @@ -3,7 +3,6 @@ using CMS.Websites.Routing; using XperienceCommunity.DataRepository.Extensions; -using XperienceCommunity.DataRepository.Helpers; using XperienceCommunity.DataRepository.Interfaces; using XperienceCommunity.DataRepository.Models; @@ -16,19 +15,19 @@ public sealed class ContentTypeRepository( IProgressiveCache cache, IContentQueryExecutor executor, IWebsiteChannelContext websiteChannelContext, - RepositoryOptions options) + RepositoryOptions options, + ICacheDependencyBuilder cacheDependencyBuilder) : BaseRepository(cache, executor, - websiteChannelContext, options), IContentRepository + websiteChannelContext, options, cacheDependencyBuilder), IContentRepository where TEntity : class, IContentItemFieldsSource { private readonly string contentType = typeof(TEntity)?.GetContentTypeName() ?? string.Empty; - public override string CachePrefix => $"data|{contentType}|{WebsiteChannelContext.WebsiteChannelName}"; /// public async Task> GetAllAsync(IEnumerable nodeGuid, string? languageName, - int maxLinkedItems = 0, + int maxLinkedItems = 0, Func? dependencyFunc = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(contentType); @@ -47,8 +46,7 @@ public async Task> GetAllAsync(IEnumerable nodeGuid, .Where(where => where.WhereIn(nameof(IContentItemFieldsSource.SystemFields.ContentItemGUID), guidList))).When(!string.IsNullOrEmpty(languageName), lang => lang.InLanguage(languageName)); - var result = await ExecuteContentQuery(builder, - () => CacheDependencyHelper.CreateContentItemGUIDCacheDependency(guidList!), + var result = await ExecuteContentQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetAllAsync), guidList, maxLinkedItems); return result; @@ -56,7 +54,7 @@ public async Task> GetAllAsync(IEnumerable nodeGuid, /// public async Task> GetAllAsync(IEnumerable itemIds, string? languageName, - int maxLinkedItems = 0, + int maxLinkedItems = 0, Func? dependencyFunc = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(contentType); @@ -75,8 +73,7 @@ public async Task> GetAllAsync(IEnumerable itemIds, st .Where(where => where.WhereIn(nameof(IContentItemFieldsSource.SystemFields.ContentItemID), idList))) .When(!string.IsNullOrEmpty(languageName), lang => lang.InLanguage(languageName)); - var result = await ExecuteContentQuery(builder, - () => CacheDependencyHelper.CreateContentItemIDCacheDependency(idList!), + var result = await ExecuteContentQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetAllAsync), idList, maxLinkedItems); return result; @@ -84,7 +81,7 @@ public async Task> GetAllAsync(IEnumerable itemIds, st /// public async Task> GetAllAsync(string? languageName, int maxLinkedItems = 0, - CancellationToken cancellationToken = default) + Func? dependencyFunc = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(contentType); @@ -94,8 +91,7 @@ public async Task> GetAllAsync(string? languageName, int ma config.When(maxLinkedItems > 0, linkOptions => linkOptions.WithLinkedItems(maxLinkedItems))) .When(!string.IsNullOrEmpty(languageName), lang => lang.InLanguage(languageName)); - var result = await ExecuteContentQuery(builder, - () => CacheDependencyHelper.CreateContentItemTypeCacheDependency([contentType]), + var result = await ExecuteContentQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetAllAsync), contentType, maxLinkedItems); return result; @@ -103,7 +99,7 @@ public async Task> GetAllAsync(string? languageName, int ma /// public async Task> GetAllBySchema(string? languageName, int maxLinkedItems = 0, - CancellationToken cancellationToken = default) + Func? dependencyFunc = null, CancellationToken cancellationToken = default) { string? schemaName = typeof(TSchema).GetReusableFieldSchemaName(); @@ -127,7 +123,7 @@ public async Task> GetAllBySchema(string? language /// public async Task GetByGuidAsync(Guid itemGuid, string? languageName, int maxLinkedItems = 0, - CancellationToken cancellationToken = default) + Func? dependencyFunc = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(contentType); @@ -140,8 +136,7 @@ public async Task> GetAllBySchema(string? language .TopN(1)) .When(!string.IsNullOrEmpty(languageName), lang => lang.InLanguage(languageName)); - var result = await ExecuteContentQuery(builder, - () => CacheDependencyHelper.CreateContentItemGUIDCacheDependency([itemGuid]), + var result = await ExecuteContentQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetByIdAsync), itemGuid, maxLinkedItems); return result.FirstOrDefault(); @@ -149,7 +144,7 @@ public async Task> GetAllBySchema(string? language /// public async Task GetByIdAsync(int id, string? languageName, int maxLinkedItems = 0, - CancellationToken cancellationToken = default) + Func? dependencyFunc = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(contentType); var builder = new ContentItemQueryBuilder(); @@ -161,8 +156,7 @@ public async Task> GetAllBySchema(string? language .TopN(1)) .When(!string.IsNullOrEmpty(languageName), lang => lang.InLanguage(languageName)); - var result = await ExecuteContentQuery(builder, - () => CacheDependencyHelper.CreateContentItemIDCacheDependency([id]), + var result = await ExecuteContentQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetByIdAsync), id, maxLinkedItems); return result.FirstOrDefault(); @@ -170,7 +164,7 @@ public async Task> GetAllBySchema(string? language /// public async Task GetByIdentifierAsync(Guid id, string? languageName, int maxLinkedItems = 0, - CancellationToken cancellationToken = default) + Func? dependencyFunc = null, CancellationToken cancellationToken = default) { var builder = new ContentItemQueryBuilder(); @@ -181,8 +175,7 @@ public async Task> GetAllBySchema(string? language .TopN(1)) .When(!string.IsNullOrEmpty(languageName), lang => lang.InLanguage(languageName)); - var result = await ExecuteContentQuery(builder, - () => CacheDependencyHelper.CreateContentItemGUIDCacheDependency([id]), + var result = await ExecuteContentQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetByIdentifierAsync), id, maxLinkedItems); return result.FirstOrDefault(); @@ -190,7 +183,7 @@ public async Task> GetAllBySchema(string? language /// public async Task GetByNameAsync(string name, string? languageName, int maxLinkedItems = 0, - CancellationToken cancellationToken = default) + Func? dependencyFunc = null, CancellationToken cancellationToken = default) { var builder = new ContentItemQueryBuilder(); @@ -202,8 +195,7 @@ public async Task> GetAllBySchema(string? language .TopN(1)) .When(!string.IsNullOrEmpty(languageName), lang => lang.InLanguage(languageName)); - var result = await ExecuteContentQuery(builder, - () => CacheDependencyHelper.CreateContentItemTypeCacheDependency([contentType]), + var result = await ExecuteContentQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetByNameAsync), name, maxLinkedItems); return result.FirstOrDefault(); @@ -211,7 +203,7 @@ public async Task> GetAllBySchema(string? language /// public async Task> GetBySmartFolderGuidAsync(Guid smartFolderId, int maxLinkedItems = 0, - CancellationToken cancellationToken = default) + Func? dependencyFunc = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(contentType); @@ -222,8 +214,7 @@ public async Task> GetBySmartFolderGuidAsync(Guid smartFold .OfContentType(contentType) .InSmartFolder(smartFolderId)); - var result = await ExecuteContentQuery(builder, - () => CacheDependencyHelper.CreateContentItemTypeCacheDependency([contentType]), + var result = await ExecuteContentQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetBySmartFolderGuidAsync), smartFolderId, maxLinkedItems); return result; @@ -231,7 +222,7 @@ public async Task> GetBySmartFolderGuidAsync(Guid smartFold /// public async Task> GetBySmartFolderIdAsync(int smartFolderId, int maxLinkedItems = 0, - CancellationToken cancellationToken = default) + Func? dependencyFunc = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(contentType); @@ -242,8 +233,7 @@ public async Task> GetBySmartFolderIdAsync(int smartFolderI .OfContentType(contentType) .InSmartFolder(smartFolderId)); - var result = await ExecuteContentQuery(builder, - () => CacheDependencyHelper.CreateContentItemTypeCacheDependency([contentType]!), + var result = await ExecuteContentQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetBySmartFolderIdAsync), contentType, smartFolderId, maxLinkedItems); @@ -252,7 +242,7 @@ public async Task> GetBySmartFolderIdAsync(int smartFolderI /// public async Task> GetBySmartFolderIdAsync(int smartFolderId, - int maxLinkedItems = 0, + int maxLinkedItems = 0, Func? dependencyFunc = null, CancellationToken cancellationToken = default) { string?[] contentTypes = [typeof(T1).GetContentTypeName(), typeof(T2).GetContentTypeName()]; @@ -269,9 +259,7 @@ public async Task> GetBySmartFolderIdAsync .OfContentType(contentTypes) .InSmartFolder(smartFolderId)); - - var result = await ExecuteContentQuery(builder, - () => CacheDependencyHelper.CreateContentItemTypeCacheDependency(contentTypes!), + var result = await ExecuteContentQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetBySmartFolderIdAsync), contentTypes, smartFolderId, maxLinkedItems); @@ -280,7 +268,7 @@ public async Task> GetBySmartFolderIdAsync /// public async Task> GetBySmartFolderIdAsync(int smartFolderId, - int maxLinkedItems = 0, + int maxLinkedItems = 0, Func? dependencyFunc = null, CancellationToken cancellationToken = default) { string?[] contentTypes = @@ -300,9 +288,7 @@ public async Task> GetBySmartFolderIdAsync .OfContentType(contentTypes) .InSmartFolder(smartFolderId)); - - var result = await ExecuteContentQuery(builder, - () => CacheDependencyHelper.CreateContentItemTypeCacheDependency(contentTypes!), + var result = await ExecuteContentQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetBySmartFolderIdAsync), contentTypes, smartFolderId, maxLinkedItems); @@ -311,7 +297,7 @@ public async Task> GetBySmartFolderIdAsync /// public async Task> GetBySmartFolderIdAsync( - int smartFolderId, int maxLinkedItems = 0, + int smartFolderId, int maxLinkedItems = 0, Func? dependencyFunc = null, CancellationToken cancellationToken = default) { string?[] contentTypes = @@ -332,17 +318,16 @@ public async Task> GetBySmartFolderIdAsync .OfContentType(contentTypes) .InSmartFolder(smartFolderId)); - - var result = await ExecuteContentQuery(builder, - () => CacheDependencyHelper.CreateContentItemTypeCacheDependency(contentTypes!), + var result = await ExecuteContentQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetBySmartFolderIdAsync), contentTypes, smartFolderId, maxLinkedItems); return result; } + /// public async Task> GetBySmartFolderIdAsync( - int smartFolderId, int maxLinkedItems = 0, + int smartFolderId, int maxLinkedItems = 0, Func? dependencyFunc = null, CancellationToken cancellationToken = default) { string?[] contentTypes = @@ -363,9 +348,7 @@ public async Task> GetBySmartFolderIdAsync .OfContentType(contentTypes) .InSmartFolder(smartFolderId)); - - var result = await ExecuteContentQuery(builder, - () => CacheDependencyHelper.CreateContentItemTypeCacheDependency(contentTypes!), + var result = await ExecuteContentQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetBySmartFolderIdAsync), contentTypes, smartFolderId, maxLinkedItems); @@ -374,7 +357,7 @@ public async Task> GetBySmartFolderIdAsync /// public async Task> GetByTagsAsync(string columnName, IEnumerable tagIdentifiers, - int maxLinkedItems = 0, + int maxLinkedItems = 0, Func? dependencyFunc = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(contentType); @@ -395,8 +378,7 @@ public async Task> GetByTagsAsync(string columnName, IEnume linkOptions => linkOptions.IncludeWebPageData())) .Where(where => where.WhereContainsTags(columnName, tagIdents))); - var result = await ExecuteContentQuery(builder, - () => CacheDependencyHelper.CreateContentItemTypeCacheDependency([contentType]), + var result = await ExecuteContentQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetByTagsAsync), contentType, columnName, maxLinkedItems); diff --git a/src/XperienceCommunity.DataRepository/DependencyInjection.cs b/src/XperienceCommunity.DataRepository/DependencyInjection.cs index d255e5f..ca7847c 100644 --- a/src/XperienceCommunity.DataRepository/DependencyInjection.cs +++ b/src/XperienceCommunity.DataRepository/DependencyInjection.cs @@ -35,6 +35,8 @@ public static IServiceCollection AddXperienceDataRepositories(this IServiceColle services.TryAddScoped(); + services.TryAddScoped(); + return services; } } diff --git a/src/XperienceCommunity.DataRepository/Interfaces/ICacheDependencyBuilder.cs b/src/XperienceCommunity.DataRepository/Interfaces/ICacheDependencyBuilder.cs new file mode 100644 index 0000000..37bd216 --- /dev/null +++ b/src/XperienceCommunity.DataRepository/Interfaces/ICacheDependencyBuilder.cs @@ -0,0 +1,18 @@ +using CMS.Helpers; + +namespace XperienceCommunity.DataRepository.Interfaces +{ + /// + /// Defines a method to create a CMS cache dependency for a collection of items. + /// + public interface ICacheDependencyBuilder + { + /// + /// Creates a CMS cache dependency for the specified collection of items. + /// + /// The type of items in the collection. + /// The collection of items for which to create the cache dependency. + /// A representing the cache dependency for the specified items. + CMSCacheDependency? Create(IEnumerable items); + } +} diff --git a/src/XperienceCommunity.DataRepository/Interfaces/IContentRepository.cs b/src/XperienceCommunity.DataRepository/Interfaces/IContentRepository.cs index e806059..a6b88e1 100644 --- a/src/XperienceCommunity.DataRepository/Interfaces/IContentRepository.cs +++ b/src/XperienceCommunity.DataRepository/Interfaces/IContentRepository.cs @@ -1,4 +1,5 @@ using CMS.ContentEngine; +using CMS.Helpers; namespace XperienceCommunity.DataRepository.Interfaces; @@ -10,10 +11,11 @@ public interface IContentRepository : IRepository where TEntit /// The name of the entities. /// The language name. /// The maximum number of linked items to retrieve. + /// The function to create a cache dependency. /// The cancellation token. - /// A task that represents the asynchronous operation. The task result contains the collection of entities. + /// A task that represents the asynchronous operation. The task result contains the entity. /// Thrown if content type is empty, or name is Empty. - Task GetByNameAsync(string name, string? languageName, int maxLinkedItems = 0, CancellationToken cancellationToken = default); + Task GetByNameAsync(string name, string? languageName, int maxLinkedItems = 0, Func? dependencyFunc = null, CancellationToken cancellationToken = default); /// /// Gets an entity by the specified identifier asynchronously. @@ -21,76 +23,89 @@ public interface IContentRepository : IRepository where TEntit /// The identifier of the entity. /// The language name. /// The maximum number of linked items to retrieve. + /// The function to create a cache dependency. /// The cancellation token. /// A task that represents the asynchronous operation. The task result contains the entity. /// Thrown if content type is empty. - Task GetByIdentifierAsync(Guid id, string? languageName, int maxLinkedItems = 0, CancellationToken cancellationToken = default); + Task GetByIdentifierAsync(Guid id, string? languageName, int maxLinkedItems = 0, Func? dependencyFunc = null, CancellationToken cancellationToken = default); /// /// Gets entities by the specified smart folder ID asynchronously. /// /// The ID of the smart folder. /// Maximum linked items to return. + /// The function to create a cache dependency. /// The cancellation token. /// A task that represents the asynchronous operation. The task result contains the collection of entities. /// Thrown if content type is empty. - Task> GetBySmartFolderIdAsync(int smartFolderId, int maxLinkedItems = 0, - CancellationToken cancellationToken = default); + Task> GetBySmartFolderIdAsync(int smartFolderId, int maxLinkedItems = 0, Func? dependencyFunc = null, CancellationToken cancellationToken = default); /// /// Gets entities by the specified smart folder ID asynchronously. /// + /// The type of the first entity. + /// The type of the second entity. /// The ID of the smart folder. /// Maximum linked items to return. + /// The function to create a cache dependency. /// The cancellation token. /// A task that represents the asynchronous operation. The task result contains the collection of entities. /// Thrown if content type is empty. - Task> GetBySmartFolderIdAsync(int smartFolderId, int maxLinkedItems = 0, - CancellationToken cancellationToken = default); + Task> GetBySmartFolderIdAsync(int smartFolderId, int maxLinkedItems = 0, Func? dependencyFunc = null, CancellationToken cancellationToken = default); /// /// Gets entities by the specified smart folder ID asynchronously. /// + /// The type of the first entity. + /// The type of the second entity. + /// The type of the third entity. /// The ID of the smart folder. /// Maximum linked items to return. + /// The function to create a cache dependency. /// The cancellation token. /// A task that represents the asynchronous operation. The task result contains the collection of entities. /// Thrown if content type is empty. - Task> GetBySmartFolderIdAsync(int smartFolderId, int maxLinkedItems = 0, - CancellationToken cancellationToken = default); + Task> GetBySmartFolderIdAsync(int smartFolderId, int maxLinkedItems = 0, Func? dependencyFunc = null, CancellationToken cancellationToken = default); /// /// Gets entities by the specified smart folder ID asynchronously. /// + /// The type of the first entity. + /// The type of the second entity. + /// The type of the third entity. + /// The type of the fourth entity. /// The ID of the smart folder. /// Maximum linked items to return. + /// The function to create a cache dependency. /// The cancellation token. /// A task that represents the asynchronous operation. The task result contains the collection of entities. /// Thrown if content type is empty. - Task> GetBySmartFolderIdAsync(int smartFolderId, int maxLinkedItems = 0, - CancellationToken cancellationToken = default); - + Task> GetBySmartFolderIdAsync(int smartFolderId, int maxLinkedItems = 0, Func? dependencyFunc = null, CancellationToken cancellationToken = default); /// /// Gets entities by the specified smart folder ID asynchronously. /// + /// The type of the first entity. + /// The type of the second entity. + /// The type of the third entity. + /// The type of the fourth entity. + /// The type of the fifth entity. /// The ID of the smart folder. /// Maximum linked items to return. + /// The function to create a cache dependency. /// The cancellation token. /// A task that represents the asynchronous operation. The task result contains the collection of entities. /// Thrown if content type is empty. - Task> GetBySmartFolderIdAsync(int smartFolderId, int maxLinkedItems = 0, - CancellationToken cancellationToken = default); + Task> GetBySmartFolderIdAsync(int smartFolderId, int maxLinkedItems = 0, Func? dependencyFunc = null, CancellationToken cancellationToken = default); /// - /// Gets entities by the specified smart folder ID asynchronously. + /// Gets entities by the specified smart folder GUID asynchronously. /// /// The GUID of the smart folder. /// Maximum linked items to return. + /// The function to create a cache dependency. /// The cancellation token. /// A task that represents the asynchronous operation. The task result contains the collection of entities. /// Thrown if content type is empty. - Task> GetBySmartFolderGuidAsync(Guid smartFolderId, int maxLinkedItems = 0, - CancellationToken cancellationToken = default); - + Task> GetBySmartFolderGuidAsync(Guid smartFolderId, int maxLinkedItems = 0, Func? dependencyFunc = null, CancellationToken cancellationToken = default); } diff --git a/src/XperienceCommunity.DataRepository/Interfaces/IPageRepository.cs b/src/XperienceCommunity.DataRepository/Interfaces/IPageRepository.cs index bf05d88..6c12f97 100644 --- a/src/XperienceCommunity.DataRepository/Interfaces/IPageRepository.cs +++ b/src/XperienceCommunity.DataRepository/Interfaces/IPageRepository.cs @@ -1,4 +1,5 @@ -using CMS.Websites; +using CMS.Helpers; +using CMS.Websites; namespace XperienceCommunity.DataRepository.Interfaces; @@ -8,53 +9,64 @@ namespace XperienceCommunity.DataRepository.Interfaces; /// The type of the entity. public interface IPageRepository : IRepository where TEntity : class, IWebPageFieldsSource { - /// /// Gets entities by the specified path asynchronously. /// /// The path. /// The language name. - /// Maximum Linked Items to Return + /// Maximum linked items to return. + /// The function to create a cache dependency. /// The cancellation token. /// A task that represents the asynchronous operation. The task result contains the collection of entities. - /// Thrown if content type is empty, or Path is Empty. + /// Thrown if content type is empty, or path is empty. Task> GetByPathAsync(string path, string? languageName, int maxLinkedItems = 0, - CancellationToken cancellationToken = default); - + Func? dependencyFunc = null, CancellationToken cancellationToken = default); /// /// Gets entities by the specified path asynchronously. /// + /// The type of the first entity. + /// The type of the second entity. /// The path. /// The language name. - /// Maximum Linked Items to Return + /// Maximum linked items to return. + /// The function to create a cache dependency. /// The cancellation token. /// A task that represents the asynchronous operation. The task result contains the collection of entities. - /// Thrown if content type is empty, or Path is Empty. + /// Thrown if content type is empty, or path is empty. Task> GetByPathAsync(string path, string? languageName, int maxLinkedItems = 0, - CancellationToken cancellationToken = default); + Func? dependencyFunc = null, CancellationToken cancellationToken = default); /// /// Gets entities by the specified path asynchronously. /// + /// The type of the first entity. + /// The type of the second entity. + /// The type of the third entity. /// The path. /// The language name. - /// Maximum Linked Items to Return + /// Maximum linked items to return. + /// The function to create a cache dependency. /// The cancellation token. /// A task that represents the asynchronous operation. The task result contains the collection of entities. - /// Thrown if content type is empty, or Path is Empty. + /// Thrown if content type is empty, or path is empty. Task> GetByPathAsync(string path, string? languageName, int maxLinkedItems = 0, - CancellationToken cancellationToken = default); + Func? dependencyFunc = null, CancellationToken cancellationToken = default); /// /// Gets entities by the specified path asynchronously. /// + /// The type of the first entity. + /// The type of the second entity. + /// The type of the third entity. + /// The type of the fourth entity. /// The path. /// The language name. - /// Maximum Linked Items to Return + /// Maximum linked items to return. + /// The function to create a cache dependency. /// The cancellation token. /// A task that represents the asynchronous operation. The task result contains the collection of entities. - /// Thrown if content type is empty, or Path is Empty. + /// Thrown if content type is empty, or path is empty. Task> GetByPathAsync(string path, string? languageName, int maxLinkedItems = 0, - CancellationToken cancellationToken = default); + Func? dependencyFunc = null, CancellationToken cancellationToken = default); } diff --git a/src/XperienceCommunity.DataRepository/Interfaces/IRepository.cs b/src/XperienceCommunity.DataRepository/Interfaces/IRepository.cs index 9530545..ea6692e 100644 --- a/src/XperienceCommunity.DataRepository/Interfaces/IRepository.cs +++ b/src/XperienceCommunity.DataRepository/Interfaces/IRepository.cs @@ -1,4 +1,6 @@ -namespace XperienceCommunity.DataRepository.Interfaces; +using CMS.Helpers; + +namespace XperienceCommunity.DataRepository.Interfaces; /// /// Represents a generic repository interface for accessing data. @@ -11,46 +13,54 @@ public interface IRepository /// /// The name of the column to filter by tags. /// The collection of tag identifiers. - /// Maximum Linked Items to Return + /// Maximum linked items to return. + /// The function to create a cache dependency. /// The cancellation token. /// A task that represents the asynchronous operation. The task result contains the collection of entities. /// Thrown if columnName is empty. - Task> GetByTagsAsync(string columnName, IEnumerable tagIdentifiers, int maxLinkedItems = 0, - CancellationToken cancellationToken = default); + Task> GetByTagsAsync(string columnName, IEnumerable tagIdentifiers, + int maxLinkedItems = 0, + Func? dependencyFunc = null, CancellationToken cancellationToken = default); /// /// Gets all entities asynchronously based on the specified node GUIDs. /// /// The node GUIDs. /// The language name. + /// Maximum linked items to return. + /// The function to create a cache dependency. /// The cancellation token. - /// Maximum Linked Items to Return /// A task that represents the asynchronous operation. The task result contains the collection of entities. /// Thrown if content type is empty. Task> GetAllAsync(IEnumerable nodeGuid, string? languageName, int maxLinkedItems = 0, + Func? dependencyFunc = null, CancellationToken cancellationToken = default); /// - /// Gets all entities asynchronously based on the specified Item IDs. + /// Gets all entities asynchronously based on the specified item IDs. /// - /// The Item IDs. + /// The item IDs. /// The language name. - /// Maximum Linked Items to Return + /// Maximum linked items to return. + /// The function to create a cache dependency. /// The cancellation token. /// A task that represents the asynchronous operation. The task result contains the collection of entities. /// Thrown if content type is empty. Task> GetAllAsync(IEnumerable itemIds, string? languageName, int maxLinkedItems = 0, + Func? dependencyFunc = null, CancellationToken cancellationToken = default); /// /// Gets all entities asynchronously. /// /// The language name. - /// Maximum Linked Items to Return + /// Maximum linked items to return. + /// The function to create a cache dependency. /// The cancellation token. /// A task that represents the asynchronous operation. The task result contains the collection of entities. /// Thrown if content type is empty. Task> GetAllAsync(string? languageName, int maxLinkedItems = 0, + Func? dependencyFunc = null, CancellationToken cancellationToken = default); /// @@ -58,11 +68,13 @@ Task> GetAllAsync(string? languageName, int maxLinkedItems /// /// The ID of the entity. /// The language name. - /// Maximum Linked Items to Return + /// Maximum linked items to return. + /// The function to create a cache dependency. /// The cancellation token. /// A task that represents the asynchronous operation. The task result contains the entity. /// Thrown if content type is empty. Task GetByIdAsync(int id, string? languageName, int maxLinkedItems = 0, + Func? dependencyFunc = null, CancellationToken cancellationToken = default); /// @@ -70,11 +82,13 @@ Task> GetAllAsync(string? languageName, int maxLinkedItems /// /// The item GUID of the entity. /// The language name. - /// Maximum Linked Items to Return + /// Maximum linked items to return. + /// The function to create a cache dependency. /// The cancellation token. /// A task that represents the asynchronous operation. The task result contains the entity. /// Thrown if content type is empty. Task GetByGuidAsync(Guid itemGuid, string? languageName, int maxLinkedItems = 0, + Func? dependencyFunc = null, CancellationToken cancellationToken = default); /// @@ -82,10 +96,12 @@ Task> GetAllAsync(string? languageName, int maxLinkedItems /// /// The type of the schema. /// The language name. - /// Maximum Linked Items to Return + /// Maximum linked items to return. + /// The function to create a cache dependency. /// The cancellation token. /// A task that represents the asynchronous operation. The task result contains the collection of entities. /// Thrown if content type is empty. Task> GetAllBySchema(string? languageName, int maxLinkedItems = 0, + Func? dependencyFunc = null, CancellationToken cancellationToken = default); } diff --git a/src/XperienceCommunity.DataRepository/PageTypeRepository.cs b/src/XperienceCommunity.DataRepository/PageTypeRepository.cs index 5598019..f1f59bb 100644 --- a/src/XperienceCommunity.DataRepository/PageTypeRepository.cs +++ b/src/XperienceCommunity.DataRepository/PageTypeRepository.cs @@ -13,8 +13,8 @@ namespace XperienceCommunity.DataRepository; public sealed class PageTypeRepository(IProgressiveCache cache, IContentQueryExecutor executor, - IWebsiteChannelContext websiteChannelContext, RepositoryOptions options) : BaseRepository(cache, executor, - websiteChannelContext, options), IPageRepository + IWebsiteChannelContext websiteChannelContext, RepositoryOptions options, ICacheDependencyBuilder cacheDependencyBuilder) : BaseRepository(cache, executor, + websiteChannelContext, options, cacheDependencyBuilder), IPageRepository where TEntity : class, IWebPageFieldsSource { private readonly string? contentType = typeof(TEntity)?.GetContentTypeName() ?? string.Empty; @@ -23,7 +23,7 @@ public sealed class PageTypeRepository(IProgressiveCache cache, IConten /// public async Task> GetAllAsync(string? languageName, int maxLinkedItems = 0, - CancellationToken cancellationToken = default) + Func? dependencyFunc = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(contentType); @@ -37,7 +37,7 @@ public async Task> GetAllAsync(string? languageName, int ma .ForWebsite(WebsiteChannelContext.WebsiteChannelName)) .When(!string.IsNullOrEmpty(languageName), lang => lang.InLanguage(languageName)); - var result = await ExecutePageQuery(builder, () => CacheDependencyHelper.CreateWebPageItemTypeCacheDependency([contentType], WebsiteChannelContext.WebsiteChannelName), + var result = await ExecutePageQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetAllAsync), languageName ?? string.Empty, contentType, maxLinkedItems); return result; @@ -45,8 +45,7 @@ public async Task> GetAllAsync(string? languageName, int ma /// public async Task> GetAllAsync(IEnumerable nodeGuid, string? languageName, - int maxLinkedItems = 0, - CancellationToken cancellationToken = default) + int maxLinkedItems = 0, Func? dependencyFunc = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(contentType); @@ -69,7 +68,7 @@ public async Task> GetAllAsync(IEnumerable nodeGuid, guidList))) .When(!string.IsNullOrEmpty(languageName), options => options.InLanguage(languageName)); - var result = await ExecutePageQuery(builder, () => CacheDependencyHelper.CreateWebPageItemGUIDCacheDependency(guidList), + var result = await ExecutePageQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetAllAsync), guidList, languageName ?? string.Empty, contentType, maxLinkedItems); return result; @@ -77,7 +76,7 @@ public async Task> GetAllAsync(IEnumerable nodeGuid, /// public async Task> GetAllAsync(IEnumerable itemIds, string? languageName, - int maxLinkedItems = 0, CancellationToken cancellationToken = default) + int maxLinkedItems = 0, Func? dependencyFunc = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(contentType); @@ -99,7 +98,7 @@ public async Task> GetAllAsync(IEnumerable itemIds, st where.WhereIn(nameof(IWebPageFieldsSource.SystemFields.WebPageItemID), itemIdList))) .When(!string.IsNullOrEmpty(languageName), lang => lang.InLanguage(languageName)); - var result = await ExecutePageQuery(builder, () => CacheDependencyHelper.CreateWebPageItemIDCacheDependency(itemIdList), + var result = await ExecutePageQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetAllAsync), itemIdList, languageName ?? string.Empty, contentType, maxLinkedItems); return result; @@ -107,7 +106,7 @@ public async Task> GetAllAsync(IEnumerable itemIds, st /// public async Task> GetAllBySchema(string? languageName, int maxLinkedItems = 0, - CancellationToken cancellationToken = default) + Func? dependencyFunc = null, CancellationToken cancellationToken = default) { string? schemaName = typeof(TSchema).GetReusableFieldSchemaName(); @@ -122,41 +121,15 @@ public async Task> GetAllBySchema(string? language .ForWebsite(WebsiteChannelContext.WebsiteChannelName)) .When(!string.IsNullOrEmpty(languageName), lang => lang.InLanguage(languageName)); - var queryOptions = GetQueryExecutionOptions(); + var result = await ExecuteContentQuery(builder, dependencyFunc, + cancellationToken, CachePrefix, nameof(GetAllBySchema), schemaName, languageName ?? string.Empty, maxLinkedItems); - if (WebsiteChannelContext.IsPreview) - { - var query = await Executor.GetMappedResult(builder, queryOptions, - cancellationToken: cancellationToken); - - return query; - } - - var cacheSettings = - new CacheSettings(CacheMinutes, "data", nameof(GetAllBySchema), schemaName, languageName, maxLinkedItems); - - return await Cache.LoadAsync(async (cs, ct) => - { - var result = (await Executor.GetMappedResult(builder, queryOptions, - cancellationToken: ct))?.ToList() ?? []; - - cs.BoolCondition = result.Count > 0; - - if (!cs.Cached) - { - return result; - } - - cs.CacheDependency = CacheHelper.GetCacheDependency( - $"{schemaName}|all"); - - return result; - }, cacheSettings, cancellationToken); + return result; } /// public async Task GetByGuidAsync(Guid itemGuid, string? languageName, int maxLinkedItems = 0, - CancellationToken cancellationToken = default) + Func? dependencyFunc = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(contentType); @@ -174,7 +147,7 @@ public async Task> GetAllBySchema(string? language .TopN(1)) .When(!string.IsNullOrEmpty(languageName), lang => lang.InLanguage(languageName)); - var result = await ExecutePageQuery(builder, () => CacheDependencyHelper.CreateWebPageItemGUIDCacheDependency([itemGuid]), + var result = await ExecutePageQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetByGuidAsync), itemGuid, contentType, maxLinkedItems); return result.FirstOrDefault(); @@ -182,7 +155,7 @@ public async Task> GetAllBySchema(string? language /// public async Task GetByIdAsync(int id, string? languageName, int maxLinkedItems = 0, - CancellationToken cancellationToken = default) + Func? dependencyFunc = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(contentType); @@ -199,7 +172,7 @@ public async Task> GetAllBySchema(string? language .TopN(1)) .When(!string.IsNullOrEmpty(languageName), lang => lang.InLanguage(languageName)); - var result = await ExecutePageQuery(builder, () => CacheDependencyHelper.CreateWebPageItemIDCacheDependency([id]), + var result = await ExecutePageQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetByIdAsync), id, contentType, maxLinkedItems); return result.FirstOrDefault(); @@ -207,7 +180,7 @@ public async Task> GetAllBySchema(string? language /// public async Task> GetByPathAsync(string path, string? languageName, int maxLinkedItems = 0, - CancellationToken cancellationToken = default) + Func? dependencyFunc = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(contentType); @@ -221,7 +194,7 @@ public async Task> GetByPathAsync(string path, string? lang .ForWebsite(WebsiteChannelContext.WebsiteChannelName, PathMatch.Single(path))) .When(!string.IsNullOrEmpty(languageName), lang => lang.InLanguage(languageName)); - var result = await ExecutePageQuery(builder, () => CacheDependencyHelper.CreateWebPageItemTypeCacheDependency([contentType], WebsiteChannelContext.WebsiteChannelName), + var result = await ExecutePageQuery(builder, dependencyFunc ?? (() => CacheDependencyHelper.CreateWebPageItemTypeCacheDependency([contentType], WebsiteChannelContext.WebsiteChannelName)), cancellationToken, CachePrefix, nameof(GetByPathAsync), path, contentType, maxLinkedItems); return result; @@ -229,8 +202,7 @@ public async Task> GetByPathAsync(string path, string? lang /// public async Task> GetByPathAsync(string path, string? languageName, - int maxLinkedItems = 0, - CancellationToken cancellationToken = default) + int maxLinkedItems = 0, Func? dependencyFunc = null, CancellationToken cancellationToken = default) { string?[] contentTypes = [ @@ -252,7 +224,7 @@ public async Task> GetByPathAsync(stri .ForWebsite(WebsiteChannelContext.WebsiteChannelName, PathMatch.Single(path))) .When(!string.IsNullOrEmpty(languageName), lang => lang.InLanguage(languageName)); - var result = await ExecutePageQuery(builder, () => CacheDependencyHelper.CreateWebPageItemTypeCacheDependency(contentTypes!, WebsiteChannelContext.WebsiteChannelName), + var result = await ExecutePageQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetByPathAsync), path, contentTypes, maxLinkedItems); return result; @@ -260,8 +232,7 @@ public async Task> GetByPathAsync(stri /// public async Task> GetByPathAsync(string path, string? languageName, - int maxLinkedItems = 0, - CancellationToken cancellationToken = default) + int maxLinkedItems = 0, Func? dependencyFunc = null, CancellationToken cancellationToken = default) { string?[] contentTypes = [ @@ -283,7 +254,7 @@ public async Task> GetByPathAsync( .ForWebsite(WebsiteChannelContext.WebsiteChannelName, PathMatch.Single(path))) .When(!string.IsNullOrEmpty(languageName), lang => lang.InLanguage(languageName)); - var result = await ExecutePageQuery(builder, () => CacheDependencyHelper.CreateWebPageItemTypeCacheDependency(contentTypes!, WebsiteChannelContext.WebsiteChannelName), + var result = await ExecutePageQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetByPathAsync), path, contentTypes, maxLinkedItems); return result; @@ -291,8 +262,7 @@ public async Task> GetByPathAsync( /// public async Task> GetByPathAsync(string path, - string? languageName, int maxLinkedItems = 0, - CancellationToken cancellationToken = default) + string? languageName, int maxLinkedItems = 0, Func? dependencyFunc = null, CancellationToken cancellationToken = default) { string?[] contentTypes = [ @@ -315,7 +285,7 @@ public async Task> GetByPathAsync lang.InLanguage(languageName)); - var result = await ExecutePageQuery(builder, () => CacheDependencyHelper.CreateWebPageItemTypeCacheDependency(contentTypes!, WebsiteChannelContext.WebsiteChannelName), + var result = await ExecutePageQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetByPathAsync), path, contentTypes, maxLinkedItems); return result; @@ -323,8 +293,7 @@ public async Task> GetByPathAsync public async Task> GetByTagsAsync(string columnName, IEnumerable tagIdentifiers, - int maxLinkedItems = 0, - CancellationToken cancellationToken = default) + int maxLinkedItems = 0, Func? dependencyFunc = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(columnName); ArgumentException.ThrowIfNullOrEmpty(contentType); @@ -347,7 +316,7 @@ public async Task> GetByTagsAsync(string columnName, IEnume .Where(where => where.WhereContainsTags(columnName, guidList))); - var result = await ExecutePageQuery(builder, () => CacheDependencyHelper.CreateWebPageItemGUIDCacheDependency(guidList!), + var result = await ExecutePageQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetByTagsAsync), columnName, guidList, maxLinkedItems); return result; diff --git a/tests/XperienceCommunity.DataRepository.Tests/Builder/CacheDependencyBuilderTests.cs b/tests/XperienceCommunity.DataRepository.Tests/Builder/CacheDependencyBuilderTests.cs new file mode 100644 index 0000000..bb3dd80 --- /dev/null +++ b/tests/XperienceCommunity.DataRepository.Tests/Builder/CacheDependencyBuilderTests.cs @@ -0,0 +1,85 @@ +using CMS.ContentEngine; +using CMS.Helpers; + +using XperienceCommunity.DataRepository.Builders; + +namespace XperienceCommunity.DataRepository.Tests.Builders +{ + [TestFixture] + public class CacheDependencyBuilderTests + { + private CacheDependencyBuilder builder; + + [SetUp] + public void SetUp() => builder = new CacheDependencyBuilder(); + + [Test] + public void Create_ShouldReturnNull_WhenItemsAreEmpty() + { + // Arrange + var items = Enumerable.Empty(); + + // Act + var result = builder.Create(items); + + // Assert + Assert.That(result, Is.Null); + } + + [Test] + public void Create_ShouldReturnNull_WhenNoDependencyKeysAreFound() + { + // Arrange + var items = new List { new() }; + + // Act + var result = builder.Create(items); + + // Assert + Assert.That(result, Is.Null); + } + + [Test] + public void Create_ShouldReturnCMSCacheDependency_WhenDependencyKeysAreFound() + { + // Arrange + var items = new List + { + new MockContentItemFieldsSource { SystemFields = new ContentItemFields { ContentItemID = 1 } } + }; + + // Act + var result = builder.Create(items); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.InstanceOf()); + Assert.That(1, Is.EqualTo(result.CacheKeys.Length)); + Assert.That("contentitem|byid|1", Is.EqualTo(result.CacheKeys[0])); + } + + [Test] + public void Create_ShouldHandleMultipleDependencyKeys() + { + // Arrange + var items = new List + { + new MockContentItemFieldsSource { SystemFields = new ContentItemFields { ContentItemID = 1 } }, + new MockContentItemFieldsSource { SystemFields = new ContentItemFields { ContentItemID = 2 } } + }; + + // Act + var result = builder.Create(items); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.InstanceOf()); + Assert.That(2, Is.EqualTo(result.CacheKeys.Length)); + } + + public class MockContentItemFieldsSource : IContentItemFieldsSource + { + public ContentItemFields SystemFields { get; set; } = new ContentItemFields(); + } + } +} diff --git a/tests/XperienceCommunity.DataRepository.Tests/XperienceCommunity.DataRepository.Tests.csproj b/tests/XperienceCommunity.DataRepository.Tests/XperienceCommunity.DataRepository.Tests.csproj index 190c878..806e451 100644 --- a/tests/XperienceCommunity.DataRepository.Tests/XperienceCommunity.DataRepository.Tests.csproj +++ b/tests/XperienceCommunity.DataRepository.Tests/XperienceCommunity.DataRepository.Tests.csproj @@ -5,7 +5,8 @@ enable enable false - $(NoWarn);NU1504;NU1505;NU1506;NU1701;1591 + $(NoWarn);NU1504;NU1505;NU1506;NU1701;1591;NUnit2007 + true @@ -29,4 +30,8 @@ + + + +