diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index ac5f185..fde7893 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -11,10 +11,11 @@ - Prefer var over explicit types. - Employ dependency injection using the built-in DI container. - Handle exceptions gracefully and use `Microsoft.Extensions.Logging` for logging. +- This is a Xperience By Kentico library, and as such should leverage built in api as much as possible. ## Testing Guidelnes -- Use the AAA paattern (Arrange, Act, Assert) +- Use the AAA pattern (Arrange, Act, Assert) - Avoid infrastructure dependencies. - Name tests clearly. - Write minimally passing tests. @@ -22,8 +23,4 @@ - Avoid logic in tests. - Prefer helper methods for setup and teardown. - Avoid multiple acts in a single test. -- Write unit tests using **NUnit** and aim for high code coverage. - -## Project-specific instructions - -- Utilize Xperience by Kentico API for database interactions if applicable. +- Write unit tests using **NUnit** and aim for high code coverage. \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index f72faa7..2ec4bfd 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,7 +6,7 @@ true - + @@ -15,9 +15,9 @@ - + - + diff --git a/src/XperienceCommunity.DataRepository/BaseRepository.cs b/src/XperienceCommunity.DataRepository/BaseRepository.cs index fdfbc94..caeb3b0 100644 --- a/src/XperienceCommunity.DataRepository/BaseRepository.cs +++ b/src/XperienceCommunity.DataRepository/BaseRepository.cs @@ -104,7 +104,6 @@ protected async Task> ExecutePageQuery(ContentItemQueryBuilder return result; } - if (dependencyFunc is not null) { cs.CacheDependency = dependencyFunc.Invoke(); diff --git a/src/XperienceCommunity.DataRepository/ContentTypeRepository.cs b/src/XperienceCommunity.DataRepository/ContentTypeRepository.cs index bb4222a..e2fa463 100644 --- a/src/XperienceCommunity.DataRepository/ContentTypeRepository.cs +++ b/src/XperienceCommunity.DataRepository/ContentTypeRepository.cs @@ -42,9 +42,10 @@ public async Task> GetAllAsync(IEnumerable nodeGuid, var builder = new ContentItemQueryBuilder(); builder.ForContentType(config => config - .When(maxLinkedItems > 0, linkOptions => linkOptions.WithLinkedItems(maxLinkedItems)) - .Where(where => where.WhereIn(nameof(IContentItemFieldsSource.SystemFields.ContentItemGUID), - guidList))).When(!string.IsNullOrEmpty(languageName), lang => lang.InLanguage(languageName)); + .WithLinkedItemsAndWebPageData(maxLinkedItems) + .Where(where => where.WhereIn(nameof(IContentItemFieldsSource.SystemFields.ContentItemGUID), + guidList))) + .WithLanguage(languageName); var result = await ExecuteContentQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetAllAsync), guidList, maxLinkedItems); @@ -69,9 +70,9 @@ public async Task> GetAllAsync(IEnumerable itemIds, st var builder = new ContentItemQueryBuilder(); builder.ForContentType(config => config - .When(maxLinkedItems > 0, linkOptions => linkOptions.WithLinkedItems(maxLinkedItems)) + .WithLinkedItemsAndWebPageData(maxLinkedItems) .Where(where => where.WhereIn(nameof(IContentItemFieldsSource.SystemFields.ContentItemID), idList))) - .When(!string.IsNullOrEmpty(languageName), lang => lang.InLanguage(languageName)); + .WithLanguage(languageName); var result = await ExecuteContentQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetAllAsync), idList, maxLinkedItems); @@ -88,8 +89,9 @@ public async Task> GetAllAsync(string? languageName, int ma var builder = new ContentItemQueryBuilder(); builder.ForContentType(config => - config.When(maxLinkedItems > 0, linkOptions => linkOptions.WithLinkedItems(maxLinkedItems))) - .When(!string.IsNullOrEmpty(languageName), lang => lang.InLanguage(languageName)); + config + .WithLinkedItemsAndWebPageData(maxLinkedItems)) + .WithLanguage(languageName); var result = await ExecuteContentQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetAllAsync), contentType, maxLinkedItems); @@ -108,11 +110,10 @@ public async Task> GetAllBySchema(string? language var builder = new ContentItemQueryBuilder(); builder.ForContentTypes(parameters => parameters - .When(maxLinkedItems > 0, linkItemOptions => linkItemOptions.WithLinkedItems(maxLinkedItems)) + .WithLinkedItemsAndWebPageData(maxLinkedItems) .OfReusableSchema(schemaName) .WithContentTypeFields()) - .When(!string.IsNullOrEmpty(languageName), - lang => lang.InLanguage(languageName)); + .WithLanguage(languageName); var result = await ExecuteContentQuery(builder, () => CacheHelper.GetCacheDependency($"{schemaName}|all"), @@ -130,11 +131,11 @@ public async Task> GetAllBySchema(string? language var builder = new ContentItemQueryBuilder(); builder.ForContentType(config => config - .When(maxLinkedItems > 0, linkOptions => linkOptions.WithLinkedItems(maxLinkedItems)) + .WithLinkedItemsAndWebPageData(maxLinkedItems) .Where(where => where.WhereEquals(nameof(IContentItemFieldsSource.SystemFields.ContentItemGUID), itemGuid)) .TopN(1)) - .When(!string.IsNullOrEmpty(languageName), lang => lang.InLanguage(languageName)); + .WithLanguage(languageName); var result = await ExecuteContentQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetByIdAsync), itemGuid, maxLinkedItems); @@ -150,11 +151,11 @@ public async Task> GetAllBySchema(string? language var builder = new ContentItemQueryBuilder(); builder.ForContentType(config => config - .When(maxLinkedItems > 0, linkOptions => linkOptions.WithLinkedItems(maxLinkedItems)) + .WithLinkedItemsAndWebPageData(maxLinkedItems) .Where(where => where.WhereEquals(nameof(IContentItemFieldsSource.SystemFields.ContentItemID), id)) .TopN(1)) - .When(!string.IsNullOrEmpty(languageName), lang => lang.InLanguage(languageName)); + .WithLanguage(languageName); var result = await ExecuteContentQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetByIdAsync), id, maxLinkedItems); @@ -169,11 +170,11 @@ public async Task> GetAllBySchema(string? language var builder = new ContentItemQueryBuilder(); builder.ForContentType(config => config - .When(maxLinkedItems > 0, linkOptions => linkOptions.WithLinkedItems(maxLinkedItems)) + .WithLinkedItemsAndWebPageData(maxLinkedItems) .Where(where => where .WhereEquals(nameof(IContentQueryDataContainer.ContentItemGUID), id)) .TopN(1)) - .When(!string.IsNullOrEmpty(languageName), lang => lang.InLanguage(languageName)); + .WithLanguage(languageName); var result = await ExecuteContentQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetByIdentifierAsync), id, maxLinkedItems); @@ -189,11 +190,11 @@ public async Task> GetAllBySchema(string? language builder.ForContentType(query => query - .When(maxLinkedItems > 0, linkOptions => linkOptions.WithLinkedItems(maxLinkedItems)) + .WithLinkedItemsAndWebPageData(maxLinkedItems) .Where(where => where.WhereEquals(nameof(IContentItemFieldsSource.SystemFields.ContentItemName), name)) .TopN(1)) - .When(!string.IsNullOrEmpty(languageName), lang => lang.InLanguage(languageName)); + .WithLanguage(languageName); var result = await ExecuteContentQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetByNameAsync), name, maxLinkedItems); @@ -210,7 +211,7 @@ public async Task> GetBySmartFolderGuidAsync(Guid smartFold var builder = new ContentItemQueryBuilder(); builder.ForContentTypes(config => config - .When(maxLinkedItems > 0, linkOptions => linkOptions.WithLinkedItems(maxLinkedItems)) + .WithLinkedItemsAndWebPageData(maxLinkedItems) .OfContentType(contentType) .InSmartFolder(smartFolderId)); @@ -229,7 +230,7 @@ public async Task> GetBySmartFolderIdAsync(int smartFolderI var builder = new ContentItemQueryBuilder(); builder.ForContentTypes(config => config - .When(maxLinkedItems > 0, linkOptions => linkOptions.WithLinkedItems(maxLinkedItems)) + .WithLinkedItemsAndWebPageData(maxLinkedItems) .OfContentType(contentType) .InSmartFolder(smartFolderId)); @@ -255,7 +256,7 @@ public async Task> GetBySmartFolderIdAsync var builder = new ContentItemQueryBuilder(); builder.ForContentTypes(config => config - .When(maxLinkedItems > 0, linkOptions => linkOptions.WithLinkedItems(maxLinkedItems)) + .WithLinkedItemsAndWebPageData(maxLinkedItems) .OfContentType(contentTypes) .InSmartFolder(smartFolderId)); @@ -284,7 +285,7 @@ public async Task> GetBySmartFolderIdAsync var builder = new ContentItemQueryBuilder(); builder.ForContentTypes(config => config - .When(maxLinkedItems > 0, linkOptions => linkOptions.WithLinkedItems(maxLinkedItems)) + .WithLinkedItemsAndWebPageData(maxLinkedItems) .OfContentType(contentTypes) .InSmartFolder(smartFolderId)); @@ -314,7 +315,7 @@ public async Task> GetBySmartFolderIdAsync var builder = new ContentItemQueryBuilder(); builder.ForContentTypes(config => config - .When(maxLinkedItems > 0, linkOptions => linkOptions.WithLinkedItems(maxLinkedItems)) + .WithLinkedItemsAndWebPageData(maxLinkedItems) .OfContentType(contentTypes) .InSmartFolder(smartFolderId)); @@ -344,7 +345,7 @@ public async Task> GetBySmartFolderIdAsync var builder = new ContentItemQueryBuilder(); builder.ForContentTypes(config => config - .When(maxLinkedItems > 0, linkOptions => linkOptions.WithLinkedItems(maxLinkedItems)) + .WithLinkedItemsAndWebPageData(maxLinkedItems) .OfContentType(contentTypes) .InSmartFolder(smartFolderId)); @@ -374,8 +375,7 @@ public async Task> GetByTagsAsync(string columnName, IEnume builder.ForContentType(config => config - .When(maxLinkedItems > 0, options => options.WithLinkedItems(maxLinkedItems, - linkOptions => linkOptions.IncludeWebPageData())) + .WithLinkedItemsAndWebPageData(maxLinkedItems) .Where(where => where.WhereContainsTags(columnName, tagIdents))); var result = await ExecuteContentQuery(builder, dependencyFunc, diff --git a/src/XperienceCommunity.DataRepository/Extensions/ContentItemQueryBuilderExtensions.cs b/src/XperienceCommunity.DataRepository/Extensions/ContentItemQueryBuilderExtensions.cs index fc0ed6f..a7b2f8b 100644 --- a/src/XperienceCommunity.DataRepository/Extensions/ContentItemQueryBuilderExtensions.cs +++ b/src/XperienceCommunity.DataRepository/Extensions/ContentItemQueryBuilderExtensions.cs @@ -39,4 +39,17 @@ public static ContentItemQueryBuilder When(this ContentItemQueryBuilder source, return source; } + + /// + /// Configures the to filter by the specified language. + /// + /// The to apply the action to. + /// The language name. + /// The original with the action applied. + public static ContentItemQueryBuilder WithLanguage(this ContentItemQueryBuilder source, string? language) + { + source.When(!string.IsNullOrEmpty(language), q => q.InLanguage(language)); + + return source; + } } diff --git a/src/XperienceCommunity.DataRepository/Extensions/ContentTypeParametersExtensions.cs b/src/XperienceCommunity.DataRepository/Extensions/ContentTypeParametersExtensions.cs index 6bfeeaf..39909a8 100644 --- a/src/XperienceCommunity.DataRepository/Extensions/ContentTypeParametersExtensions.cs +++ b/src/XperienceCommunity.DataRepository/Extensions/ContentTypeParametersExtensions.cs @@ -1,9 +1,25 @@ using CMS.ContentEngine; +using CMS.DataEngine; +using CMS.Websites; namespace XperienceCommunity.DataRepository.Extensions; public static class ContentTypeParametersExtensions { + + /// + /// Orders the instance by the WebPageItemOrder field in ascending order. + /// + /// The instance. + /// The instance ordered by WebPageItemOrder. + public static ContentTypeQueryParameters OrderByWebPageItemOrder(this ContentTypeQueryParameters source) + { + source + .OrderBy(OrderByColumn.Asc(nameof(IWebPageFieldsSource.SystemFields.WebPageItemOrder))); + + return source; + } + /// /// Conditionally applies an action to the instance. /// @@ -39,4 +55,32 @@ public static ContentTypesQueryParameters When(this ContentTypesQueryParameters return source; } + + /// + /// Configures the instance to include linked items and web page data. + /// + /// The instance. + /// The maximum number of linked items to include. + /// The instance. + public static ContentTypesQueryParameters WithLinkedItemsAndWebPageData(this ContentTypesQueryParameters source, int maxLinkedItems) + { + source.When(maxLinkedItems > 0, options => options.WithLinkedItems(maxLinkedItems, + linkOptions => linkOptions.IncludeWebPageData())); + + return source; + } + + /// + /// Configures the instance to include linked items and web page data. + /// + /// The instance. + /// The maximum number of linked items to include. + /// The instance. + public static ContentTypeQueryParameters WithLinkedItemsAndWebPageData(this ContentTypeQueryParameters source, int maxLinkedItems) + { + source.When(maxLinkedItems > 0, options => options.WithLinkedItems(maxLinkedItems, + linkOptions => linkOptions.IncludeWebPageData())); + + return source; + } } diff --git a/src/XperienceCommunity.DataRepository/Extensions/IContentItemFieldsSourceExtensions.cs b/src/XperienceCommunity.DataRepository/Extensions/IContentItemFieldsSourceExtensions.cs index 9d62313..988931d 100644 --- a/src/XperienceCommunity.DataRepository/Extensions/IContentItemFieldsSourceExtensions.cs +++ b/src/XperienceCommunity.DataRepository/Extensions/IContentItemFieldsSourceExtensions.cs @@ -45,6 +45,13 @@ public static class IContentItemFieldsSourceExtensions /// An enumerable containing the content item IDs. public static IEnumerable GetContentItemIds(this IEnumerable? source) => source?.Select(x => x.SystemFields.ContentItemID) ?? []; + /// + /// Gets the content item GUIDs from the specified collection of content item fields sources. + /// + /// The collection of content item fields sources. + /// An enumerable containing the content item GUIDs. + public static IEnumerable GetContentItemGUIDs(this IEnumerable? source) => source?.Select(x => x.SystemFields.ContentItemGUID) ?? []; + /// /// Gets the content types from the specified collection of content item fields sources. /// diff --git a/src/XperienceCommunity.DataRepository/Extensions/IWebPageFieldsSourceExtensions.cs b/src/XperienceCommunity.DataRepository/Extensions/IWebPageFieldsSourceExtensions.cs index e82c2d4..444821c 100644 --- a/src/XperienceCommunity.DataRepository/Extensions/IWebPageFieldsSourceExtensions.cs +++ b/src/XperienceCommunity.DataRepository/Extensions/IWebPageFieldsSourceExtensions.cs @@ -44,4 +44,10 @@ public static class IWebPageFieldsSourceExtensions /// An enumerable containing the web page item IDs. public static IEnumerable GetWebPageItemIds(this IEnumerable? source) => source?.Select(x => x.SystemFields.WebPageItemID) ?? []; + /// + /// Gets the web page item GUIDs for the specified collection of . + /// + /// The collection of sources to get the web page item GUIDs for. + /// An enumerable containing the web page item GUIDs. + public static IEnumerable GetWebPageItemGuids(this IEnumerable? source) => source?.Select(x => x.SystemFields.WebPageItemGUID) ?? []; } diff --git a/src/XperienceCommunity.DataRepository/Extensions/TypeExtensions.cs b/src/XperienceCommunity.DataRepository/Extensions/TypeExtensions.cs index 3f5a30b..49a5c2b 100644 --- a/src/XperienceCommunity.DataRepository/Extensions/TypeExtensions.cs +++ b/src/XperienceCommunity.DataRepository/Extensions/TypeExtensions.cs @@ -155,14 +155,17 @@ public static IEnumerable GetRelatedAssetItemGuids(this T source, Expre { if (property.GetValue(source) is AssetRelatedItem item) { - guids.Add(item.Identifier); + if (item.Identifier != Guid.Empty) + { + guids.Add(item.Identifier); + } } } else if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType)) { if (property.GetValue(source) is IEnumerable items) { - guids.AddRange(items.Select(item => item.Identifier)); + guids.AddRange(items.Where(item => item.Identifier != Guid.Empty).Select(item => item.Identifier)); } } } @@ -189,14 +192,17 @@ public static IEnumerable GetRelatedAssetItemGuids(this T source) where { if (property.GetValue(source) is AssetRelatedItem item) { - guids.Add(item.Identifier); + if (item.Identifier != Guid.Empty) + { + guids.Add(item.Identifier); + } } } else if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType)) { if (property.GetValue(source) is IEnumerable items) { - guids.AddRange(items.Select(item => item.Identifier)); + guids.AddRange(items.Where(item => item.Identifier != Guid.Empty).Select(item => item.Identifier)); } } } @@ -229,14 +235,17 @@ public static IEnumerable GetRelatedWebPageGuids(this T source, Express { if (property.GetValue(source) is WebPageRelatedItem item) { - guids.Add(item.WebPageGuid); + if (item.WebPageGuid != Guid.Empty) + { + guids.Add(item.WebPageGuid); + } } } else if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType)) { if (property.GetValue(source) is IEnumerable items) { - guids.AddRange(items.Select(item => item.WebPageGuid)); + guids.AddRange(items.Where(item => item.WebPageGuid != Guid.Empty).Select(item => item.WebPageGuid)); } } } @@ -253,14 +262,17 @@ public static IEnumerable GetRelatedWebPageGuids(this T source, Express { if (property.GetValue(source) is WebPageRelatedItem item) { - guids.Add(item.WebPageGuid); + if (item.WebPageGuid != Guid.Empty) + { + guids.Add(item.WebPageGuid); + } } } else if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType)) { if (property.GetValue(source) is IEnumerable items) { - guids.AddRange(items.Select(item => item.WebPageGuid)); + guids.AddRange(items.Where(item => item.WebPageGuid != Guid.Empty).Select(item => item.WebPageGuid)); } } } @@ -287,14 +299,17 @@ public static IEnumerable GetRelatedWebPageGuids(this T source) where T { if (property.GetValue(source) is WebPageRelatedItem item) { - guids.Add(item.WebPageGuid); + if (item.WebPageGuid != Guid.Empty) + { + guids.Add(item.WebPageGuid); + } } } else if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType)) { if (property.GetValue(source) is IEnumerable items) { - guids.AddRange(items.Select(item => item.WebPageGuid)); + guids.AddRange(items.Where(item => item.WebPageGuid != Guid.Empty).Select(item => item.WebPageGuid)); } } } diff --git a/src/XperienceCommunity.DataRepository/PageTypeRepository.cs b/src/XperienceCommunity.DataRepository/PageTypeRepository.cs index f1f59bb..c119502 100644 --- a/src/XperienceCommunity.DataRepository/PageTypeRepository.cs +++ b/src/XperienceCommunity.DataRepository/PageTypeRepository.cs @@ -1,5 +1,6 @@ -using CMS.ContentEngine; -using CMS.DataEngine; +using System.Collections.ObjectModel; + +using CMS.ContentEngine; using CMS.Helpers; using CMS.Websites; using CMS.Websites.Routing; @@ -10,10 +11,11 @@ using XperienceCommunity.DataRepository.Models; #pragma warning disable S1121 + namespace XperienceCommunity.DataRepository; public sealed class PageTypeRepository(IProgressiveCache cache, IContentQueryExecutor executor, - IWebsiteChannelContext websiteChannelContext, RepositoryOptions options, ICacheDependencyBuilder cacheDependencyBuilder) : BaseRepository(cache, executor, + IWebsiteChannelContext websiteChannelContext, RepositoryOptions options, ICacheDependencyBuilder cacheDependencyBuilder, IWebPageUrlRetriever webPageUrlRetriever) : BaseRepository(cache, executor, websiteChannelContext, options, cacheDependencyBuilder), IPageRepository where TEntity : class, IWebPageFieldsSource { @@ -31,15 +33,16 @@ public async Task> GetAllAsync(string? languageName, int ma .ForContentType( config => config - .When(maxLinkedItems > 0, options => options.WithLinkedItems(maxLinkedItems, - linkOptions => linkOptions.IncludeWebPageData())) - .OrderBy(OrderByColumn.Asc(nameof(IWebPageFieldsSource.SystemFields.WebPageItemOrder))) + .WithLinkedItemsAndWebPageData(maxLinkedItems) + .OrderByWebPageItemOrder() .ForWebsite(WebsiteChannelContext.WebsiteChannelName)) - .When(!string.IsNullOrEmpty(languageName), lang => lang.InLanguage(languageName)); + .WithLanguage(languageName); var result = await ExecutePageQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetAllAsync), languageName ?? string.Empty, contentType, maxLinkedItems); + await UpdateWebPageUrls(webPageUrlRetriever, languageName, result, cancellationToken); + return result; } @@ -60,17 +63,18 @@ public async Task> GetAllAsync(IEnumerable nodeGuid, .ForContentType( config => config - .When(maxLinkedItems > 0, options => options.WithLinkedItems(maxLinkedItems, - linkOptions => linkOptions.IncludeWebPageData())) - .OrderBy(OrderByColumn.Asc(nameof(IWebPageFieldsSource.SystemFields.WebPageItemOrder))) + .WithLinkedItemsAndWebPageData(maxLinkedItems) + .OrderByWebPageItemOrder() .ForWebsite(WebsiteChannelContext.WebsiteChannelName) .Where(where => where.WhereIn(nameof(IWebPageContentQueryDataContainer.WebPageItemGUID), guidList))) - .When(!string.IsNullOrEmpty(languageName), options => options.InLanguage(languageName)); + .WithLanguage(languageName); var result = await ExecutePageQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetAllAsync), guidList, languageName ?? string.Empty, contentType, maxLinkedItems); + await UpdateWebPageUrls(webPageUrlRetriever, languageName, result, cancellationToken); + return result; } @@ -91,16 +95,17 @@ public async Task> GetAllAsync(IEnumerable itemIds, st .ForContentType( config => config - .When(maxLinkedItems > 0, options => options.WithLinkedItems(maxLinkedItems, - linkOptions => linkOptions.IncludeWebPageData())) - .OrderBy(OrderByColumn.Asc(nameof(IWebPageFieldsSource.SystemFields.WebPageItemOrder))) + .WithLinkedItemsAndWebPageData(maxLinkedItems) + .OrderByWebPageItemOrder() .Where(where => where.WhereIn(nameof(IWebPageFieldsSource.SystemFields.WebPageItemID), itemIdList))) - .When(!string.IsNullOrEmpty(languageName), lang => lang.InLanguage(languageName)); + .WithLanguage(languageName); var result = await ExecutePageQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetAllAsync), itemIdList, languageName ?? string.Empty, contentType, maxLinkedItems); + await UpdateWebPageUrls(webPageUrlRetriever, languageName, result, cancellationToken); + return result; } @@ -115,11 +120,11 @@ public async Task> GetAllBySchema(string? language var builder = new ContentItemQueryBuilder(); builder.ForContentTypes(parameters => parameters - .When(maxLinkedItems > 0, linkItemOptions => linkItemOptions.WithLinkedItems(maxLinkedItems)) + .WithLinkedItemsAndWebPageData(maxLinkedItems) .OfReusableSchema(schemaName) .WithWebPageData() .ForWebsite(WebsiteChannelContext.WebsiteChannelName)) - .When(!string.IsNullOrEmpty(languageName), lang => lang.InLanguage(languageName)); + .WithLanguage(languageName); var result = await ExecuteContentQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetAllBySchema), schemaName, languageName ?? string.Empty, maxLinkedItems); @@ -137,19 +142,20 @@ public async Task> GetAllBySchema(string? language .ForContentType(contentType, config => config - .When(maxLinkedItems > 0, options => options.WithLinkedItems(maxLinkedItems, - linkOptions => linkOptions.IncludeWebPageData())) - .OrderBy(OrderByColumn.Asc(nameof(IWebPageFieldsSource.SystemFields.WebPageItemOrder))) + .WithLinkedItemsAndWebPageData(maxLinkedItems) + .OrderByWebPageItemOrder() .ForWebsite(WebsiteChannelContext.WebsiteChannelName) .Where(predicate => predicate.WhereEquals(nameof(IWebPageFieldsSource.SystemFields.WebPageItemGUID), itemGuid)) .TopN(1)) - .When(!string.IsNullOrEmpty(languageName), lang => lang.InLanguage(languageName)); + .WithLanguage(languageName); var result = await ExecutePageQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetByGuidAsync), itemGuid, contentType, maxLinkedItems); + await UpdateWebPageUrls(webPageUrlRetriever, languageName, result, cancellationToken); + return result.FirstOrDefault(); } @@ -163,18 +169,19 @@ public async Task> GetAllBySchema(string? language .ForContentType(contentType, config => config - .When(maxLinkedItems > 0, options => options.WithLinkedItems(maxLinkedItems, - linkOptions => linkOptions.IncludeWebPageData())) - .OrderBy(OrderByColumn.Asc(nameof(IWebPageFieldsSource.SystemFields.WebPageItemOrder))) + .WithLinkedItemsAndWebPageData(maxLinkedItems) + .OrderByWebPageItemOrder() .ForWebsite(WebsiteChannelContext.WebsiteChannelName) .Where(predicate => predicate.WhereEquals(nameof(IWebPageFieldsSource.SystemFields.WebPageItemID), id)) .TopN(1)) - .When(!string.IsNullOrEmpty(languageName), lang => lang.InLanguage(languageName)); + .WithLanguage(languageName); var result = await ExecutePageQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetByIdAsync), id, contentType, maxLinkedItems); + await UpdateWebPageUrls(webPageUrlRetriever, languageName, result, cancellationToken); + return result.FirstOrDefault(); } @@ -188,15 +195,17 @@ public async Task> GetByPathAsync(string path, string? lang .ForContentType(contentType, config => config - .When(maxLinkedItems > 0, options => options.WithLinkedItems(maxLinkedItems, - linkOptions => linkOptions.IncludeWebPageData())) - .OrderBy(OrderByColumn.Asc(nameof(IWebPageFieldsSource.SystemFields.WebPageItemOrder))) + .WithLinkedItemsAndWebPageData(maxLinkedItems) + .OrderByWebPageItemOrder() .ForWebsite(WebsiteChannelContext.WebsiteChannelName, PathMatch.Single(path))) - .When(!string.IsNullOrEmpty(languageName), lang => lang.InLanguage(languageName)); + .WithLanguage(languageName); var result = await ExecutePageQuery(builder, dependencyFunc ?? (() => CacheDependencyHelper.CreateWebPageItemTypeCacheDependency([contentType], WebsiteChannelContext.WebsiteChannelName)), cancellationToken, CachePrefix, nameof(GetByPathAsync), path, contentType, maxLinkedItems); + + await UpdateWebPageUrls(webPageUrlRetriever, languageName, result, cancellationToken); + return result; } @@ -218,15 +227,16 @@ public async Task> GetByPathAsync(stri .ForContentTypes( config => config - .When(maxLinkedItems > 0, options => options.WithLinkedItems(maxLinkedItems, - linkOptions => linkOptions.IncludeWebPageData())) + .WithLinkedItemsAndWebPageData(maxLinkedItems) .OfContentType(contentTypes) .ForWebsite(WebsiteChannelContext.WebsiteChannelName, PathMatch.Single(path))) - .When(!string.IsNullOrEmpty(languageName), lang => lang.InLanguage(languageName)); + .WithLanguage(languageName); var result = await ExecutePageQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetByPathAsync), path, contentTypes, maxLinkedItems); + await UpdateWebPageUrls(webPageUrlRetriever, languageName, result, cancellationToken); + return result; } @@ -249,14 +259,15 @@ public async Task> GetByPathAsync( config => config .OfContentType(contentTypes) - .When(maxLinkedItems > 0, options => options.WithLinkedItems(maxLinkedItems, - linkOptions => linkOptions.IncludeWebPageData())) + .WithLinkedItemsAndWebPageData(maxLinkedItems) .ForWebsite(WebsiteChannelContext.WebsiteChannelName, PathMatch.Single(path))) - .When(!string.IsNullOrEmpty(languageName), lang => lang.InLanguage(languageName)); + .WithLanguage(languageName); var result = await ExecutePageQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetByPathAsync), path, contentTypes, maxLinkedItems); + await UpdateWebPageUrls(webPageUrlRetriever, languageName, result, cancellationToken); + return result; } @@ -280,14 +291,15 @@ public async Task> GetByPathAsync config .OfContentType(contentTypes) - .When(maxLinkedItems > 0, options => options.WithLinkedItems(maxLinkedItems, - linkOptions => linkOptions.IncludeWebPageData())) + .WithLinkedItemsAndWebPageData(maxLinkedItems) .ForWebsite(WebsiteChannelContext.WebsiteChannelName, PathMatch.Single(path))) - .When(!string.IsNullOrEmpty(languageName), lang => lang.InLanguage(languageName)); + .WithLanguage(languageName); var result = await ExecutePageQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetByPathAsync), path, contentTypes, maxLinkedItems); + await UpdateWebPageUrls(webPageUrlRetriever, languageName, result, cancellationToken); + return result; } @@ -306,12 +318,10 @@ public async Task> GetByTagsAsync(string columnName, IEnume } var builder = new ContentItemQueryBuilder() - .ForContentType(contentType, - config => + .ForContentType(config => config - .When(maxLinkedItems > 0, options => options.WithLinkedItems(maxLinkedItems, - linkOptions => linkOptions.IncludeWebPageData())) - .OrderBy(OrderByColumn.Asc(nameof(IWebPageFieldsSource.SystemFields.WebPageItemOrder))) + .WithLinkedItemsAndWebPageData(maxLinkedItems) + .OrderByWebPageItemOrder() .ForWebsite(WebsiteChannelContext.WebsiteChannelName) .Where(where => where.WhereContainsTags(columnName, guidList))); @@ -319,6 +329,21 @@ public async Task> GetByTagsAsync(string columnName, IEnume var result = await ExecutePageQuery(builder, dependencyFunc, cancellationToken, CachePrefix, nameof(GetByTagsAsync), columnName, guidList, maxLinkedItems); + await UpdateWebPageUrls(webPageUrlRetriever, null, result, cancellationToken); + return result; } + + + private async Task UpdateWebPageUrls(IWebPageUrlRetriever webPageUrlRetriever, string? languageName, IEnumerable result, CancellationToken cancellationToken) where T : IWebPageFieldsSource + { + var webPageGuids = new ReadOnlyCollection(result.Select(x => x.SystemFields.WebPageItemGUID).ToArray()); + + var webpageLinks = await webPageUrlRetriever.Retrieve(webPageGuids, WebsiteChannelContext.WebsiteChannelName, languageName, WebsiteChannelContext.IsPreview, cancellationToken); + + foreach (var item in result) + { + item.SystemFields.WebPageUrlPath = webpageLinks.FirstOrDefault(x => x.Key == item.SystemFields.WebPageItemGUID).Value.AbsoluteUrl; + } + } } diff --git a/src/XperienceCommunity.DataRepository/XperienceCommunity.DataRepository.csproj b/src/XperienceCommunity.DataRepository/XperienceCommunity.DataRepository.csproj index 6640876..9d873a8 100644 --- a/src/XperienceCommunity.DataRepository/XperienceCommunity.DataRepository.csproj +++ b/src/XperienceCommunity.DataRepository/XperienceCommunity.DataRepository.csproj @@ -4,7 +4,7 @@ net8.0;net9.0 enable enable - $(NoWarn);NU1504;NU1505;NU1506;NU1701;1591;S1066 + $(NoWarn);NU1504;NU1505;NU1506;NU1701;1591;S1066;S3267 @@ -18,7 +18,9 @@ - + + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/XperienceCommunity.DataRepository/packages.lock.json b/src/XperienceCommunity.DataRepository/packages.lock.json index ff568a3..a584007 100644 --- a/src/XperienceCommunity.DataRepository/packages.lock.json +++ b/src/XperienceCommunity.DataRepository/packages.lock.json @@ -25,9 +25,9 @@ }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[10.4.0.108396, )", - "resolved": "10.4.0.108396", - "contentHash": "xGcLZ+dvkVuBbd3sjPur9X+1owSL/iDoxVFJLhMx3/vq1fmoKM2fwvrZ8ReAas6l715GJ/dWU2ckwlrRVopmbg==" + "requested": "[10.5.0.109200, )", + "resolved": "10.5.0.109200", + "contentHash": "yS8uepqf+HwSEZtkJR7O4siqfyGKX4Lu7mIB6TTppy59czq7qkFjZ+fB7NTRZg8a/Pu7tvwVhpEMcxUTiQZmSw==" }, "AngleSharp": { "type": "Transitive", @@ -530,9 +530,9 @@ }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[10.4.0.108396, )", - "resolved": "10.4.0.108396", - "contentHash": "xGcLZ+dvkVuBbd3sjPur9X+1owSL/iDoxVFJLhMx3/vq1fmoKM2fwvrZ8ReAas6l715GJ/dWU2ckwlrRVopmbg==" + "requested": "[10.5.0.109200, )", + "resolved": "10.5.0.109200", + "contentHash": "yS8uepqf+HwSEZtkJR7O4siqfyGKX4Lu7mIB6TTppy59czq7qkFjZ+fB7NTRZg8a/Pu7tvwVhpEMcxUTiQZmSw==" }, "AngleSharp": { "type": "Transitive", diff --git a/tests/XperienceCommunity.DataRepository.Tests/Extensions/IContentItemFieldsSourceExtensionsTests.cs b/tests/XperienceCommunity.DataRepository.Tests/Extensions/IContentItemFieldsSourceExtensionsTests.cs index 40a9504..a80e160 100644 --- a/tests/XperienceCommunity.DataRepository.Tests/Extensions/IContentItemFieldsSourceExtensionsTests.cs +++ b/tests/XperienceCommunity.DataRepository.Tests/Extensions/IContentItemFieldsSourceExtensionsTests.cs @@ -147,6 +147,25 @@ public void ToTypedList_ReturnsCorrectList() Assert.That(result, Has.Count.EqualTo(2)); } + [Test] + public void GetContentItemGUIDs_ReturnsCorrectGUIDs() + { + var source = new List + { + Substitute.For(), Substitute.For() + }; + + var systemFields1 = new ContentItemFields { ContentItemGUID = Guid.NewGuid() }; + var systemFields2 = new ContentItemFields { ContentItemGUID = Guid.NewGuid() }; + + source[0].SystemFields.Returns(systemFields1); + source[1].SystemFields.Returns(systemFields2); + + var result = source.GetContentItemGUIDs(); + + Assert.That(result, Is.EqualTo(new[] { systemFields1.ContentItemGUID, systemFields2.ContentItemGUID })); + } + public class TestContentItemFieldsSource : IContentItemFieldsSource { public static string CONTENT_TYPE_NAME = "TestContentItemFieldsSource"; diff --git a/tests/XperienceCommunity.DataRepository.Tests/Extensions/IWebPageFieldsSourceExtensionsTests.cs b/tests/XperienceCommunity.DataRepository.Tests/Extensions/IWebPageFieldsSourceExtensionsTests.cs index 54f52c2..b214789 100644 --- a/tests/XperienceCommunity.DataRepository.Tests/Extensions/IWebPageFieldsSourceExtensionsTests.cs +++ b/tests/XperienceCommunity.DataRepository.Tests/Extensions/IWebPageFieldsSourceExtensionsTests.cs @@ -135,4 +135,61 @@ public void GetContentTypes_ShouldReturnCorrectContentTypes_WhenSourcesAreNotNul Assert.That(result, Is.EqualTo(new[] { "TestWebPageFieldsSource", "TestWebPageFieldsSource" })); Assert.That(result, Is.EqualTo(new[] { "TestWebPageFieldsSource", "TestWebPageFieldsSource" })); } + [Test] + public void GetCacheDependencyKey_ShouldReturnEmptyArray_WhenSourceIsNull() + { + IWebPageFieldsSource? source = null; + + string[] result = source.GetCacheDependencyKey(); + + Assert.That(result, Is.Empty); + } + + [Test] + public void GetCacheDependencyKeys_ShouldReturnEmptyArray_WhenSourcesAreNull() + { + IEnumerable? sources = null; + + string[] result = sources.GetCacheDependencyKeys(); + + Assert.That(result, Is.Empty); + } + + [Test] + public void GetWebPageItemIds_ShouldReturnEmptyArray_WhenSourcesAreNull() + { + IEnumerable? sources = null; + + var result = sources.GetWebPageItemIds(); + + Assert.That(result, Is.Empty); + } + + [Test] + public void GetWebPageItemGuids_ShouldReturnCorrectGuids_WhenSourcesAreNotNull() + { + var sources = new List + { + Substitute.For(), Substitute.For() + }; + + var systemFields1 = new WebPageFields() { WebPageItemGUID = Guid.NewGuid(), ContentItemIsSecured = false }; + var systemFields2 = new WebPageFields() { WebPageItemGUID = Guid.NewGuid(), ContentItemIsSecured = false }; + + sources[0].SystemFields.Returns(systemFields1); + sources[1].SystemFields.Returns(systemFields2); + + var result = sources.GetWebPageItemGuids(); + Assert.That(result, Is.EqualTo(new[] { systemFields1.WebPageItemGUID, systemFields2.WebPageItemGUID })); + } + + [Test] + public void GetWebPageItemGuids_ShouldReturnEmptyArray_WhenSourcesAreNull() + { + IEnumerable? sources = null; + + var result = sources.GetWebPageItemGuids(); + + Assert.That(result, Is.Empty); + } } diff --git a/tests/XperienceCommunity.DataRepository.Tests/Extensions/TypeExtensionsTests.cs b/tests/XperienceCommunity.DataRepository.Tests/Extensions/TypeExtensionsTests.cs index 433522e..ca68809 100644 --- a/tests/XperienceCommunity.DataRepository.Tests/Extensions/TypeExtensionsTests.cs +++ b/tests/XperienceCommunity.DataRepository.Tests/Extensions/TypeExtensionsTests.cs @@ -1,4 +1,5 @@ using CMS.ContentEngine; +using CMS.MediaLibrary; using CMS.Websites; using NSubstitute; @@ -158,7 +159,7 @@ public void GetRelatedWebPageGuids_SingleItem_ReturnsGuid() var guid = Guid.NewGuid(); var relatedItem = new WebPageRelatedItem { WebPageGuid = guid }; - var source = new TestContentItemFieldsSource() { RelatedItem = relatedItem }; + var source = new TestWebPageFieldsSource() { RelatedItem = relatedItem }; // Act var result = source.GetRelatedWebPageGuids(x => x.RelatedItem); @@ -190,6 +191,29 @@ public void GetRelatedWebPageGuids_MultipleItems_ReturnsGuids() Assert.That(result, Is.EquivalentTo(new[] { guid1, guid2 })); } + [Test] + public void GetRelatedWebPageGuids_NoPropertySpecified_MultipleItems_ReturnsGuids() + { + // Arrange + var guid1 = Guid.NewGuid(); + var guid2 = Guid.NewGuid(); + + var relatedItems = new List + { + new() { WebPageGuid = guid1 }, + new() { WebPageGuid = guid2 } + }; + + var source = new TestWebPageFieldsSource() { RelatedItems = relatedItems }; + + + // Act + var result = source.GetRelatedWebPageGuids(); + + // Assert + Assert.That(result, Is.EquivalentTo(new[] { guid1, guid2 })); + } + [Test] public void GetRelatedWebPageGuids_NoItems_ReturnsEmpty() { @@ -202,11 +226,88 @@ public void GetRelatedWebPageGuids_NoItems_ReturnsEmpty() // Assert Assert.That(result, Is.Empty); } + + [Test] + public void GetRelatedAssetItemGuids_SingleItem_ReturnsGuid() + { + // Arrange + var guid = Guid.NewGuid(); + var relatedItem = new AssetRelatedItem { Identifier = guid }; + + var source = new TestContentItemFieldsSource() { RelatedAssetItem = relatedItem }; + + // Act + var result = source.GetRelatedAssetItemGuids(x => x.RelatedAssetItem); + + // Assert + Assert.That(result, Is.EquivalentTo(new[] { guid })); + } + + [Test] + public void GetRelatedAssetItemGuids_MultipleItems_ReturnsGuids() + { + // Arrange + var guid1 = Guid.NewGuid(); + var guid2 = Guid.NewGuid(); + + var relatedItems = new List + { + new() { Identifier = guid1 }, + new() { Identifier = guid2 } + }; + + var source = new TestContentItemFieldsSource() { RelatedAssetItems = relatedItems }; + + // Act + var result = source.GetRelatedAssetItemGuids(x => x.RelatedAssetItems); + + // Assert + Assert.That(result, Is.EquivalentTo(new[] { guid1, guid2 })); + } + + [Test] + public void GetRelatedAssetItemGuids_NoPropertyExpression_MultipleItems_ReturnsGuids() + { + // Arrange + var guid1 = Guid.NewGuid(); + var guid2 = Guid.NewGuid(); + + var relatedItems = new List + { + new() { Identifier = guid1 }, + new() { Identifier = guid2 } + }; + + var source = new TestContentItemFieldsSource() { RelatedAssetItems = relatedItems }; + + // Act + var result = source.GetRelatedAssetItemGuids(); + + // Assert + Assert.That(result, Is.EquivalentTo(new[] { guid1, guid2 })); + } + + [Test] + public void GetRelatedAssetItemGuids_NoItems_ReturnsEmpty() + { + // Arrange + var source = new TestContentItemFieldsSource(); + + // Act + var result = source.GetRelatedAssetItemGuids(x => x.RelatedAssetItems); + + // Assert + Assert.That(result, Is.Empty); + } + public class TestContentItemFieldsSource : IContentItemFieldsSource { public static string CONTENT_TYPE_NAME = "TestContentItemFieldsSource"; - public WebPageRelatedItem RelatedItem { get; set; } = new WebPageRelatedItem(); + public AssetRelatedItem RelatedAssetItem { get; set; } = new AssetRelatedItem(); + + + public IEnumerable RelatedAssetItems { get; set; } = []; public ContentItemFields SystemFields => throw new NotImplementedException(); } @@ -215,9 +316,13 @@ public class TestWebPageFieldsSource : IWebPageFieldsSource { public static string CONTENT_TYPE_NAME = "TestWebPageFieldsSource"; + public WebPageRelatedItem RelatedItem { get; set; } = new WebPageRelatedItem(); + public IEnumerable RelatedItems { get; set; } = []; public WebPageFields SystemFields => throw new NotImplementedException(); } + + } } diff --git a/tests/XperienceCommunity.DataRepository.Tests/XperienceCommunity.DataRepository.Tests.csproj b/tests/XperienceCommunity.DataRepository.Tests/XperienceCommunity.DataRepository.Tests.csproj index 806e451..b985f4f 100644 --- a/tests/XperienceCommunity.DataRepository.Tests/XperienceCommunity.DataRepository.Tests.csproj +++ b/tests/XperienceCommunity.DataRepository.Tests/XperienceCommunity.DataRepository.Tests.csproj @@ -11,14 +11,20 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + all runtime; build; native; contentfiles; analyzers; buildtransitive - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -34,4 +40,11 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/tests/XperienceCommunity.DataRepository.Tests/packages.lock.json b/tests/XperienceCommunity.DataRepository.Tests/packages.lock.json index e097c5e..d641eee 100644 --- a/tests/XperienceCommunity.DataRepository.Tests/packages.lock.json +++ b/tests/XperienceCommunity.DataRepository.Tests/packages.lock.json @@ -4,9 +4,9 @@ "net8.0": { "coverlet.collector": { "type": "Direct", - "requested": "[6.0.3, )", - "resolved": "6.0.3", - "contentHash": "SvLbRq7gjzE34BI90vP6ge812+PAjinNoKhdFZHwVEu/ozJgZY+0KyDh1K0teDeMeuzQJuF8OvleRBYXsZDz0A==" + "requested": "[6.0.4, )", + "resolved": "6.0.4", + "contentHash": "lkhqpF8Pu2Y7IiN7OntbsTtdbpR1syMsm2F3IgX6ootA4ffRqWL5jF7XipHuZQTdVuWG/gVAAcf8mjk8Tz0xPg==" }, "Kentico.Xperience.Core": { "type": "Direct", @@ -62,9 +62,9 @@ }, "NUnit.Analyzers": { "type": "Direct", - "requested": "[4.5.0, )", - "resolved": "4.5.0", - "contentHash": "GQysJCO0mi4bAW64DolpTcYLU3euxLiv7o3EYkp2RPGalUm4vgzX6ANiRAyIDWSSWmYUtFqBlmOUH6WkzxEADw==" + "requested": "[4.6.0, )", + "resolved": "4.6.0", + "contentHash": "uK1TEViVBugOO6uDou1amu7CoNhrd2sEUFr/iaEmVfoeY8qq/zzWCCUZi97aCCSZmjnHKCCWKh3RucU27qPlKg==" }, "NUnit3TestAdapter": { "type": "Direct", @@ -74,9 +74,9 @@ }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[10.4.0.108396, )", - "resolved": "10.4.0.108396", - "contentHash": "xGcLZ+dvkVuBbd3sjPur9X+1owSL/iDoxVFJLhMx3/vq1fmoKM2fwvrZ8ReAas6l715GJ/dWU2ckwlrRVopmbg==" + "requested": "[10.5.0.109200, )", + "resolved": "10.5.0.109200", + "contentHash": "yS8uepqf+HwSEZtkJR7O4siqfyGKX4Lu7mIB6TTppy59czq7qkFjZ+fB7NTRZg8a/Pu7tvwVhpEMcxUTiQZmSw==" }, "AngleSharp": { "type": "Transitive", @@ -596,9 +596,9 @@ "net9.0": { "coverlet.collector": { "type": "Direct", - "requested": "[6.0.3, )", - "resolved": "6.0.3", - "contentHash": "SvLbRq7gjzE34BI90vP6ge812+PAjinNoKhdFZHwVEu/ozJgZY+0KyDh1K0teDeMeuzQJuF8OvleRBYXsZDz0A==" + "requested": "[6.0.4, )", + "resolved": "6.0.4", + "contentHash": "lkhqpF8Pu2Y7IiN7OntbsTtdbpR1syMsm2F3IgX6ootA4ffRqWL5jF7XipHuZQTdVuWG/gVAAcf8mjk8Tz0xPg==" }, "Kentico.Xperience.Core": { "type": "Direct", @@ -654,9 +654,9 @@ }, "NUnit.Analyzers": { "type": "Direct", - "requested": "[4.5.0, )", - "resolved": "4.5.0", - "contentHash": "GQysJCO0mi4bAW64DolpTcYLU3euxLiv7o3EYkp2RPGalUm4vgzX6ANiRAyIDWSSWmYUtFqBlmOUH6WkzxEADw==" + "requested": "[4.6.0, )", + "resolved": "4.6.0", + "contentHash": "uK1TEViVBugOO6uDou1amu7CoNhrd2sEUFr/iaEmVfoeY8qq/zzWCCUZi97aCCSZmjnHKCCWKh3RucU27qPlKg==" }, "NUnit3TestAdapter": { "type": "Direct", @@ -666,9 +666,9 @@ }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[10.4.0.108396, )", - "resolved": "10.4.0.108396", - "contentHash": "xGcLZ+dvkVuBbd3sjPur9X+1owSL/iDoxVFJLhMx3/vq1fmoKM2fwvrZ8ReAas6l715GJ/dWU2ckwlrRVopmbg==" + "requested": "[10.5.0.109200, )", + "resolved": "10.5.0.109200", + "contentHash": "yS8uepqf+HwSEZtkJR7O4siqfyGKX4Lu7mIB6TTppy59czq7qkFjZ+fB7NTRZg8a/Pu7tvwVhpEMcxUTiQZmSw==" }, "AngleSharp": { "type": "Transitive",