AI is correct. All item fields on the paragraphs of the page is merged into one big blob of things to search.
I just gave my codex the existing contentindexbuilder and asked it to make a new version that handles the item fields individually. Code below in a ContentItemIndexBuilder- it is not at all tested. You can add it as custom and use that to build an index.
This version does exactly what you asked:
-
Page document is the single place where page/row/paragraph item fields end up.
-
Repeated fields are merged (scalar → becomes multi-valued when repeated; arrays append + de-dupe).
-
Adds PageItemIds, PagePropertyItemIds, RowItemIds, ParagraphItemIds (as string[] values like ItemType:ItemId).
-
Keeps existing ParagraphHeaders[] and ParagraphTexts[].
-
Does not keep any “ParagraphContent merged blob” field.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Text;
using Dynamicweb.Content.Items;
using Dynamicweb.Content.Items.Editors;
using Dynamicweb.Content.Items.Metadata;
using Dynamicweb.Core;
using Dynamicweb.Core.Encoders;
using Dynamicweb.Core.Helpers;
using Dynamicweb.Data;
using Dynamicweb.Diagnostics.Tasks;
using Dynamicweb.Diagnostics.Tracking;
using Dynamicweb.Extensibility.AddIns;
using Dynamicweb.Extensibility.Editors;
using Dynamicweb.Indexing;
using Dynamicweb.Indexing.Schemas;
using Dynamicweb.Logging;
using Dynamicweb.Logging.LogEvents;
namespace Dynamicweb.Content
{
/// <summary>
/// ContentItemIndexBuilder implements an <seealso cref="IndexBuilderBase" /> that indexes Pages, paragraphs and items to the index.
/// Stores item fields individually on the PAGE document and merges repeated fields.
/// </summary>
[AddInName("Content Item Index Builder")]
public class ContentItemIndexBuilder : IndexBuilderBase
{
private const string AddInParameterGroupName = "Content Item Index Builder Settings";
[AddInParameterGroup(AddInParameterGroupName), AddInParameter("AppsToHandle"), AddInLabel("Apps to handle"),
AddInParameterEditor(typeof(TextParameterEditor), "infoText=Defaults to all; Explanation=Allows specification of ContentAppIndexProviders via a comma-separated list;")]
public string AppsToHandle
{
get => GetString("AppsToHandle");
set => SetValue("AppsToHandle", value);
}
[AddInParameterGroup(AddInParameterGroupName), AddInParameter("ExcludeItemsFromIndex"), AddInLabel("Exclude Items from Index"),
AddInParameterEditor(typeof(YesNoParameterEditor), "Explanation=Excludes item content from the index;")]
public bool ExcludeItemsFromIndex
{
get => GetBoolean("ExcludeItemsFromIndex");
set => SetValue("ExcludeItemsFromIndex", value);
}
[AddInParameterGroup(AddInParameterGroupName), AddInParameter("SkipPageItemRelationLists"), AddInLabel("Skip page Item relation lists"),
AddInParameterEditor(typeof(YesNoParameterEditor), "")]
public bool SkipPageItemRelationLists
{
get => GetBoolean("SkipPageItemRelationLists");
set => SetValue("SkipPageItemRelationLists", value);
}
[AddInParameterGroup(AddInParameterGroupName), AddInParameter("SkipRowItemRelationLists"), AddInLabel("Skip row Item relation lists"),
AddInParameterEditor(typeof(YesNoParameterEditor), "")]
public bool SkipRowItemRelationLists
{
get => GetBoolean("SkipRowItemRelationLists");
set => SetValue("SkipRowItemRelationLists", value);
}
[AddInParameterGroup(AddInParameterGroupName), AddInParameter("SkipParagraphItemRelationLists"), AddInLabel("Skip paragraph Item relation lists"),
AddInParameterEditor(typeof(YesNoParameterEditor), "")]
public bool SkipParagraphItemRelationLists
{
get => GetBoolean("SkipParagraphItemRelationLists");
set => SetValue("SkipParagraphItemRelationLists", value);
}
[AddInParameterGroup(AddInParameterGroupName), AddInParameter("StripHtml"), AddInLabel("Remove html markup from text content"),
AddInParameterEditor(typeof(YesNoParameterEditor), "")]
public bool StripHtml
{
get => GetBoolean("StripHtml");
set => SetValue("StripHtml", value);
}
private Dictionary<int, DataRow> Pages { get; set; } = [];
private Dictionary<int, List<DataRow>> PageParagraphs { get; set; } = [];
private readonly Dictionary<string, ItemField> ItemTypeFields = [];
private readonly HashSet<string> ItemRelationListFields = [];
private readonly Dictionary<string, Type> ItemArrayFields = [];
private readonly HashSet<string> ItemExcludedFields = [];
private readonly ValueConverter ValueConverter = new();
private readonly Lazy<IEnumerable<ContentAppIndexProvider>> _allAppProviders = new(ContentAppIndexProvider.GetAll);
private void InitializeItemFields()
{
ItemTypeFields.Clear();
ItemRelationListFields.Clear();
ItemArrayFields.Clear();
ItemExcludedFields.Clear();
var itemTypeEditor = typeof(ItemTypeEditor);
var relationListTypeEditor = typeof(ItemRelationListEditor);
var itemTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var area in Services.Areas.GetAreas())
if (!string.IsNullOrEmpty(area.ItemTypePageProperty))
itemTypes.Add(area.ItemTypePageProperty);
foreach (var page in Services.Pages.GetPages())
if (!string.IsNullOrEmpty(page.ItemType))
itemTypes.Add(page.ItemType);
foreach (var paragraph in Services.Paragraphs.GetParagraphs())
if (!string.IsNullOrEmpty(paragraph.ItemType))
itemTypes.Add(paragraph.ItemType);
foreach (var row in Services.Grids.GetGridRows())
if (!string.IsNullOrEmpty(row.ItemType))
itemTypes.Add(row.ItemType);
foreach (string itemType in itemTypes)
{
var metadata = ItemManager.Metadata.GetItemType(itemType);
if (metadata is null)
continue;
var fields = ItemManager.Metadata.GetItemFields(metadata);
if (fields is null)
continue;
foreach (ItemField fieldMetadata in fields)
{
var fieldKey = GetItemFieldKey(itemType, fieldMetadata.SystemName);
if (fieldMetadata.ExcludeFromSearch)
{
ItemExcludedFields.Add(fieldKey);
continue;
}
if (fieldMetadata.IsEditor(relationListTypeEditor))
ItemRelationListFields.Add(fieldKey);
else if (fieldMetadata.IsEditor(itemTypeEditor, true))
ItemTypeFields.Add(fieldKey, fieldMetadata);
else if (fieldMetadata.UnderlyingType.IsGenericType &&
fieldMetadata.UnderlyingType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
ItemArrayFields.Add(fieldKey, fieldMetadata.UnderlyingType);
}
}
}
private static string GetItemFieldKey(string itemType, string fieldName) => $"[{itemType}].[{fieldName}]";
public override void Build(IIndexWriter writer, Tracker tracker)
{
ArgumentNullException.ThrowIfNull(writer);
ArgumentNullException.ThrowIfNull(tracker);
if (TaskManager.Context is null)
throw new InvalidOperationException("TaskManager.Context is null");
writer.Open(false);
using var conn = new SqlConnection(TaskManager.Context["Database.ConnectionString"].ToString());
conn.Open();
TrackSettings(tracker);
LoadPages(conn);
LoadParagraphs(conn);
InitializeItemFields();
BuildContent(writer, tracker);
}
private void TrackSettings(Tracker tracker)
{
if (tracker is null) return;
tracker.LogInformation("AppsToHandle: '{0}'", AppsToHandle);
tracker.LogInformation("ExcludeItemsFromIndex: '{0}'", ExcludeItemsFromIndex);
tracker.LogInformation("SkipPageItemRelationLists: '{0}'", SkipPageItemRelationLists);
tracker.LogInformation("SkipRowItemRelationLists: '{0}'", SkipRowItemRelationLists);
tracker.LogInformation("SkipParagraphItemRelationLists: '{0}'", SkipParagraphItemRelationLists);
tracker.LogInformation("StripHtml: '{0}'", StripHtml);
}
private void LoadPages(SqlConnection connection)
{
var pages = new Dictionary<int, DataRow>();
var cb = CommandBuilder.Create(
"SELECT [Page].* FROM Page INNER JOIN Area on [Page].PageAreaID = Area.AreaID WHERE Area.AreaActive = 1 AND Area.AreaDeleted = 0 AND Page.PageDeleted = 0 AND Page.PageHidden = 0");
using (var ds = Database.CreateDataSet(cb, connection))
{
foreach (DataRow row in ds.Tables[0].Rows)
{
int pageId = (int)row["PageID"];
pages.Add(pageId, row);
}
}
Pages = pages;
}
private void LoadParagraphs(SqlConnection connection)
{
var pageParagraphs = new Dictionary<int, List<DataRow>>();
var cb = CommandBuilder.Create(
@"SELECT p.ParagraphID, p.ParagraphPageID, p.ParagraphModuleSystemName, p.ParagraphHeader, p.ParagraphText, p.ParagraphModuleSettings, p.ParagraphItemType, p.ParagraphItemId
FROM Paragraph p
LEFT JOIN GridRow rows ON p.ParagraphGridRowId = rows.GridRowId
WHERE
p.ParagraphShowParagraph = 1
AND p.ParagraphDeleted = 0
AND (p.ParagraphGridRowId = 0 OR (
rows.GridRowActive = 1
AND rows.GridRowDeleted = 0
AND (rows.GridRowValidFrom IS NULL OR rows.GridRowValidFrom <= {0})
AND (rows.GridRowValidTo IS NULL OR rows.GridRowValidTo > {0})))
AND (( {0} BETWEEN p.[ParagraphValidFrom] AND p.[ParagraphValidTo] ) OR ( p.[ParagraphValidTo] IS NULL ))",
DateTime.Now);
using (var ds = Database.CreateDataSet(cb, connection))
{
foreach (DataRow row in ds.Tables[0].Rows)
{
int pageId = (int)row["ParagraphPageID"];
if (!pageParagraphs.TryGetValue(pageId, out var paragraphs))
{
paragraphs = [];
pageParagraphs.Add(pageId, paragraphs);
}
paragraphs.Add(row);
}
}
PageParagraphs = pageParagraphs;
}
private void BuildContent(IIndexWriter writer, Tracker tracker)
{
tracker.Status.TotalCount = Pages.Count;
var fieldsIdsForFacets = new Dictionary<string, FieldDefinition>(StringComparer.OrdinalIgnoreCase)
{
{ ContentIndexSchemaExtender.PageItemIdStringFieldDefinition.Source, ContentIndexSchemaExtender.PageItemIdStringFieldDefinition },
{ ContentIndexSchemaExtender.PageIdStringFieldDifenition.Source, ContentIndexSchemaExtender.PageIdStringFieldDifenition },
{ ContentIndexSchemaExtender.PageParentIdStringFieldDefinition.Source, ContentIndexSchemaExtender.PageParentIdStringFieldDefinition },
{ ContentIndexSchemaExtender.PageMasterPageIdStringFieldDefinition.Source, ContentIndexSchemaExtender.PageMasterPageIdStringFieldDefinition },
{ ContentIndexSchemaExtender.PageAreaIdStringFieldDefinition.Source, ContentIndexSchemaExtender.PageAreaIdStringFieldDefinition }
};
foreach (var pageRow in Pages.Values)
{
var document = new IndexDocument();
int pageId = 0;
if (pageRow is not null)
{
foreach (DataColumn col in pageRow.Table.Columns)
{
var value = pageRow[col];
if (col.ColumnName.Equals("PageID", StringComparison.OrdinalIgnoreCase))
pageId = (int)value;
if (IsNullOrDbNull(value))
continue;
if (fieldsIdsForFacets.TryGetValue(col.ColumnName, out FieldDefinition? facetField))
AddOrMerge(document, facetField.SystemName, value);
// Keep original raw columns as-is (but merge-safe anyway)
AddOrMerge(document, col.ColumnName, value);
}
if (!ExcludeItemsFromIndex)
{
HandlePageItems(pageRow, document, tracker); // now merges
HandleRowItems(pageId, document); // now adds RowItemIds + Row_* fields
}
HandlePagePath(pageId, document, pageRow);
HandleParagraphs(pageId, document, writer, tracker); // now adds ParagraphItemIds + Paragraph_* fields + header/text arrays
}
WriteDocument(document, pageId, writer, tracker);
}
}
private void HandlePagePath(int pageId, IndexDocument document, DataRow pageRow)
{
if (pageRow == null) return;
var pagePath = new List<int> { pageId };
if (pageRow["PageParentPageId"] is int pageParentId)
{
while (Pages.TryGetValue(pageParentId, out var parentRow))
{
pagePath.Insert(0, pageParentId);
pageParentId = Converter.ToInt32(parentRow["PageParentPageId"]);
}
}
AddOrMerge(document, "PagePath", pagePath.ToArray());
}
private void WriteDocument(IndexDocument document, int pageId, IIndexWriter writer, Tracker tracker)
{
foreach (var extender in Extenders)
extender.ExtendDocument(document);
writer.AddDocument(document);
tracker.Status.Meta["CurrentPageID"] = Converter.ToString(pageId);
tracker.IncrementCounter();
}
private void HandlePageItems(DataRow pageRow, IndexDocument document, Tracker tracker)
{
// Track item IDs/types on the document
var pageItemIds = new List<string>();
var pagePropertyItemIds = new List<string>();
// PropertyItemID case
if (Converter.ToInt32(pageRow["PagePropertyItemId"]) > 0)
{
var area = Services.Areas.GetArea(Converter.ToInt32(pageRow["PageAreaID"]));
if (area is not null && !string.IsNullOrEmpty(area.ItemTypePageProperty))
{
var itemType = area.ItemTypePageProperty;
var itemId = Converter.ToString(pageRow["PagePropertyItemId"]);
pagePropertyItemIds.Add($"{itemType}:{itemId}");
_ = HandlePageItem(
pageId: Converter.ToInt32(pageRow["PageID"]),
itemType: itemType,
itemId: itemId,
document: document,
isPropertyItem: true,
tracker: tracker);
}
}
// Normal page item
if (!string.IsNullOrEmpty(Converter.ToString(pageRow["PageItemType"])))
{
var itemType = Converter.ToString(pageRow["PageItemType"]);
var itemId = Converter.ToString(pageRow["PageItemID"]);
pageItemIds.Add($"{itemType}:{itemId}");
_ = HandlePageItem(
pageId: Converter.ToInt32(pageRow["PageID"]),
itemType: itemType,
itemId: itemId,
document: document,
isPropertyItem: false,
tracker: tracker);
}
if (pageItemIds.Count > 0)
AddOrMerge(document, "PageItemIds", pageItemIds.ToArray());
if (pagePropertyItemIds.Count > 0)
AddOrMerge(document, "PagePropertyItemIds", pagePropertyItemIds.ToArray());
}
private string HandlePageItem(int pageId, string itemType, string itemId, IndexDocument document, bool isPropertyItem, Tracker tracker)
{
Item? item = null;
try
{
if (isPropertyItem)
{
item = ItemManager.Storage.GetById(itemType, itemId);
}
else if (pageId > 0)
{
item = Services.Pages.GetPage(pageId)?.Item;
}
}
catch (SqlException ex)
{
tracker.LogException($"Error processing page item fields (Page ID: {pageId}, Page item ID: {itemId}, Page item type: {itemType}). Exception logged", ex);
}
if (item is null)
return string.Empty;
if (isPropertyItem)
AddOrMerge(document, "PagePropertyItemType", itemType);
// Merge fields into page document
foreach (string name in item.Names)
{
var value = item[name];
if (IsNullOrDbNull(value))
continue;
var key = isPropertyItem
? string.Format(CultureInfo.InvariantCulture, "Property_{0}_{1}", itemType, name)
: string.Format(CultureInfo.InvariantCulture, "{0}_{1}", itemType, name);
var itemFieldKey = GetItemFieldKey(itemType, name);
if (ItemArrayFields.TryGetValue(itemFieldKey, out Type? typeOfCollection))
{
if (TryConvertCollectionValue(value, typeOfCollection, out var arrValue) && arrValue is not null)
AddOrMerge(document, key, arrValue);
else
AddOrMerge(document, key, value);
}
else
{
AddOrMerge(document, key, value);
}
}
// Keep return value for compatibility (not used as merged blob here)
return GetItemContent(item, SkipPageItemRelationLists);
}
private void HandleParagraphs(int pageId, IndexDocument document, IIndexWriter writer, Tracker tracker)
{
var paragraphHeaders = new List<string>();
var paragraphTexts = new List<string>();
var paragraphItemIds = new List<string>();
if (!PageParagraphs.TryGetValue(pageId, out var paragraphs))
return;
Exception? firstException = null;
var errorMessages = new List<string>();
foreach (DataRow paragraphRow in paragraphs)
{
// App providers may emit separate docs (kept as-is)
string sysName = Converter.ToString(paragraphRow["ParagraphModuleSystemName"]);
if (!string.IsNullOrEmpty(sysName) && (string.IsNullOrEmpty(AppsToHandle) || AppsToHandle.Split([',', ';']).Contains(sysName)))
{
var paragraphAppIndexProvider = _allAppProviders.Value.FirstOrDefault(appIndexProvider => appIndexProvider.CanHandle(sysName));
if (paragraphAppIndexProvider is not null)
{
IEnumerable<IndexDocument> indexDocs = paragraphAppIndexProvider.GetDocuments(pageId, document, paragraphRow).ToList();
tracker.Status.TotalCount = tracker.Status.TotalCount + indexDocs.Count();
foreach (var doc in indexDocs)
WriteDocument(doc, pageId, writer, tracker);
}
}
// Header/text arrays (kept)
var paragraphHeader = Converter.ToString(paragraphRow["ParagraphHeader"]);
if (!string.IsNullOrEmpty(paragraphHeader))
paragraphHeaders.Add(paragraphHeader);
var paragraphText = Converter.ToString(paragraphRow["ParagraphText"]);
if (!string.IsNullOrEmpty(paragraphText))
{
paragraphText = HtmlEncoder.HtmlDecode(paragraphText);
if (StripHtml)
paragraphText = StringHelper.StripHtml(paragraphText);
paragraphTexts.Add(paragraphText);
}
// Paragraph item fields individually (NO merged ParagraphContent blob)
if (!ExcludeItemsFromIndex)
{
string itemType = Converter.ToString(paragraphRow["ParagraphItemType"]);
if (!string.IsNullOrEmpty(itemType))
{
int paragraphId = Converter.ToInt32(paragraphRow["ParagraphID"]);
try
{
var paragraph = Services.Paragraphs.GetParagraph(paragraphId)
?? throw new InvalidOperationException($"Paragraph with ID {paragraphId} not found");
var item = paragraph.Item;
if (item is not null)
{
var itemId = Converter.ToString(paragraphRow["ParagraphItemID"]);
if (!string.IsNullOrEmpty(itemId) && itemId != "0")
paragraphItemIds.Add($"{itemType}:{itemId}");
AddItemFieldsToDocument(document, item, prefix: "Paragraph_", skipRelationLists: SkipParagraphItemRelationLists);
}
}
catch (Exception ex) when (ex is InvalidOperationException || ex is SqlException)
{
firstException ??= ex;
errorMessages.Add($"Error processing paragraph (Paragraph ID: '{paragraphRow["ParagraphID"]}', Paragraph item type: '{paragraphRow["ParagraphItemType"]}', Paragraph item ID: '{paragraphRow["ParagraphItemID"]}')");
}
}
}
}
if (firstException is not null)
{
LogEventManager.Current.LogErrors(LogCategory.Health, "IndexBuild", $"Error processing paragraph on page {pageId}", errorMessages, firstException);
tracker.LogException($"Error processing paragraphs on page {pageId}", firstException);
}
AddOrMerge(document, "ParagraphHeaders", paragraphHeaders.ToArray());
AddOrMerge(document, "ParagraphTexts", paragraphTexts.ToArray());
if (paragraphItemIds.Count > 0)
AddOrMerge(document, "ParagraphItemIds", paragraphItemIds.ToArray());
}
private void HandleRowItems(int pageId, IndexDocument document)
{
var rowItemIds = new List<string>();
foreach (var row in Services.Grids.GetGridRowsByPageId(pageId))
{
if (string.IsNullOrEmpty(row.ItemType) || string.IsNullOrEmpty(row.ItemId))
continue;
var item = ItemManager.Storage.GetById(row.ItemType, row.ItemId);
if (item is null)
continue;
rowItemIds.Add($"{row.ItemType}:{row.ItemId}");
AddItemFieldsToDocument(document, item, prefix: "Row_", skipRelationLists: SkipRowItemRelationLists);
}
if (rowItemIds.Count > 0)
AddOrMerge(document, "RowItemIds", rowItemIds.ToArray());
}
private void AddItemFieldsToDocument(IndexDocument document, Item item, string prefix, bool skipRelationLists)
{
foreach (var name in item.Names)
{
var raw = item[name];
if (IsNullOrDbNull(raw))
continue;
var fieldKey = GetItemFieldKey(item.SystemName, name);
// Respect ExcludeFromSearch and your derived exclusions
if (ItemExcludedFields.Contains(fieldKey))
continue;
// If relation list fields should be skipped, skip them here (we don't flatten relation list items into fields)
if (skipRelationLists && ItemRelationListFields.Contains(fieldKey))
continue;
var indexFieldName = $"{prefix}{item.SystemName}_{name}";
if (ItemArrayFields.TryGetValue(fieldKey, out var collectionType))
{
if (TryConvertCollectionValue(raw, collectionType, out var converted) && converted is not null)
AddOrMerge(document, indexFieldName, converted);
else
AddOrMerge(document, indexFieldName, raw);
}
else
{
AddOrMerge(document, indexFieldName, raw);
}
}
}
private static bool IsNullOrDbNull(object? v) => v is null || Convert.IsDBNull(v);
private static IEnumerable<object> ToObjectEnumerable(object value)
{
// Treat string as scalar, not IEnumerable<char>
if (value is string) return new object[] { value };
if (value is IEnumerable enumerable)
{
var list = new List<object>();
foreach (var x in enumerable)
{
if (!IsNullOrDbNull(x)) list.Add(x!);
}
return list;
}
return new object[] { value };
}
private static object? MergeValues(object? existing, object incoming)
{
if (IsNullOrDbNull(incoming))
return existing;
if (existing is null)
return incoming;
var left = ToObjectEnumerable(existing);
var right = ToObjectEnumerable(incoming);
var merged = left.Concat(right)
.Where(x => x is not null && !IsNullOrDbNull(x))
.Distinct(new LooseEqualityComparer())
.ToList();
if (merged.Count == 0)
return null;
// If the value repeats, keep multi-valued.
if (merged.Count == 1)
return merged[0];
// Try to keep typed arrays where it’s unambiguous; otherwise object[]
var elementType = merged.Select(x => x.GetType()).Distinct().SingleOrDefault();
if (elementType == typeof(string))
return merged.Cast<string>().ToArray();
if (elementType == typeof(int))
return merged.Cast<int>().ToArray();
return merged.ToArray();
}
private sealed class LooseEqualityComparer : IEqualityComparer<object>
{
public new bool Equals(object? x, object? y)
{
if (ReferenceEquals(x, y)) return true;
if (x is null || y is null) return false;
if (x is string xs && y is string ys)
return string.Equals(xs, ys, StringComparison.OrdinalIgnoreCase);
return x.Equals(y);
}
public int GetHashCode(object obj)
{
if (obj is string s)
return StringComparer.OrdinalIgnoreCase.GetHashCode(s);
return obj.GetHashCode();
}
}
private static void AddOrMerge(IndexDocument doc, string field, object value)
{
if (IsNullOrDbNull(value))
return;
if (!doc.ContainsKey(field))
{
doc.Add(field, value);
return;
}
var existing = doc[field];
var merged = MergeValues(existing, value);
if (merged is not null)
doc[field] = merged;
}
private bool TryConvertCollectionValue(object? value, Type collectionType, [NotNullWhen(true)] out object? result)
{
result = null;
if (!TryConvertCollectionValue(value, collectionType, [typeof(int), typeof(string)], out var convertedCollection))
return false;
if (collectionType.IsAssignableFrom(convertedCollection.GetType()))
{
result = convertedCollection;
return true;
}
try
{
result = ValueConverter.Convert(convertedCollection, collectionType);
return result is not null;
}
catch
{
return false;
}
}
private bool TryConvertCollectionValue(object? value, Type collectionType, List<Type> valueTypesToTry, [NotNullWhen(true)] out object? result)
{
try
{
result = ValueConverter.Convert(value, collectionType);
return result is not null;
}
catch
{
// Ignore and retry with alternative element type
}
var isGenericCollectionType = collectionType.IsGenericType && !collectionType.IsGenericTypeDefinition;
if (!isGenericCollectionType)
{
result = null;
return false;
}
if (valueTypesToTry.Count == 0)
{
result = null;
return false;
}
var valueType = collectionType.GetGenericArguments().FirstOrDefault();
if (valueType is not null)
valueTypesToTry.Remove(valueType);
else
{
result = null;
return false;
}
var typeDefinition = collectionType.GetGenericTypeDefinition();
collectionType = typeDefinition.MakeGenericType(valueTypesToTry.First());
return TryConvertCollectionValue(value, collectionType, valueTypesToTry, out result);
}
private string GetItemContent(Item item, bool skipItemRelationLists)
{
var output = new StringBuilder();
GetItemContent(output, item, skipItemRelationLists, 0);
var content = HtmlEncoder.HtmlDecode(output.ToString());
if (StripHtml)
content = StringHelper.StripHtml(content);
return content;
}
private void GetItemContent(StringBuilder stringBuilder, Item item, bool skipItemRelationLists, int depth)
{
if (depth > 2)
return;
foreach (string name in item.Names)
{
string value = Converter.ToString(item[name]);
if (string.IsNullOrEmpty(value))
continue;
var fieldKey = GetItemFieldKey(item.SystemName, name);
if (!skipItemRelationLists && ItemRelationListFields.Contains(fieldKey))
{
GetItemRelationListContent(stringBuilder, Converter.ToInt32(value), depth + 1);
}
else if (ItemTypeFields.TryGetValue(fieldKey, out var fieldMetaData))
{
var editor = fieldMetaData.CreateEditorInstance(false) as ItemTypeEditor;
if (!string.IsNullOrEmpty(editor?.ItemType))
{
var fieldItem = ItemManager.Storage.GetById(editor.ItemType, value);
if (fieldItem is not null)
GetItemContent(stringBuilder, fieldItem, skipItemRelationLists, depth);
}
}
else if (!ItemExcludedFields.Contains(fieldKey))
{
stringBuilder.Append(value);
stringBuilder.Append(' ');
}
}
}
private void GetItemRelationListContent(StringBuilder stringBuilder, int itemListId, int depth)
{
var itemList = ItemList.GetItemListById(itemListId);
if (itemList is not null)
{
foreach (Item item in itemList.Relations.Cast<Item>())
GetItemContent(stringBuilder, item, false, depth);
}
}
public override IEnumerable<FieldDefinitionBase> GetFields()
{
var extender = new ContentIndexSchemaExtender();
return extender.GetFields();
}
public override IEnumerable<string> SupportedActions => new[] { "Full" };
public override IDictionary<string, object> DefaultSettings => new Dictionary<string, object>()
{
{ "ExcludeItemsFromIndex", false },
{ "SkipPageItemRelationLists", false },
{ "SkipRowItemRelationLists", false },
{ "SkipParagraphItemRelationLists", false },
{ "StripHtml", false },
{ "AppsToHandle", string.Empty }
};
}
}