<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title type="text">Blog posts by Ashish Rasal</title><link href="http://world.optimizely.com" /><updated>2024-11-07T06:19:16.0000000Z</updated><id>https://world.optimizely.com/blogs/ashish-rasal/</id> <generator uri="http://world.optimizely.com" version="2.0">Optimizely World</generator> <entry><title>ExcludeDeleted(): Prevent Trashed Content from Appearing in Search Results</title><link href="https://world.optimizely.com/blogs/ashish-rasal/dates/2024/11/excludedeleted-prevent-trashed-content-from-appearing-in-search-results/" /><id>&lt;h3&gt;Introduction&lt;/h3&gt;
&lt;p&gt;In Optimizely CMS, content that is moved to the trash can still appear in search results if it&amp;rsquo;s not explicitly excluded using the &lt;code&gt;IsDeleted&lt;/code&gt; property on &lt;code&gt;IContent&lt;/code&gt;. This behavior can clutter search results and lead to a confusing user experience, especially if the &quot;deleted&quot; content remains accessible when it shouldn&#39;t be. Unfortunately, Optimizely&amp;rsquo;s documentation lacks a straightforward way to filter out trashed content, so we developed a custom solution that excludes trashed items from search results.&lt;/p&gt;
&lt;p&gt;This solution leverages Optimizely Content Events to detect when content is moved to or from the trash and applies an &lt;code&gt;IsDeleted&lt;/code&gt; flag to custom page templates that inherit from &lt;code&gt;SitePageData&lt;/code&gt;, our project&amp;rsquo;s base page class. By using these events, we ensure real-time updates to the search index, providing a consistent and clear search experience for users.&lt;/p&gt;
&lt;h3&gt;Overview of Optimizely Content Events&lt;/h3&gt;
&lt;p&gt;Optimizely Content Events are essential for managing content lifecycle actions such as creation, updates, movement, and deletion. With these events, developers can trigger specific actions in response to content changes, enabling them to integrate with other systems or handle special tasks for particular content types.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s a quick overview of common content events in Optimizely:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CreatedContent&lt;/strong&gt;: Fired when new content is created.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PublishedContent&lt;/strong&gt;: Triggered when content is published.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DeletedContent&lt;/strong&gt;: Fires when content is permanently removed from the CMS.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MovedContent&lt;/strong&gt;: Activated when content is moved to a new location or to the trash.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SavingContent&lt;/strong&gt;: Occurs before content is saved, often used for validation or setting default values.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For a more in-depth look at Content Events, check out &lt;a href=&quot;/link/a0f73c83f9874dd5aeec4bf69739c27c.aspx&quot;&gt;&lt;span&gt;Daniel&lt;/span&gt;&lt;span&gt; Ovaska&amp;rsquo;s&lt;/span&gt;&lt;span&gt; blog&lt;/span&gt;&lt;span&gt; on&lt;/span&gt;&lt;span&gt; Content&lt;/span&gt;&lt;span&gt; Events&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Solution Overview&lt;/h3&gt;
&lt;p&gt;Our custom solution involves setting up an &lt;code&gt;OnMovedContentToTrash&lt;/code&gt; event handler that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Detects when content is moved to or from the trash.&lt;/li&gt;
&lt;li&gt;Sets an &lt;code&gt;IsDeleted&lt;/code&gt; flag on custom page types that inherit from &lt;code&gt;SitePageData&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Updates the content index in Optimizely Find to keep search results accurate.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We&amp;rsquo;ll break down each part of this solution below, including a reusable method that dynamically identifies all custom page types derived from &lt;code&gt;SitePageData&lt;/code&gt; and efficiently applies the &lt;code&gt;IsDeleted&lt;/code&gt; flag.&lt;/p&gt;
&lt;h3&gt;Implementation&lt;/h3&gt;
&lt;h4&gt;Step 1: Setting up the &lt;code&gt;OnMovedContentToTrash&lt;/code&gt; Event Handler&lt;/h4&gt;
&lt;p&gt;In our &lt;code&gt;OnMovedContentToTrash&lt;/code&gt; method, we monitor if content has been moved to or from the trash. Based on its location, we set the &lt;code&gt;IsDeleted&lt;/code&gt; flag to true (if it&amp;rsquo;s in the trash) or false (if it&amp;rsquo;s restored). This updated information is saved in the CMS and re-indexed in Optimizely Find to ensure search results reflect the current content state.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h4&gt;Step 2: Creating the Processor Action for Each Page Type&lt;/h4&gt;
&lt;p&gt;To apply &lt;code&gt;IsDeleted&lt;/code&gt; on each page type without reflection at runtime, we use an expression tree to create an action for each type. This approach avoids runtime performance overhead and prevents potential issues with reflection.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h4&gt;Step 3: Defining the &lt;code&gt;ProcessContent&lt;/code&gt; Method&lt;/h4&gt;
&lt;p&gt;This method applies the &lt;code&gt;IsDeleted&lt;/code&gt; flag to the specific content type and re-indexes it in Optimizely Find to ensure that it appears or disappears from search results based on its status.&lt;/p&gt;
&lt;p&gt;Here&#39;s the code for the above steps:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;
[InitializableModule]
[ModuleDependency(typeof(EPiServer.Web.InitializationModule), typeof(EPiServer.Commerce.Initialization.InitializationModule))]
public class ContentEventInitialization : IInitializableModule
{

