Skip to content

Instantly share code, notes, and snippets.

@MilanLund
Last active October 10, 2024 07:14
Show Gist options
  • Save MilanLund/7920e50e724ef23bf9b108d9c3311a47 to your computer and use it in GitHub Desktop.
Save MilanLund/7920e50e724ef23bf9b108d9c3311a47 to your computer and use it in GitHub Desktop.
Repository pattern with a Generic type for a Xperience by Kentico project
This gist represents an implementation of repository pattern for Xperiece by Kentico.
The details about this gist could be found on https://www.milanlund.com/knowledge-base/guide-to-repository-pattern-with-a-generic-type-in-xperience-by-kentico
using CMS.ContentEngine;
using CMS.Helpers;
using CMS.Websites;
using CMS.Websites.Routing;
namespace YourProject.Models;
public abstract class ContentRepositoryBase
{
protected IWebsiteChannelContext WebsiteChannelContext { get; init; }
private readonly IContentQueryExecutor executor;
private readonly IProgressiveCache cache;
public ContentRepositoryBase(IWebsiteChannelContext websiteChannelContext, IContentQueryExecutor executor, IProgressiveCache cache)
{
WebsiteChannelContext = websiteChannelContext;
this.executor = executor;
this.cache = cache;
}
public Task<IEnumerable<T>> GetCachedQueryResult<T>(GetCachedQueryResultParameters<T> parameters)
{
if (parameters.QueryBuilder is null)
{
throw new ArgumentNullException(nameof(parameters.QueryBuilder));
}
if (parameters.CacheSettings is null)
{
throw new ArgumentNullException(nameof(parameters.CacheSettings));
}
if (parameters.BuildCacheItemNames is null)
{
throw new ArgumentNullException(nameof(parameters.BuildCacheItemNames));
}
return GetCachedQueryResultInternal(parameters);
}
private async Task<IEnumerable<T>> GetCachedQueryResultInternal<T>(GetCachedQueryResultParameters<T> parameters)
{
if (WebsiteChannelContext.IsPreview)
{
var queryOptions = new ContentQueryExecutionOptions()
{
ForPreview = true
};
return await GetMappedResultByContentItemType(parameters, queryOptions);
}
return await cache.LoadAsync(async (cacheSettings) =>
{
var result = await GetMappedResultByContentItemType(parameters);
if (cacheSettings.Cached = result != null && result.Any())
{
cacheSettings.CacheDependency = CacheHelper.GetCacheDependency(await parameters.BuildCacheItemNames(new GetDependencyCacheKeysParameters<T>
{
CancellationToken = parameters.CancellationToken,
Items = result
}));
}
return result;
}, parameters.CacheSettings);
}
private async Task<List<T>> GetMappedResultByContentItemType<T>(CachedQueryResultParameters<T> parameters, ContentQueryExecutionOptions queryOptions = null)
{
switch (parameters.ContentItemType)
{
case ContentItemType.ReusableContent:
return (await executor.GetMappedResult<T>(parameters.QueryBuilder, queryOptions, cancellationToken: parameters.CancellationToken)).ToList();
case ContentItemType.WebPage:
return (await executor.GetMappedWebPageResult<T>(parameters.QueryBuilder, queryOptions, cancellationToken: parameters.CancellationToken)).ToList();
default:
throw new NotSupportedException($"ContentItemType '{parameters.ContentItemType}' is not supported. Use 'ReusableContent' or 'WebPage' instead.");
}
}
}
using CMS.ContentEngine;
using CMS.Helpers;
namespace YourProject.Models;
public abstract class RepositoryParameters
{
public CancellationToken CancellationToken { get; set; }
public int LinkedItemsMaxLevel { get; set; } = 4;
}
public class GetByGuidsRepositoryParameters : RepositoryParameters
{
public ICollection<Guid> Guids { get; set; }
}
public enum ContentItemType
{
WebPage,
ReusableContent
}
public class GetCachedQueryResultParameters<T>
{
public ContentItemQueryBuilder QueryBuilder { get; set; }
public CacheSettings CacheSettings { get; set; }
public Func<GetDependencyCacheKeysParameters<T>, Task<ISet<string>>> BuildCacheItemNames { get; set; }
public ContentItemType ContentItemType { get; set; }
public CancellationToken CancellationToken { get; set; }
}
public class GetDependencyCacheKeysParameters<T> : RepositoryParameters
{
public IEnumerable<T> Items { get; set; }
}
using YourProject;
using YourProject.Controllers;
using YourProject.Models;
using Kentico.Content.Web.Mvc;
using Kentico.Content.Web.Mvc.Routing;
using Microsoft.AspNetCore.Mvc;
[assembly: RegisterWebPageRoute(Page.CONTENT_TYPE_NAME, typeof(PageController), WebsiteChannelNames = [WebsiteConstants.WEBSITE_CHANNEL_NAME])]
namespace YourProject.Controllers;
public class PageController : Controller
{
private readonly WebPageRepository webPageRepository;
private readonly IWebPageDataContextRetriever webPageDataContextRetriever;
public PageController(WebPageRepository webPageRepository, IWebPageDataContextRetriever webPageDataContextRetriever)
{
this.webPageRepository = webPageRepository;
this.webPageDataContextRetriever = webPageDataContextRetriever;
}
public async Task<IActionResult> Index()
{
var webPage = webPageDataContextRetriever.Retrieve().WebPage;
var pages = await webPageRepository.GetPages<Page>(new GetByGuidsRepositoryParameters()
{
Guids = new List<Guid> { webPage.WebPageItemGUID },
CancellationToken = HttpContext.RequestAborted
});
if (pages == null || !pages.Any())
{
return NotFound();
}
var page = pages.First();
var model = PageViewModel.GetViewModel(page);
return View(model);
}
}
using CMS.ContentEngine;
using CMS.Helpers;
using CMS.Websites.Routing;
namespace YourProject.Models;
public class ReusableContentRepository : ContentRepositoryBase
{
private readonly ILinkedItemsDependencyAsyncRetriever linkedItemsDependencyRetriever;
public ReusableContentRepository(
IWebsiteChannelContext websiteChannelContext,
IContentQueryExecutor executor,
IProgressiveCache cache,
ILinkedItemsDependencyAsyncRetriever linkedItemsDependencyRetriever)
: base(websiteChannelContext, executor, cache)
{
this.linkedItemsDependencyRetriever = linkedItemsDependencyRetriever;
}
public async Task<IEnumerable<T>> GetItems<T>(GetByGuidsRepositoryParameters parameters = null) where T : IContentItemFieldsSource
{
if (parameters == null)
{
parameters = new GetByGuidsRepositoryParameters();
}
var typeName = typeof(T).FullName;
var query = new ContentItemQueryBuilder()
.ForContentType(typeName,
config => config
.WithLinkedItems(parameters.LinkedItemsMaxLevel)
.Where(parameters.Guids != null ? where => where.WhereIn(nameof(IContentQueryDataContainer.ContentItemGUID), parameters.Guids) : null))
.InLanguage(WebsiteConstants.LANGUAGE_DEFAULT);
var cacheSettings = new CacheSettings(WebsiteConstants.CACHE_MINUTES, WebsiteChannelContext.WebsiteChannelName, typeName, WebsiteConstants.LANGUAGE_DEFAULT, parameters.Guids != null ? parameters.Guids.Select(guid => guid.ToString()).Join("|") : "all");
return await GetCachedQueryResult<T>(new GetCachedQueryResultParameters<T>
{
QueryBuilder = query,
CacheSettings = cacheSettings,
BuildCacheItemNames = GetDependencyCacheKeys,
ContentItemType = ContentItemType.ReusableContent,
CancellationToken = parameters.CancellationToken
});
}
private async Task<ISet<string>> GetDependencyCacheKeys<T>(GetDependencyCacheKeysParameters<T> parameters) where T : IContentItemFieldsSource
{
var dependencyCacheKeys =
(await linkedItemsDependencyRetriever
.Get(parameters.Items.Select(item => item.SystemFields.ContentItemID), maxLevel: parameters.LinkedItemsMaxLevel))
.ToHashSet(StringComparer.InvariantCultureIgnoreCase);
foreach (var item in parameters.Items)
{
dependencyCacheKeys.Add(CacheHelper.BuildCacheItemName(new[] { "contentitem", "bychannel", WebsiteChannelContext.WebsiteChannelName, "bycontenttype", typeof(T).FullName }, false));
}
return dependencyCacheKeys;
}
}
...
await reusableContentRepository.GetItems<ReusableContent>(new GetByGuidsRepositoryParameters()
{
Guids = guidsList,
CancellationToken = HttpContext.RequestAborted
});
...
using CMS.ContentEngine;
using CMS.DataEngine;
using CMS.Helpers;
using CMS.Websites;
using CMS.Websites.Routing;
namespace YourProject.Models;
public class WebPageRepository : ContentRepositoryBase
{
private readonly IWebPageLinkedItemsDependencyAsyncRetriever webPageLinkedItemsDependencyAsyncRetriever;
public WebPageRepository(IWebsiteChannelContext websiteChannelContext, IContentQueryExecutor executor, IProgressiveCache cache, IWebPageLinkedItemsDependencyAsyncRetriever webPageLinkedItemsDependencyAsyncRetriever)
: base(websiteChannelContext, executor, cache)
{
this.webPageLinkedItemsDependencyAsyncRetriever = webPageLinkedItemsDependencyAsyncRetriever;
}
public async Task<IEnumerable<T>> GetPages<T>(GetByGuidsRepositoryParameters parameters = null) where T : IWebPageFieldsSource
{
if (parameters == null)
{
parameters = new GetByGuidsRepositoryParameters();
}
var typeName = typeof(T).FullName;
var query = new ContentItemQueryBuilder()
.ForContentType(typeName,
config => config
.WithLinkedItems(parameters.LinkedItemsMaxLevel)
.OrderBy(OrderByColumn.Asc(nameof(IWebPageFieldsSource.SystemFields.WebPageItemOrder)))
.ForWebsite(WebsiteChannelContext.WebsiteChannelName)
.Where(parameters?.Guids != null ? where => where.WhereIn(nameof(IWebPageContentQueryDataContainer.WebPageItemGUID), parameters.Guids) : null))
.InLanguage(WebsiteConstants.LANGUAGE_DEFAULT);
var cacheSettings = new CacheSettings(WebsiteConstants.CACHE_MINUTES, WebsiteChannelContext.WebsiteChannelName, typeName, WebsiteConstants.LANGUAGE_DEFAULT, parameters.Guids != null ? parameters.Guids.Select(guid => guid.ToString()).Join("|") : "all");
return await GetCachedQueryResult(new GetCachedQueryResultParameters<T>
{
QueryBuilder = query,
CacheSettings = cacheSettings,
BuildCacheItemNames = GetDependencyCacheKeys,
ContentItemType = ContentItemType.WebPage,
CancellationToken = parameters.CancellationToken
});
}
private async Task<ISet<string>> GetDependencyCacheKeys<T>(GetDependencyCacheKeysParameters<T> parameters) where T : IWebPageFieldsSource
{
var dependencyCacheKeys =
(await webPageLinkedItemsDependencyAsyncRetriever
.Get(parameters.Items.Select(item => item.SystemFields.WebPageItemID), maxLevel: parameters.LinkedItemsMaxLevel))
.ToHashSet(StringComparer.InvariantCultureIgnoreCase);
foreach (var item in parameters.Items)
{
dependencyCacheKeys.Add(CacheHelper.BuildCacheItemName(new[] { "webpageitem", "bychannel", WebsiteChannelContext.WebsiteChannelName, "bycontenttype", typeof(T).FullName }, false));
}
return dependencyCacheKeys;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment