"Filter" your commerce navigation
Read my blog here
Instantly share code, notes, and snippets.
using System; | |
using System.Collections.Generic; | |
using System.Collections.ObjectModel; | |
using System.Linq; | |
using System.Linq.Expressions; | |
using EPiServer; | |
using EPiServer.Commerce.Catalog.ContentTypes; | |
using EPiServer.Core; | |
using EPiServer.Find; | |
using EPiServer.Find.ClientConventions; | |
using EPiServer.Find.Cms; | |
using EPiServer.Find.Commerce; | |
using EPiServer.Logging; | |
using EPiServer.ServiceLocation; | |
public static class FindNavigationExtensions | |
{ | |
private static readonly Lazy<IContentLoader> ContentLoader = | |
new Lazy<IContentLoader>(() => ServiceLocator.Current.GetInstance<IContentLoader>()); | |
private static readonly Lazy<IClient> FindClient = | |
new Lazy<IClient>(() => ServiceLocator.Current.GetInstance<IClient>()); | |
private static readonly ILogger Log = LogManager.GetLogger(); | |
/// <summary> | |
/// Adds the segment conventions. | |
/// </summary> | |
/// <param name="conventionBuilder">The convention builder.</param>> | |
/// <exception cref="T:System.MemberAccessException">The <see cref="T:System.Lazy`1" /> instance is initialized to use the default constructor of the type that is being lazily initialized, and permissions to access the constructor are missing.</exception> | |
/// <exception cref="T:System.MissingMemberException">The <see cref="T:System.Lazy`1" /> instance is initialized to use the default constructor of the type that is being lazily initialized, and that type does not have a public, parameterless constructor.</exception> | |
/// <exception cref="T:System.InvalidOperationException">The initialization function tries to access <see cref="P:System.Lazy`1.Value" /> on this instance.</exception> | |
public static void AddSegmentConventions(this TypeConventionBuilder<ProductContent> conventionBuilder) | |
{ | |
conventionBuilder | |
.IncludeField(x => x.MainCategoryRouteSegments()) | |
.IncludeField(p => p.SubCategoryRouteSegments()); | |
} | |
/// <summary> | |
/// Gets the catalog main nodes. | |
/// </summary> | |
/// <param name="catalogContent">The catalog.</param> | |
/// <returns>A List of nodes.</returns> | |
/// <exception cref="T:System.MemberAccessException">The <see cref="T:System.Lazy`1" /> instance is initialized to use the default constructor of the type that is being lazily initialized, and permissions to access the constructor are missing.</exception> | |
/// <exception cref="T:System.MissingMemberException">The <see cref="T:System.Lazy`1" /> instance is initialized to use the default constructor of the type that is being lazily initialized, and that type does not have a public, parameterless constructor.</exception> | |
/// <exception cref="T:System.InvalidOperationException">The initialization function tries to access <see cref="P:System.Lazy`1.Value" /> on this instance.</exception> | |
public static List<NodeContent> GetCatalogMainNodes(this CatalogContent catalogContent) | |
{ | |
if (catalogContent == null) | |
{ | |
return new List<NodeContent>(); | |
} | |
List<NodeContent> mainNodes = | |
ContentLoader.Value.GetChildren<NodeContent>(contentLink: catalogContent.ContentLink).ToList(); | |
try | |
{ | |
List<string> segments = catalogContent.ContentLink.GetMainSegments(); | |
return mainNodes.Where(n => segments.Contains(item: n.RouteSegment)).OrderBy(n => n.DisplayName).ToList(); | |
} | |
catch (Exception exception) | |
{ | |
Log.Error(message: exception.Message, exception: exception); | |
return mainNodes.ToList(); | |
} | |
} | |
/// <summary> | |
/// Gets the catalog sub nodes. | |
/// </summary> | |
/// <param name="node">The catalog node.</param> | |
/// <returns>A List of nodes.</returns> | |
/// <exception cref="T:System.MemberAccessException">The <see cref="T:System.Lazy`1" /> instance is initialized to use the default constructor of the type that is being lazily initialized, and permissions to access the constructor are missing.</exception> | |
/// <exception cref="T:System.MissingMemberException">The <see cref="T:System.Lazy`1" /> instance is initialized to use the default constructor of the type that is being lazily initialized, and that type does not have a public, parameterless constructor.</exception> | |
/// <exception cref="T:System.InvalidOperationException">The initialization function tries to access <see cref="P:System.Lazy`1.Value" /> on this instance.</exception> | |
public static List<NodeContent> GetCatalogSubNodes(NodeContent node) | |
{ | |
if (node == null) | |
{ | |
return new List<NodeContent>(); | |
} | |
List<NodeContent> subNodes = | |
ContentLoader.Value.GetChildren<NodeContent>(contentLink: node.ContentLink).ToList(); | |
try | |
{ | |
List<string> segments = node.ContentLink.GetSubSegments(); | |
return subNodes.Where(n => segments.Contains(item: n.RouteSegment)).ToList(); | |
} | |
catch (Exception exception) | |
{ | |
Log.Error(message: exception.Message, exception: exception); | |
return subNodes.ToList(); | |
} | |
} | |
/// <summary> | |
/// Gets the main segments. | |
/// </summary> | |
/// <param name="contentReference">The content reference.</param> | |
/// <returns>A List of main route segments.</returns> | |
/// <exception cref="T:System.MemberAccessException">The <see cref="T:System.Lazy`1" /> instance is initialized to use the default constructor of the type that is being lazily initialized, and permissions to access the constructor are missing.</exception> | |
/// <exception cref="T:System.MissingMemberException">The <see cref="T:System.Lazy`1" /> instance is initialized to use the default constructor of the type that is being lazily initialized, and that type does not have a public, parameterless constructor.</exception> | |
/// <exception cref="T:System.InvalidOperationException">The initialization function tries to access <see cref="P:System.Lazy`1.Value" /> on this instance.</exception> | |
public static List<string> GetMainSegments(this ContentReference contentReference) | |
{ | |
ITypeSearch<ProductContent> query = FindClient.Value.Search<ProductContent>() | |
.Filter(x => x.Ancestors().Match(contentReference.ToString())) | |
.FilterOnCurrentMarket() | |
.FilterPriceAvailableForCurrentUser() | |
.FilterOnCurrentSite() | |
.FilterForVisitor(); | |
return query.GetMainSegmentsForQuery(); | |
} | |
/// <summary> | |
/// Gets the main segments. | |
/// </summary> | |
/// <typeparam name="T">The type to search for.</typeparam> | |
/// <param name="query">The query.</param> | |
/// <returns>A List of main route segments.</returns> | |
public static List<string> GetMainSegmentsForQuery<T>(this ITypeSearch<T> query) where T : ProductContent | |
{ | |
query = query | |
.TermsFacetFor(x => x.MainCategoryRouteSegments(), facet => facet.Size = 25) | |
.Take(0) | |
.StaticallyCacheFor(new TimeSpan(0, 1, 0, 0)); | |
IContentResult<T> results = query.GetContentResult(3600); | |
List<string> segments = results.TermsFacetFor(x => x.MainCategoryRouteSegments()).Terms | |
.Where(tc => tc.Count > 0).Select(tc => tc.Term).ToList(); | |
return segments; | |
} | |
/// <summary> | |
/// Gets the sub segments. | |
/// </summary> | |
/// <param name="contentReference">The content reference.</param> | |
/// <returns>A List of sub route segments.</returns> /// <exception cref="T:System.MemberAccessException">The <see cref="T:System.Lazy`1" /> instance is initialized to use the default constructor of the type that is being lazily initialized, and permissions to access the constructor are missing.</exception> | |
/// <exception cref="T:System.MissingMemberException">The <see cref="T:System.Lazy`1" /> instance is initialized to use the default constructor of the type that is being lazily initialized, and that type does not have a public, parameterless constructor.</exception> | |
/// <exception cref="T:System.InvalidOperationException">The initialization function tries to access <see cref="P:System.Lazy`1.Value" /> on this instance.</exception> | |
public static List<string> GetSubSegments(this ContentReference contentReference) | |
{ | |
ITypeSearch<ProductContent> query = FindClient.Value.Search<ProductContent>() | |
.Filter(x => x.Ancestors().Match(contentReference.ToString())) | |
.FilterOnCurrentMarket() | |
.FilterPriceAvailableForCurrentUser() | |
.FilterOnCurrentSite() | |
.FilterForVisitor(); | |
return query.GetSubSegmentsForQuery(); | |
} | |
/// <summary> | |
/// Gets the sub segments. | |
/// </summary> | |
/// <typeparam name="T">The type to search for.</typeparam> | |
/// <param name="query">The query.</param> | |
/// <returns>A List of sub route segments.</returns> | |
public static List<string> GetSubSegmentsForQuery<T>(this ITypeSearch<T> query) where T : ProductContent | |
{ | |
query = query | |
.TermsFacetFor(x => x.SubCategoryRouteSegments(), facet => facet.Size = 25) | |
.Take(0) | |
.StaticallyCacheFor(new TimeSpan(0, 1, 0, 0)); | |
IContentResult<T> results = query.GetContentResult(3600); | |
List<string> segments = results.TermsFacetFor(x => x.SubCategoryRouteSegments()).Terms | |
.Where(tc => tc.Count > 0).Select(tc => tc.Term).ToList(); | |
return segments; | |
} | |
/// <summary> | |
/// Gets the main category route segments. | |
/// </summary> | |
/// <param name="product">The product.</param> | |
/// <returns>A list of the main category route segments.</returns> | |
/// <exception cref="T:System.MemberAccessException">The <see cref="T:System.Lazy`1" /> instance is initialized to use the default constructor of the type that is being lazily initialized, and permissions to access the constructor are missing.</exception> | |
/// <exception cref="T:System.MissingMemberException">The <see cref="T:System.Lazy`1" /> instance is initialized to use the default constructor of the type that is being lazily initialized, and that type does not have a public, parameterless constructor.</exception> | |
/// <exception cref="T:System.InvalidOperationException">The initialization function tries to access <see cref="P:System.Lazy`1.Value" /> on this instance.</exception> | |
public static List<string> MainCategoryRouteSegments(this ProductContent product) | |
{ | |
List<string> categorieList = new List<string>(); | |
IEnumerable<ContentReference> categories = product.GetCategories(); | |
foreach (ContentReference categorySubNode in categories) | |
{ | |
IEnumerable<NodeContent> mainCategories = ContentLoader.Value.GetAncestors(contentLink: categorySubNode) | |
.OfType<NodeContent>(); | |
categorieList.AddRange(mainCategories.Select(c => c.RouteSegment).ToList()); | |
} | |
return categorieList.Distinct().ToList(); | |
} | |
/// <summary> | |
/// Gets the sub category route segments. | |
/// </summary> | |
/// <param name="product">The product.</param> | |
/// <returns>A list of the sub category route segments.</returns> | |
/// <exception cref="T:System.MemberAccessException">The <see cref="T:System.Lazy`1" /> instance is initialized to use the default constructor of the type that is being lazily initialized, and permissions to access the constructor are missing.</exception> | |
/// <exception cref="T:System.MissingMemberException">The <see cref="T:System.Lazy`1" /> instance is initialized to use the default constructor of the type that is being lazily initialized, and that type does not have a public, parameterless constructor.</exception> | |
/// <exception cref="T:System.InvalidOperationException">The initialization function tries to access <see cref="P:System.Lazy`1.Value" /> on this instance.</exception> | |
public static List<string> SubCategoryRouteSegments(this ProductContent product) | |
{ | |
ReadOnlyCollection<NodeContent> categories = product.GetCategories().ToContent<NodeContent>(); | |
return categories.Select(c => c.RouteSegment).Distinct().ToList(); | |
} | |
/// <summary> | |
/// Convert a ContentReference collection to a list of IContent objects. | |
/// </summary> | |
/// <typeparam name="T">The type of the content items.</typeparam> | |
/// <param name="contentReferenceCollection">The link item collection.</param> | |
/// <returns>A List of IContent objects of type {T}</returns> | |
/// <exception cref="T:System.MemberAccessException">The <see cref="T:System.Lazy`1" /> instance is initialized to use the default constructor of the type that is being lazily initialized, and permissions to access the constructor are missing.</exception> | |
/// <exception cref="T:System.MissingMemberException">The <see cref="T:System.Lazy`1" /> instance is initialized to use the default constructor of the type that is being lazily initialized, and that type does not have a public, parameterless constructor.</exception> | |
/// <exception cref="T:System.InvalidOperationException">The initialization function tries to access <see cref="P:System.Lazy`1.Value" /> on this instance.</exception> | |
public static ReadOnlyCollection<T> ToContent<T>(this IEnumerable<ContentReference> contentReferenceCollection) | |
where T : IContent | |
{ | |
List<T> contentList = new List<T>(); | |
if (contentReferenceCollection == null) | |
{ | |
return new ReadOnlyCollection<T>(list: contentList); | |
} | |
foreach (ContentReference contentReference in contentReferenceCollection) | |
{ | |
T item; | |
if (ContentLoader.Value.TryGet(contentLink: contentReference, content: out item)) | |
{ | |
contentList.Add(item: item); | |
} | |
} | |
return new ReadOnlyCollection<T>(list: contentList); | |
} | |
/// <summary> | |
/// Gets the segments. | |
/// </summary> | |
/// <typeparam name="T">The type to search for.</typeparam> | |
/// <param name="contentReference">The content reference.</param> | |
/// <param name="segmentSelector">The segment selector.</param> | |
/// <returns>A List of route segments.</returns> | |
private static List<string> GetSegments<T>(this ContentReference contentReference, Expression<Func<T, IEnumerable<string>>> segmentSelector) where T : ProductContent | |
{ | |
ITypeSearch<T> query = FindClient.Value.Search<T>() | |
.Filter(x => x.Ancestors().Match(contentReference.ToString())) | |
.FilterOnCurrentMarket() | |
.FilterPriceAvailableForCurrentUser() | |
.FilterOnCurrentSite() | |
.FilterForVisitor(); | |
return query.GetSegmentsForQuery(segmentSelector); | |
} | |
/// <summary> | |
/// Gets the segments. | |
/// </summary> | |
/// <typeparam name="T">The type to search for.</typeparam> | |
/// <param name="query">The query.</param> | |
/// <param name="segmentSelector">The segment selector.</param> | |
/// <returns>A List of route segments.</returns> | |
private static List<string> GetSegmentsForQuery<T>(this ITypeSearch<T> query, Expression<Func<T, IEnumerable<string>>> segmentSelector) where T : ProductContent | |
{ | |
Expression<Func<T, object>> segmentFilter = segmentSelector.ToUntyped(); | |
query = query | |
.TermsFacetFor(fieldSelector: segmentSelector, facetRequestAction: facet => facet.Size = 25) | |
.Take(0) | |
.StaticallyCacheFor(new TimeSpan(0, 1, 0, 0)); | |
IContentResult<T> results = query.GetContentResult(3600); | |
List<string> segments = results.TermsFacetFor(fieldSelector: segmentFilter).Terms | |
.Where(tc => tc.Count > 0) | |
.Select(tc => tc.Term).ToList(); | |
return segments; | |
} | |
/// <summary> | |
/// Convert to an untyped expression. | |
/// </summary> | |
/// <typeparam name="TInput">The type of the input.</typeparam> | |
/// <typeparam name="TOutput">The type of the output.</typeparam> | |
/// <param name="expression">The expression.</param> | |
/// <returns>An Expression{Func{TInput, System.Object}}.</returns> | |
private static Expression<Func<TInput, object>> ToUntyped<TInput, TOutput>(this Expression<Func<TInput, TOutput>> expression) | |
{ | |
// Add the boxing operation, but get a weakly typed expression | |
Expression converted = Expression.Convert(expression.Body, typeof(object)); | |
// Use Expression.Lambda to get back to strong typing | |
return Expression.Lambda<Func<TInput, object>>(converted, expression.Parameters); | |
} | |
} |