	private IClient _findClient { get; set; }

	private Injected&amp;lt;ICmsContentService&amp;gt; _cmsContentService;
	private IContentTypeRepository _contentTypeRepository { get; set; }

	private static readonly Injected&amp;lt;IHttpContextAccessor&amp;gt; _httpContextAccessor;

	private static readonly Injected&amp;lt;IConfiguration&amp;gt; _configuration;
	private static readonly Injected&amp;lt;IWebHostEnvironment&amp;gt; _environment;
	private static readonly ILogger _logger = LogManager.GetLogger(typeof(ContentEventInitialization));

	public void Initialize(InitializationEngine context)
	{
		_findClient = SearchClient.Instance;
		_contentTypeRepository = context.Locate.Advanced.GetInstance&amp;lt;IContentTypeRepository&amp;gt;();

		var contentEventsService = _contentEvents.Service;

		contentEventsService.MovedContent += OnMovedContentToTrash;
		contentEventsService.PublishedContent += OnContentPublished;
		contentEventsService.CheckedInContent += OnCheckedInContent;
	}

	public void Uninitialize(InitializationEngine context)
	{
		var contentEventsService = _contentEvents.Service;

		contentEventsService.MovedContent -= OnMovedContentToTrash;
		contentEventsService.PublishedContent -= OnContentPublished;
		contentEventsService.CheckedInContent -= OnCheckedInContent;
	}

	/// &amp;lt;summary&amp;gt;
	/// Handles the event when content is moved to or from the trash. Updates the IsDeleted property
	/// of specific custom page types and re-indexes the content in Optimizely Find.
	/// &amp;lt;/summary&amp;gt;
	/// &amp;lt;param name=&quot;sender&quot;&amp;gt;The event sender.&amp;lt;/param&amp;gt;
	/// &amp;lt;param name=&quot;e&quot;&amp;gt;The content event arguments containing details about the moved content.&amp;lt;/param&amp;gt;
	private void OnMovedContentToTrash(object sender, ContentEventArgs e)
	{
		_logger.Information($&quot;Moved content to trash fired for content {e.ContentLink.ID}&quot;);

		if (e is MoveContentEventArgs eventArgs)
		{
			var contentRepository = ServiceLocator.Current.GetInstance&amp;lt;IContentRepository&amp;gt;();

			// Dictionary to store processors for each custom page type
			var contentProcessors = new Dictionary&amp;lt;Type, Action&amp;lt;IContent, bool&amp;gt;&amp;gt;();
			var sitePageDataType = typeof(SitePageData);

			// Get all types that inherit from SitePageData in the current assembly
			var pageTypes = _contentTypeRepository
				.List()
				.Where(x =&amp;gt; x.ModelType != null)
				.Select(x =&amp;gt; x.ModelType)
				.Where(x =&amp;gt; sitePageDataType.IsAssignableFrom(x) &amp;amp;&amp;amp; x.IsClass &amp;amp;&amp;amp; !x.IsAbstract);

			// Populate the dictionary with actions for each page type
			foreach (var pageType in pageTypes)
			{
				var processor = CreateProcessorAction(pageType);
				contentProcessors[pageType] = processor;
			}

			// Determine if content is moved to or from the trash, setting IsDeleted accordingly
			bool isDeleted = eventArgs.TargetLink.ID == ContentReference.WasteBasket.ID;
			bool isRestored = eventArgs.OriginalParent.ID == ContentReference.WasteBasket.ID;

			if (isDeleted || isRestored)
			{
				var contentType = e.Content.GetType().BaseType;
				if (contentProcessors.TryGetValue(contentType, out var processor))
				{
					processor.Invoke(e.Content, isDeleted);
				}
			}
		}
	}

	/// &amp;lt;summary&amp;gt;
	/// Creates a strongly-typed action for processing a specific page type when content is moved
	/// to or from the trash. This avoids the need for reflection at runtime.
	/// &amp;lt;/summary&amp;gt;
	/// &amp;lt;param name=&quot;pageType&quot;&amp;gt;The type of the page for which to create the processor action.&amp;lt;/param&amp;gt;
	/// &amp;lt;returns&amp;gt;An action that processes the page type when content is moved to or from the trash.&amp;lt;/returns&amp;gt;
	private Action&amp;lt;IContent, bool&amp;gt; CreateProcessorAction(Type pageType)
	{
		// Define parameters for the lambda: (IContent content, bool isDeleted)
		var contentParam = Expression.Parameter(typeof(IContent), &quot;content&quot;);
		var isDeletedParam = Expression.Parameter(typeof(bool), &quot;isDeleted&quot;);

		// Cast content to the specific page type
		var castContent = Expression.Convert(contentParam, pageType);

		// Create method call for ProcessContent&amp;lt;T&amp;gt; using the specific page type
		var method = typeof(ContentEventInitialization)
			.GetMethod(nameof(ProcessContent), BindingFlags.NonPublic | BindingFlags.Instance)
			.MakeGenericMethod(pageType);

		// Call ProcessContent&amp;lt;T&amp;gt;((T)content, isDeleted)
		var body = Expression.Call(Expression.Constant(this), method, castContent, isDeletedParam);

		// Compile into a lambda expression: (IContent content, bool isDeleted) =&amp;gt; ProcessContent&amp;lt;T&amp;gt;((T)content, isDeleted)
		var lambda = Expression.Lambda&amp;lt;Action&amp;lt;IContent, bool&amp;gt;&amp;gt;(body, contentParam, isDeletedParam);
		return lambda.Compile();
	}

	/// &amp;lt;summary&amp;gt;
	/// Updates the IsDeleted property for a specific content type and re-indexes it in Optimizely Find.
	/// This method is intended to be called by actions generated for each page type.
	/// &amp;lt;/summary&amp;gt;
	/// &amp;lt;typeparam name=&quot;T&quot;&amp;gt;The type of the content to process, which must inherit from PageData.&amp;lt;/typeparam&amp;gt;
	/// &amp;lt;param name=&quot;content&quot;&amp;gt;The content to be processed.&amp;lt;/param&amp;gt;
	/// &amp;lt;param name=&quot;isDeleted&quot;&amp;gt;A boolean indicating whether the content is marked as deleted (true) or restored (false).&amp;lt;/param&amp;gt;
	private void ProcessContent&amp;lt;T&amp;gt;(IContent content, bool isDeleted) where T : PageData
	{
		var contentRepository = ServiceLocator.Current.GetInstance&amp;lt;IContentRepository&amp;gt;();

                if (contentRepository.Get&amp;lt;T&amp;gt;(content.ContentLink).CreateWritableClone() is T writableContent)
                {
	                    writableContent.IsDeleted = isDeleted;

	                    if (isDeleted)
	                    {
		                        writableContent.Deleted = DateTime.Now;
		                        writableContent.DeletedBy = PrincipalInfo.CurrentPrincipal.Identity.Name;
	                     }
	                     else
	                     {
		                        writableContent.Deleted = null;
		                        writableContent.DeletedBy = null;
	                      }
	                      contentRepository.Save(writableContent, SaveAction.SkipValidation, AccessLevel.NoAccess);
	                     _findClient.Index(writableContent);
                    }
	  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;Using ExcludeDeleted():&lt;/strong&gt;&lt;/h3&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;var detailPageQuery = _searchClient.Search&amp;lt;StorytellingPage&amp;gt;()
    .ExcludeDeleted()
    .FilterForVisitor()
    .FilterOnCurrentSite()
    .CustomFilterForVisitor()
    .Take(PAGINATION);&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;By creating a generic solution using Optimizely Content Events, we can dynamically detect content type changes, set an &lt;code&gt;IsDeleted&lt;/code&gt; flag, and update the search index in real time. This approach not only makes it easy to exclude trashed content from search results but also keeps our codebase clean and efficient, without runtime reflection overhead.&lt;/p&gt;
&lt;p&gt;This solution can be adapted to any custom page type that inherits from &lt;code&gt;SitePageData&lt;/code&gt;, making it versatile for projects that need better control over search visibility in Optimizely CMS.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;P.S. Any input or alternative proposals would be gratefully received.&lt;/p&gt;</id><updated>2024-11-07T06:19:16.0000000Z</updated><summary type="html">Blog post</summary></entry></feed>