<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title type="text">Blog posts by Joe Mayberry</title><link href="http://world.optimizely.com" /><updated>2020-03-16T20:48:22.0000000Z</updated><id>https://world.optimizely.com/blogs/joe-mayberry/</id> <generator uri="http://world.optimizely.com" version="2.0">Optimizely World</generator> <entry><title>Lessons Learned with my first Personalized Content Recommendations Implementation</title><link href="https://world.optimizely.com/blogs/joe-mayberry/dates/2020/3/lessons-learned-with-my-first-personalized-content-recommendations-implementation/" /><id>&lt;p&gt;As most of you know by now, in November 2019, Episerver acquired Idio, a content personalization and analytics platform that can provide a 1:1 content recommendations based on a user&amp;rsquo;s interests and journey through your site. This means that Episerver now offers an even more spectacular content recommendation platform, that puts the best individually tailored content in front of your users.&lt;/p&gt;
&lt;p&gt;For more information about the acquisition please visit Episerver&amp;rsquo;s news release: &lt;a href=&quot;https://www.episerver.com/company/press-room/episerver-signs-definitive-agreement-to-acquire-11-personalization-company-idio/&quot;&gt;https://www.episerver.com/company/press-room/episerver-signs-definitive-agreement-to-acquire-11-personalization-company-idio/&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Getting started&lt;/h2&gt;
&lt;p&gt;First, talk with your account manager. They will get the ball rolling and get you set up with an on-boarding team, who will facilitate the provisioning of the content recommendations environment, provide you with the API keys that you will need, as well as a login to the Content Recommendations dashboard, and ultimately helping to you understand the entire process.&lt;/p&gt;
&lt;h2&gt;Collecting metadata or What should you track?&lt;/h2&gt;
&lt;p&gt;The Content Recommendations engine does a lot of great things behind the scenes. It uses Natural Language Processing (NLP) to understand the meaning and to determine the topics of your content. It also looks at the metadata set on the content, such as the OpenGraph (og) properties that Facebook uses. If those properties are on the page, they will be picked up automatically.&lt;/p&gt;
&lt;p&gt;In addition, you can set other data values that you want to be tracked by including additional metadata tags in the header of your content. For example the client I was working with wanted to track the page type and categories for all of their content.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/link/6c395125fee8467eae7bc88605aef0f5.aspx&quot;&gt;https://world.episerver.com/documentation/developer-guides/personalization/content-recommendations/collecting-metadata/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Once this data is tracked, it can be used in a variety of ways, such as setting up your Flows in order to filter the content that would be available in the recommendations group. The data will also be included in the response returned to the recommendations block, so it can be used for the rendering in the Mustache template (more on that later)&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;background-color:&amp;#32;#ffff99;&quot;&gt;&lt;strong&gt;IMPORTANT: &lt;/strong&gt;If you are looking to have an image displayed in the recommendations, then one needs to be set into the meta tags, such as the og:image tag. The indexing process will &lt;strong&gt;NOT&lt;/strong&gt; pick an image from the content automatically. Only from the meta tags.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pro tip:&lt;/strong&gt; A little bit of planning at this point in the process will save a lot of time later. Look at the end-result designs you want to use for the recommendations. What information do you want to display? Where that information is coming from? It&#39;s easier to add these metatags before you start tracking, rather than after.&lt;/p&gt;
&lt;h2&gt;&lt;/h2&gt;
&lt;h2&gt;The Tracking Script&lt;/h2&gt;
&lt;p&gt;The tracking script is one of the key parts to this whole thing. It&amp;rsquo;s a JavaScript tag that goes on every page, making an API call back to the indexing engine to the index the piece content and to track the visitor activity for that URL.&lt;/p&gt;
&lt;p&gt;There are two ways to add the tracking script, the first is by using a tag management solution, such as &lt;a href=&quot;https://marketingplatform.google.com/about/tag-manager/&quot;&gt;Google Tag Manager&lt;/a&gt;, or by using the tracking script that is included with the EPiServer.Personalization.Content.UI NuGet package.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Adding via Tag Management System:&lt;/strong&gt;&lt;br /&gt;&lt;a href=&quot;/link/653bee4fd8b14c81a023ac436d2288d7.aspx&quot;&gt;https://world.episerver.com/documentation/developer-guides/personalization/content-recommendations/installing-content-recommendations/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Installing the EPiServer.Personalization.Content.UI NuGet package:&lt;/strong&gt;&lt;br /&gt;&lt;a href=&quot;/link/5eb1d145db804031bf509cb6349a7ab8.aspx&quot;&gt;https://world.episerver.com/documentation/developer-guides/CMS/personalization/installing-and-configuring-content-recommendations-integration-package/&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;background-color:&amp;#32;#ffff99;&quot;&gt;&lt;strong&gt;IMPORTANT:&lt;/strong&gt; If you use a tag management system, set the episerver:personalization.content.DisableDefaultTracking key equal to &quot;&lt;strong&gt;true&quot;&lt;/strong&gt; in your appSettings.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;If you are using the script rendered from the NuGet package then the domain IP address that you are tracking, will need to be whitelisted.&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;background-color:&amp;#32;#ffff99;&quot;&gt;&lt;strong&gt;IMPORTANT: &lt;/strong&gt;While both methods are valid, it&amp;rsquo;s important to only use one or the other. Using both at the same time could result in duplicate tracking, or other issues, and skew the tracking results.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Once you have the tracking script installed and deployed to production, the content will start to be tracked and indexed. The tracking process is very organic in nature, meaning that the content is indexed and tracked as it is accessed by the users. You will almost immediately start to see what content is the most used, which in itself, is very valuable information.&lt;/p&gt;
&lt;h2&gt;The Dashboard&lt;/h2&gt;
&lt;p&gt;The Content Recommendations dashboard provides you with a lot of information about your content, how well it is performing, and what people are really looking at.&lt;/p&gt;
&lt;p&gt;When you install the &lt;a href=&quot;/link/5eb1d145db804031bf509cb6349a7ab8.aspx&quot;&gt;EPiServer.Personalization.Content.UI NuGet package&lt;/a&gt; you will get Content Recommendations connector installed to view the dashboard from the CMS Admin UI.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; at this point, there is not a trusted sign-on from the Episerver Admin UI to the Content Recommendations dashboard, but that is coming.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/86e8a2a91de7490abe4bdb0df2d024de.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/42254f88866c49f9b8f18382086d24ed.aspx&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Going with the Flow&amp;nbsp;&lt;/h2&gt;
&lt;p&gt;Once your content has been indexed and tracked, the next step is to set up some sections and flows to help organize the recommendations. A flow is a set of rules that you can define to match content into logical groups to provide better focused recommendations.&lt;/p&gt;
&lt;p&gt;The Content Recommendations dashboard provides an easy way to set up and refine the flows and sections over time.&lt;/p&gt;
&lt;p&gt;&lt;span&gt;Setting up sections and flows&lt;br /&gt;&lt;/span&gt;&lt;a href=&quot;/link/3c89b4d3fcd647bd80fdcb7dff1cc5ed.aspx&quot;&gt;https://world.episerver.com/documentation/developer-guides/personalization/content-recommendations/setting-up-sections-and-flows/&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Show me the Recommendations!&lt;/h2&gt;
&lt;p&gt;With your flows set up, it&amp;rsquo;s time to focus on the delivery. This is where the electronic rubber meets the digital road. Remember, the recommendations are personalized for the user, based on their journey through your site, with the items pulled from the available indexed content groups you have defined in your flows.&lt;/p&gt;
&lt;p&gt;First thing to do is to create some delivery widgets. These components will feed into the recommendations block, to return the personalized recommendations for the user. World.episerver.com already has some great documentation on setting up the sections, flows, and delivery widgets.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/link/3c89b4d3fcd647bd80fdcb7dff1cc5ed.aspx&quot;&gt;https://world.episerver.com/documentation/developer-guides/personalization/content-recommendations/setting-up-sections-and-flows/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/link/875d19d6db0f4a5282640ff61abe0311.aspx&quot;&gt;https://world.episerver.com/documentation/developer-guides/personalization/content-recommendations/setting-up-deliveries/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you haven&amp;rsquo;t already, you will need to install the &lt;a href=&quot;/link/5eb1d145db804031bf509cb6349a7ab8.aspx&quot;&gt;EPiServer.Personalization.Content.UI NuGet package&lt;/a&gt;. In addition to installing the Content Recommendations connector, and adding the tracking scripts to your pages, it also adds a new block type model and view for rendering the recommendations.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/23b060ceadb745978a246aa66d5f252c.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/242c576ef2d6458881b855e1e7ea1455.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The Content Recommendations block has three new properties:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Number of recommendations &amp;ndash; how many items to show at a time. You can have as many as you need, just make sure that the design can handle them.&lt;/li&gt;
&lt;li&gt;Delivery widget &amp;ndash; this is where you select what recommendations group (flow) will be used for this instance of the recommendations. You should see a list of all the delivery widgets that have been set up in the dashboard.&lt;/li&gt;
&lt;li&gt;Recommendation template &amp;ndash; this is a &lt;a href=&quot;https://mustache.github.io/&quot;&gt;Mustache&lt;/a&gt; template that defines the repeating pattern for the markup.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Recommendations Template&lt;/h2&gt;
&lt;p&gt;The Recommendations Template field is the workhorse of the rendering. Everything inside the {{#content}} {{/content}} mustache markup will be repeated for each recommendation item, and additional markup can also be included. Keep in mind that the recommendation template is rendered in the front-end and has no access to the back-end CMS APIs, or content object data. It only knows the JSON that is being returned from the recommendation server.&lt;/p&gt;
&lt;p&gt;You can use the custom metadata that you set up for tracking earlier in the template. For the client I worked with, they needed to display specific category and description content, so we set it up to track in custom tags, and then rendered those values in the template.&lt;/p&gt;
&lt;p&gt;To use the custom metadata, use the mustache syntax to traverse the JSON nested property values.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;{{#metadata}}
    {{#tags}}
       {{#og}}
          {{description}}
       {{/og}}
    {{/tags}}
{{/metadata}}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/link/0ab95a0da5084f1a80d51bd3905da9d3.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;One nice thing is that if you put your markup inside of the nested structure, if nothing is passed, then the markup will not be rendered. In the following example, if no description is passed in the response, then the p tags will not be rendered either.&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;{{#metadata}}
    {{#tags}}
       {{#og}}
          &amp;lt;p&amp;gt;{{description}}&amp;lt;/p&amp;gt;
       {{/og}}
    {/tags}}
{{/metadata}}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;I&amp;rsquo;ve got the block, now what?&lt;/h2&gt;
&lt;p&gt;The great thing about a block approach, and the way this one is set up is that you can use the same delivery widget with different blocks to easily change the recommendations template markup, but return the same recommendations for different uses across your site.&lt;/p&gt;
&lt;p&gt;Since it&amp;rsquo;s a block, you can drop it right into the middle of a XHtml block or add a content reference or content area property to your page models and render using Html.PropertyFor in your view. And if your users have JavaScript turned off, or for some reason no data is returned to from the recommendations engine, then the block doesn&amp;rsquo;t break. It just doesn&amp;rsquo;t show on the page.&lt;/p&gt;
&lt;p&gt;As far as using a content reference or a content block, that is up to you and your needs. But I have four words for you: &lt;strong&gt;Personalization and Visitor Groups&lt;/strong&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;Using Visitor Groups for Personalization&lt;/h2&gt;
&lt;p&gt;The 1:1 personalized recommendations sounds great, but what if you want to change the recommendations based on a visitor group? The recommendations engine doesn&amp;rsquo;t know anything about your visitor groups, but there is a way.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s take a scenario where you want a certain group of recommendations to be used if the visitor falls into a specific visitor group, and a more generic recommendations group if they do not. You would set up two flows and delivery widgets, one with the generic, and one with more targeted rules.&lt;/p&gt;
&lt;p&gt;In CMS, create two recommendations blocks, one pointing to the targeted delivery widget, and one to the generic widget. Then in a content area, add the two recommendation blocks, and add the personalization visitor groups as you normally would. If the visitor falls into the group, they will get the targeted recommendations.&lt;/p&gt;
&lt;h2&gt;Wrapping things up&lt;/h2&gt;
&lt;p&gt;The new Content Recommendations functionality is a great addition to any Episerver website. It will help you understand what content people are looking at, while helping get the right content in front of your users. And while there are several steps in the process, along with a few bumps in the road as I learned what I think is the proper order to do things, it still seemed to go smoothly, without a lot of complications.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;The thing that probably takes the longest out of the whole process is getting the content indexed, since it is an organic process of indexing the content as it is being accessed by your users. But the biggest lesson I learned was that a little bit of planning to determine the data you want to have tracked at the beginning will save you a lot of time later.&lt;/p&gt;
&lt;p&gt;There is some great documentation already set up on world.episerver.com (&lt;a href=&quot;/link/6afe1eb606dc41b7803b29b495f821bf.aspx&quot;&gt;https://world.episerver.com/documentation/developer-guides/personalization/content-recommendations/&lt;/a&gt;), and this will continue to be refined and updated over time.&lt;/p&gt;</id><updated>2020-03-16T20:48:22.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Clearing the Local Object Cache</title><link href="https://world.optimizely.com/blogs/joe-mayberry/dates/2019/2/working-with-the-local-object-cache/" /><id>&lt;p&gt;We all know that one of the best ways to improve site performance is to implement a successful caching strategy, and Episerver employs an aggressive one. However, caching can be frustrating during development, and sometimes it&amp;rsquo;s nice to be able to remove some, or all the cached objects on-demand.&lt;/p&gt;
&lt;p&gt;A couple of years ago I worked on a Web Forms based solution to remove the cache items for an Episerver site. It was okay, but you had to make sure the files were on the server correctly (not always a sure thing with the mix of MVC and Web Forms and site publishing, at least in my experience), and you had to remember a special URL to get to the page. Sure, it all worked, but it was a hassle, there was very little security, and it wasn&amp;rsquo;t MVC.&lt;/p&gt;
&lt;p&gt;When I went through the Episerver Development Fundamentals course by Mark Price (which I highly recommend, even if you are a seasoned developer), one of the exercises (exercise G1) included steps for building an MVC page template for viewing the object cache. I remember thinking that this would be a much better solution than the web forms example that I had used before and could be modified easily to include functionality for not just showing the cached items but removing them as well. Then in the Episerver Advanced Development course, Module F talked about plugins, with an exercise building an admin tool plug-in.&lt;/p&gt;
&lt;p&gt;Ding! Ding! Ding! It hit me like a lightbulb full of bricks&amp;hellip;I could make an admin plugin to view and remove items from the cache. As a plugin, it would be easy to include in deployments. It would be in a logical, secure location, and I wouldn&amp;rsquo;t have to remember a special URL to get to it. Bonus!&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s important to note that this example is primarily focused on the local object cache. It does provide a method for servers in a load balanced environment, although this is not the preferred way of clearing cache items for remote servers. Episerver provides functionality and documentation about the recommended way to invalidate cache across multiple servers:&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;a href=&quot;/link/d8535ea8672442888befed8959dc8b7b.aspx&quot;&gt;https://world.episerver.com/documentation/developer-guides/CMS/caching/Object-caching/&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2&gt;&lt;/h2&gt;
&lt;h2&gt;The Route&lt;/h2&gt;
&lt;p&gt;First thing we need to do is to create a new Initialization Module, to set up a custom route so that we can access the controller functions and make this thing work without having a page object.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;using EPiServer.Framework;
using EPiServer.Framework.Initialization;
using System.Web.Mvc;
using System.Web.Routing;

namespace LOC.Cache.Business.Initialization
{
	[InitializableModule]
	[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
	public class LocalObjectCacheInitialzationModule : IInitializableModule
	{
		private bool initialized = false;

		public void Initialize(InitializationEngine context)
		{
			if (!initialized)
			{
				RouteTable.Routes.MapRoute(
					name: &quot;LocalObjectCache&quot;,
					url: &quot;localobjectcache/{action}&quot;, 
					defaults: new { controller = &quot;LocalObjectCache&quot;, action = &quot;Index&quot; });

				initialized = true;
			}
		}

		public void Uninitialize(InitializationEngine context) { }
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;This module creates a new route named &amp;ldquo;LocalObjectCache&amp;rdquo;, that will call the LocalObjectCacheController&amp;rsquo;s Index function by default.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;The ViewModel&lt;/h2&gt;
&lt;p&gt;Next, we need a viewmodel to pass to the data to the view for rendering.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;using System.Collections;
using System.Collections.Generic;
using System.Web.Mvc;

namespace LOC.Cache.Models.ViewModels
{
	public class LocalObjectCacheViewModel
	{
		public IEnumerable&amp;lt;DictionaryEntry&amp;gt; CachedItems { get; set; }

		public string FilteredBy { get; set; }

		public IEnumerable&amp;lt;SelectListItem&amp;gt; Choices { get; set; }
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here we are setting an IEnumerable of type DictionaryEntry to store the cache object data, a string value for filtering the CachedItems list, and a select list for the filter choices.&lt;/p&gt;
&lt;h2&gt;&lt;/h2&gt;
&lt;h2&gt;The Controller&lt;/h2&gt;
&lt;p&gt;Now we get into the Controller. The workhorse of this plug-in.&lt;/p&gt;
&lt;p&gt;Full credit where it&amp;rsquo;s due&amp;hellip;if this code looks familiar, it&amp;rsquo;s because it&amp;rsquo;s based on the code from exercise G1 in the Development Fundamentals course, and exercise F3 in the Advanced Developers course, both taught by Mark Price (&lt;span&gt;&lt;a href=&quot;/link/5341f632537c4b0ab6b8fb651bd310f8.aspx?userid=9ec4e873-a200-e611-9afb-0050568d2da8&quot;&gt;https://world.episerver.com/System/Users-and-profiles/Community-Profile-Card/?userid=9ec4e873-a200-e611-9afb-0050568d2da8&lt;/a&gt;&lt;/span&gt;).&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;using System.Linq;
using System.Web.Mvc;
using LOC.Cache.Models.ViewModels;
using EPiServer.PlugIn;
using System.Collections;
using EPiServer.Core;
using EPiServer.Framework.Cache;

namespace LOC.Cache.Controllers
{
	[Authorize(Roles = &quot;CmsAdmins&quot;)]
	[GuiPlugIn(Area = PlugInArea.AdminMenu, Url = &quot;~/localobjectcache&quot;, DisplayName = &quot;Clear Local Object Cache&quot;)]
	public class LocalObjectCacheController : Controller
	{
		private readonly ISynchronizedObjectInstanceCache cache;

		public LocalObjectCacheController(ISynchronizedObjectInstanceCache cache)
		{
			this.cache = cache;
		}

		public ActionResult Index(string FilteredBy)
		{
			var viewmodel = new LocalObjectCacheViewModel();

			var cachedEntries = HttpContext.Cache.Cast&amp;lt;DictionaryEntry&amp;gt;();

			switch (FilteredBy)
			{
				case &quot;pages&quot;:
					viewmodel.CachedItems = cachedEntries.Where(item =&amp;gt; item.Value is PageData);
					break;
				case &quot;content&quot;:
					viewmodel.CachedItems = cachedEntries.Where(item =&amp;gt; item.Value is IContent);
					break;
				default:
					viewmodel.CachedItems = cachedEntries;
					break;
			}

			viewmodel.FilteredBy = FilteredBy;

			viewmodel.Choices = new[]
			{
				new SelectListItem { Text = &quot;All Cached Objects&quot;, Value = &quot;all&quot; },
				new SelectListItem { Text = &quot;Any Content&quot;, Value = &quot;content&quot; },
				new SelectListItem { Text = &quot;Pages Only&quot;, Value = &quot;pages&quot; }
			};

			return View(&quot;~/Features/LocalObjectCache/LocalObjectCache.cshtml&quot;, viewmodel);
		}

		[HttpParamAction]
		public ActionResult RemoveLocalCache(string[] cacheKey, LocalObjectCacheViewModel model)
		{
			if (cacheKey != null)
			{
				foreach (string key in cacheKey)
				{
					cache.RemoveLocal(key);
				}
			}
			return RedirectToAction(&quot;Index&quot;);
		}

		[HttpParamAction]
		public ActionResult RemoveLocalRemoteCache(string[] cacheKey)
		{
			if (cacheKey != null)
			{
				foreach(string key in cacheKey)
				{
					cache.RemoveLocal(key);
					cache.RemoveRemote(key);
				}
			}
			return RedirectToAction(&quot;Index&quot;);
		}
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The controller has a few key areas that we should go over.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;[Authorize(Roles = &quot;CmsAdmins&quot;)]
[GuiPlugIn(Area = PlugInArea.AdminMenu, Url = &quot;~/localobjectcache&quot;, DisplayName = &quot;Clear Local Object Cache&quot;)]
public class LocalObjectCacheController : Controller&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Authorize decorator sets the permissions for who can use this plugin. This should be set to an administrators role, since it will be on the Admin UI.&lt;/p&gt;
&lt;p&gt;The GuiPlugin is part of the &lt;a href=&quot;/link/55fe410f4f65482cab72fb59d47c7561.aspx?documentId=cms/11/B8E2A80A&quot;&gt;EPiServer.Plugin namespace&lt;/a&gt;. This is where you define where the plugin will be displayed, the route URL it will use when clicked, and the text to be displayed.&lt;/p&gt;
&lt;p&gt;When you build the project, Episerver will look at the decorators, and add a link to the Admin Tools menu.&lt;/p&gt;
&lt;p&gt;Next is the Index ActionResult function.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public ActionResult Index(string FilteredBy)
{
	var viewmodel = new LocalObjectCacheViewModel();

	var cachedEntries = HttpContext.Cache.Cast&amp;lt;DictionaryEntry&amp;gt;();

	switch (FilteredBy)
	{
		case &quot;pages&quot;:
			viewmodel.CachedItems = cachedEntries.Where(item =&amp;gt; item.Value is PageData);
			break;
		case &quot;content&quot;:
			viewmodel.CachedItems = cachedEntries.Where(item =&amp;gt; item.Value is IContent);
			break;
		default:
			viewmodel.CachedItems = cachedEntries;
			break;
	}

	viewmodel.FilteredBy = FilteredBy;

	viewmodel.Choices = new[]
	{
		new SelectListItem { Text = &quot;All Cached Objects&quot;, Value = &quot;all&quot; },
		new SelectListItem { Text = &quot;Any Content&quot;, Value = &quot;content&quot; },
		new SelectListItem { Text = &quot;Pages Only&quot;, Value = &quot;pages&quot; }
	};

	return View(&quot;~/Features/LocalObjectCache/LocalObjectCache.cshtml&quot;, viewmodel);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the function that is called when you click the link in the Tools menu.&lt;/p&gt;
&lt;p&gt;It first creates a new instance of the LocalObjectCacheViewModel, gets the list of cached items, filters the list of a filter is passed to it if appropriate, and sets up the filter options in the Choices select list IEnumerable. Then returning the view to be rendered.&lt;/p&gt;
&lt;p&gt;And then we come to the RemoveLocalCache and RemofeLocalRemoteCache functions. These functions take an array of the cache keys from the selected items, and loops over the array to remove each key from the cache using the ISynchronizedObjectInstanceCache.RemoveLocal or ISynchronizedObjectInstanceCache.RemoveRemote functions.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;[HttpParamAction]
public ActionResult RemoveLocalCache(string[] cacheKey, LocalObjectCacheViewModel model)
{
	if (cacheKey != null)
	{
		foreach (string key in cacheKey)
		{
			cache.RemoveLocal(key);
		}
	}
	return RedirectToAction(&quot;Index&quot;);
}

[HttpParamAction]
public ActionResult RemoveLocalRemoteCache(string[] cacheKey)
{
	if (cacheKey != null)
	{
		foreach(string key in cacheKey)
		{
			cache.RemoveLocal(key);
			cache.RemoveRemote(key);
		}
	}
	return RedirectToAction(&quot;Index&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is also a function to remove the Local and Remote cache items, although as I mentioned above, this is not necessarily the best way to clear the remote cache, although it should work. Please be aware that this option will generate a lot of traffic between the servers that could negatively affect the site performance. Use this option with caution.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m using a foreach loop to target specific keys, which is handy, but is also the reason why the RemoveLocalRemoteCache acts on both the local and remote servers. If you were to clear the local cache first, you wouldn&amp;rsquo;t have the cache keys to send to the remote server.&lt;/p&gt;
&lt;p&gt;Another element that I need to mention is the [HttpParamAction] decorator. One of the challenges that I faced here was that I wanted to have two separate submit buttons, that submitted the same data to different ActionResult functions in the controller.&lt;/p&gt;
&lt;p&gt;A quick check on Google and StackOverflow showed several possible answers, but I thought the &lt;a href=&quot;http://blog.ashmind.com/2010/03/15/multiple-submit-buttons-with-asp-net-mvc-final-solution/&quot;&gt;solution by Andrey Shchekin was the best one. (This is an old blog, but it works great in this instance)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;To make this functionality work, we need to add a new class to work with the HttpParamAction attribute.&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;using System;
using System.Reflection;
using System.Web.Mvc;

namespace LOC.Cache
{
	public class HttpParamActionAttribute : ActionNameSelectorAttribute
	{
		public override bool IsValidName(ControllerContext controllerContext, string actionName, MethodInfo methodInfo)
		{
			if (actionName.Equals(methodInfo.Name, StringComparison.InvariantCultureIgnoreCase))
				return true;

			if (!actionName.Equals(&quot;Action&quot;, StringComparison.InvariantCultureIgnoreCase))
				return false;

			var request = controllerContext.RequestContext.HttpContext.Request;
			return request[methodInfo.Name] != null;
		}
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, we are looking for a controller action name of &amp;ldquo;Action&amp;rdquo;. If that is not found, it passes everything through as it was. If it is found, then it submits to the ActionResult function with the same name as the button, decorated with the [HttpParamAction] decorator. Slick!&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;The View&lt;/h2&gt;
&lt;p&gt;Finally, the RAZOR view. This is based on the plug-in view template that was introduced in the Advanced Development course, to ensure that the page looks like it belongs in the Episerver Admin UI, and outputs that current local cached item keys and their type. If a filter is applied, the Name, ID, and Published date will be displayed.&lt;/p&gt;
&lt;p&gt;This is also where we set the actionName value for the Html.BeginForm to the generic &amp;ldquo;Action&amp;rdquo; value, to get the correct function using the HttpParamAction class.&lt;/p&gt;
&lt;pre class=&quot;language-markup&quot;&gt;&lt;code&gt;@using EPiServer.Framework.Web.Resources
@using System.Collections
@using EPiServer.Core

@model LOC.Cache.Models.ViewModels.LocalObjectCacheViewModel

@{
	Layout = null;
}

&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html xmlns=&quot;http://www.w3.org/1999/xhtml&quot; lang=&quot;en&quot; xml:lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
	&amp;lt;title&amp;gt;Local Object Cache&amp;lt;/title&amp;gt;
	&amp;lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;IE=Edge&quot; /&amp;gt;

	&amp;lt;!-- Shell --&amp;gt;
	@Html.Raw(ClientResources.RenderResources(&quot;ShellCore&quot;))

	&amp;lt;!-- Light Theme --&amp;gt;
	@Html.Raw(ClientResources.RenderResources(&quot;ShellCoreLightTheme&quot;))

	&amp;lt;link href=&quot;~/App_Themes/Default/Styles/system.css&quot; type=&quot;text/css&quot; rel=&quot;stylesheet&quot;&amp;gt;
	&amp;lt;link href=&quot;~/App_Themes/Default/Styles/ToolButton.css&quot; type=&quot;text/css&quot; rel=&quot;stylesheet&quot;&amp;gt;

	&amp;lt;style type=&quot;text/css&quot;&amp;gt;
		.table-column-width {
			width: 30%;
		}

		.stripe tbody tr:nth-child(even) {
			background-color: #f0f2f2;
		}
	&amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body id=&quot;body&quot;&amp;gt;
	@Html.Raw(Html.ShellInitializationScript())

	&amp;lt;div class=&quot;epi-contentContainer epi-padding&quot;&amp;gt;
		&amp;lt;div class=&quot;epi-contentArea&quot;&amp;gt;
			&amp;lt;h1 class=&quot;EP-Prefix&quot;&amp;gt;
				Local Object Cache
			&amp;lt;/h1&amp;gt;
			&amp;lt;p class=&quot;EP-systemInfo&quot;&amp;gt;This tool shows all of the current local object cache, and allows the deletion of one or more cached items.&amp;lt;/p&amp;gt;
		&amp;lt;/div&amp;gt;

		&amp;lt;div class=&quot;epi-contentArea epi-formArea&quot;&amp;gt;
			@using (Html.BeginForm(&quot;Index&quot;, &quot;LocalObjectCache&quot;, FormMethod.Post))
			{
				&amp;lt;table class=&quot;table&quot;&amp;gt;
					&amp;lt;tr&amp;gt;
						&amp;lt;td&amp;gt;Filter By&amp;lt;/td&amp;gt;
						&amp;lt;td&amp;gt;@Html.DropDownListFor(m =&amp;gt; m.FilteredBy, Model.Choices)&amp;lt;/td&amp;gt;
						&amp;lt;td&amp;gt;
							&amp;lt;span class=&quot;epi-cmsButton&quot;&amp;gt;
								&amp;lt;input class=&quot;epi-cmsButton-text epi-cmsButton-tools epi-cmsButton-Refresh&quot; type=&quot;submit&quot; name=&quot;filter&quot; id=&quot;filter&quot; value=&quot;Filter&quot; onmouseover=&quot;EPi.ToolButton.MouseDownHandler(this)&quot; onmouseout=&quot;EPi.ToolButton.ResetMouseDownHandler(this)&quot; /&amp;gt;
							&amp;lt;/span&amp;gt;
						&amp;lt;/td&amp;gt;
					&amp;lt;/tr&amp;gt;
				&amp;lt;/table&amp;gt;

			}

			@using (Html.BeginForm(&quot;Action&quot;, &quot;LocalObjectCache&quot;, FormMethod.Post))
			{

				&amp;lt;div class=&quot;epi-buttonDefault&quot;&amp;gt;
					&amp;lt;span class=&quot;epi-cmsButton&quot;&amp;gt;
						&amp;lt;input class=&quot;epi-cmsButton-text epi-cmsButton-tools epi-cmsButton-Delete&quot; type=&quot;submit&quot; name=&quot;RemoveLocalCache&quot; id=&quot;RemoveLocalCache&quot; value=&quot;Remove Local Cache Items&quot; onmouseover=&quot;EPi.ToolButton.MouseDownHandler(this)&quot; onmouseout=&quot;EPi.ToolButton.ResetMouseDownHandler(this)&quot; /&amp;gt;
					&amp;lt;/span&amp;gt;
					&amp;lt;span class=&quot;epi-cmsButton&quot;&amp;gt;
						&amp;lt;input class=&quot;epi-cmsButton-text epi-cmsButton-tools epi-cmsButton-Delete&quot; type=&quot;submit&quot; name=&quot;removeLocalRemoteCache&quot; id=&quot;removeLocalRemoteCache&quot; value=&quot;Remove Local and Remote Cache Items&quot; onmouseover=&quot;EPi.ToolButton.MouseDownHandler(this)&quot; onmouseout=&quot;EPi.ToolButton.ResetMouseDownHandler(this)&quot; /&amp;gt;
					&amp;lt;/span&amp;gt;
				&amp;lt;/div&amp;gt;

				&amp;lt;table class=&quot;table table-condensed table-bordered table-condensed stripe&quot;&amp;gt;
					&amp;lt;thead&amp;gt;
						&amp;lt;tr&amp;gt;
							&amp;lt;th&amp;gt;&amp;lt;input type=&quot;checkbox&quot; id=&quot;clearAll&quot; name=&quot;clearAll&quot; onClick=&quot;toggle(this)&quot; value=&quot;true&quot; /&amp;gt;&amp;lt;/th&amp;gt;
							&amp;lt;th class=&quot;table-column-width&quot;&amp;gt;Key&amp;lt;/th&amp;gt;
							&amp;lt;th class=&quot;table-column-width&quot;&amp;gt;Type&amp;lt;/th&amp;gt;
							&amp;lt;th class=&quot;table-column-width&quot;&amp;gt;@(string.IsNullOrWhiteSpace(Model.FilteredBy) ? &quot;Value&quot; : &quot;Name (ID) Published&quot;)&amp;lt;/th&amp;gt;
						&amp;lt;/tr&amp;gt;
					&amp;lt;/thead&amp;gt;
					&amp;lt;tbody&amp;gt;
						@foreach (DictionaryEntry item in Model.CachedItems)
						{
							&amp;lt;tr&amp;gt;
								&amp;lt;td class=&quot;center&quot;&amp;gt;&amp;lt;input type=&quot;checkbox&quot; id=&quot;@item.Key&quot; name=&quot;cacheKey&quot; value=&quot;@item.Key&quot; /&amp;gt;&amp;lt;/td&amp;gt;
								&amp;lt;td&amp;gt;@item.Key&amp;lt;/td&amp;gt;
								&amp;lt;td&amp;gt;@item.Value.GetType()&amp;lt;/td&amp;gt;
								&amp;lt;td&amp;gt;
									@if (item.Value is IContent)
									{
										@((item.Value as IContent).Name)
										&amp;lt;span class=&quot;badge badge-warning&quot;&amp;gt;@((item.Value as IContent).ContentLink.ID)&amp;lt;/span&amp;gt;
									}
									@if (item.Value is PageData)
									{
										@((item.Value as PageData).StartPublish)
									}
								&amp;lt;/td&amp;gt;
							&amp;lt;/tr&amp;gt;
						}
					&amp;lt;/tbody&amp;gt;
				&amp;lt;/table&amp;gt;

				&amp;lt;div class=&quot;epi-buttonDefault&quot;&amp;gt;
					&amp;lt;span class=&quot;epi-cmsButton&quot;&amp;gt;
						&amp;lt;input class=&quot;epi-cmsButton-text epi-cmsButton-tools epi-cmsButton-Delete&quot; type=&quot;submit&quot; name=&quot;RemoveLocalCache&quot; id=&quot;RemoveLocalCacheBottom&quot; value=&quot;Remove Local Cache Items&quot; onmouseover=&quot;EPi.ToolButton.MouseDownHandler(this)&quot; onmouseout=&quot;EPi.ToolButton.ResetMouseDownHandler(this)&quot; /&amp;gt;
					&amp;lt;/span&amp;gt;
					&amp;lt;span class=&quot;epi-cmsButton&quot;&amp;gt;
						&amp;lt;input class=&quot;epi-cmsButton-text epi-cmsButton-tools epi-cmsButton-Delete&quot; type=&quot;submit&quot; name=&quot;removeLocalRemoteCache&quot; id=&quot;removeLocalRemoteCacheBottom&quot; value=&quot;Remove Local and Remote Cache Items&quot; onmouseover=&quot;EPi.ToolButton.MouseDownHandler(this)&quot; onmouseout=&quot;EPi.ToolButton.ResetMouseDownHandler(this)&quot; /&amp;gt;
					&amp;lt;/span&amp;gt;
				&amp;lt;/div&amp;gt;
			}
		&amp;lt;/div&amp;gt;
	&amp;lt;/div&amp;gt;

	&amp;lt;script language=&quot;JavaScript&quot;&amp;gt;
		function toggle(source) {
			checkboxes = document.getElementsByName(&#39;cacheKey&#39;);
			for (var i = 0, n = checkboxes.length; i &amp;lt; n; i++) {
				checkboxes[i].checked = source.checked;
			}
		}
	&amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Alright, it&amp;rsquo;s time to build and run the site. Log in as an administrator and navigate to the Admin UI. You should now see an option in the Admin -&amp;gt; Tools section labeled &quot;Clear Local Object Cache&quot;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/69c448a9bce84d36b227080327acc245.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;If you want to remove a specific object, select the checkbox next to the key, and click the &amp;ldquo;Remove Local Cache Items&amp;rdquo; button. The RemoveLocalCache or RemoveLocalRemoteCache ActionResult functions in the LocalObjectCacheController will loop over all the selected cache keys and remove them.&lt;/p&gt;
&lt;p&gt;Since this list can be very long, there is also a simple JavaScript function to check or uncheck all the checkboxes.&lt;/p&gt;
&lt;p&gt;One other thing to note is that for this plug-in I am grouping all the code files together into a Features folder for easy maintenance and convenience. You don&amp;rsquo;t have to do that. These files can be organized into the standard MVC structure (Business, Controllers, Models, Views) if you prefer. If you do use the Features folder, then it would be a good idea to update the _ViewStart.cshtml and web.config files in the Features folder with the ones from your Views folder.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s it. There are probably several ways to accomplish this same functionality, and I would be excited to see something that uses the CacheEvictionPolicy and master keys for clearing cache on a remote server. Hopefully someone will find this useful. I know that I did.&lt;/p&gt;
&lt;p&gt;All of the code is available on GitHub&lt;/p&gt;
&lt;p&gt;https://github.com/jaytem/Episerver-LocalObjectCache&lt;/p&gt;</id><updated>2019-02-28T17:42:35.0000000Z</updated><summary type="html">Blog post</summary></entry></feed>