Home » Search results for 'IDTable'

Search Results for: IDTable

Synchronize IDTable Entries Across Multiple Sitecore Databases Using a Custom publishItem Pipeline Processor

In a previous post I showed a solution that uses the Composite design pattern in an attempt to answer the following question by Sitecore MVP Kyle Heon:

Although I enjoyed building that solution, it isn’t ideal for synchronizing IDTable entries across multiple Sitecore databases — entries are added to all configured IDTables even when Items might not exist in all databases of those IDTables (e.g. the Sitecore Items have not been published to those databases).

I came up with another solution to avoid the aforementioned problem — one that synchronizes IDTable entries using a custom <publishItem> pipeline processor, and the following class contains code for that processor:

using System.Collections.Generic;
using System.Linq;

using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.IDTables;
using Sitecore.Diagnostics;
using Sitecore.Publishing.Pipelines.PublishItem;

namespace Sitecore.Sandbox.Pipelines.Publishing
{
    public class SynchronizeIDTables : PublishItemProcessor
    {
        private IEnumerable<string> _IDTablePrefixes;
        private IEnumerable<string> IDTablePrefixes
        {
            get
            {
                if (_IDTablePrefixes == null)
                {
                    _IDTablePrefixes = GetIDTablePrefixes();
                }

                return _IDTablePrefixes;
            }
        }

        private string IDTablePrefixesConfigPath { get; set; }

        public override void Process(PublishItemContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            Assert.ArgumentNotNull(context.PublishOptions, "context.PublishOptions");
            Assert.ArgumentNotNull(context.PublishOptions.SourceDatabase, "context.PublishOptions.SourceDatabase");
            Assert.ArgumentNotNull(context.PublishOptions.TargetDatabase, "context.PublishOptions.TargetDatabase");
            IDTableProvider sourceProvider = CreateNewIDTableProvider(context.PublishOptions.SourceDatabase);
            IDTableProvider targetProvider = CreateNewIDTableProvider(context.PublishOptions.TargetDatabase);
            RemoveEntries(targetProvider, GetAllEntries(targetProvider, context.ItemId));
            AddEntries(targetProvider, GetAllEntries(sourceProvider, context.ItemId));
        }

        protected virtual IDTableProvider CreateNewIDTableProvider(Database database)
        {
            Assert.ArgumentNotNull(database, "database");
            return Factory.CreateObject(string.Format("IDTable[@id='{0}']", database.Name), true) as IDTableProvider;
        }

        protected virtual IEnumerable<IDTableEntry> GetAllEntries(IDTableProvider provider, ID itemId)
        {
            Assert.ArgumentNotNull(provider, "provider");
            Assert.ArgumentCondition(!ID.IsNullOrEmpty(itemId), "itemId", "itemId cannot be null or empty!");
            List<IDTableEntry> entries = new List<IDTableEntry>();
            foreach(string prefix in IDTablePrefixes)
            {
                IEnumerable<IDTableEntry> entriesForPrefix = provider.GetKeys(prefix, itemId);
                if (entriesForPrefix.Any())
                {
                    entries.AddRange(entriesForPrefix);
                }
            }

            return entries;
        }

        private static void RemoveEntries(IDTableProvider provider, IEnumerable<IDTableEntry> entries)
        {
            Assert.ArgumentNotNull(provider, "provider");
            Assert.ArgumentNotNull(entries, "entries");
            foreach (IDTableEntry entry in entries)
            {
                provider.Remove(entry.Prefix, entry.Key);
            }
        }

        private static void AddEntries(IDTableProvider provider, IEnumerable<IDTableEntry> entries)
        {
            Assert.ArgumentNotNull(provider, "provider");
            Assert.ArgumentNotNull(entries, "entries");
            foreach (IDTableEntry entry in entries)
            {
                provider.Add(entry);
            }
        }

        protected virtual IEnumerable<string> GetIDTablePrefixes()
        {
            Assert.ArgumentNotNullOrEmpty(IDTablePrefixesConfigPath, "IDTablePrefixConfigPath");
            return Factory.GetStringSet(IDTablePrefixesConfigPath);
        }
    }
}

The Process method above grabs all IDTable entries for all defined IDTable prefixes — these are pulled from the configuration file that is shown later on in this post — from the source database for the Item being published, and pushes them all to the target database after deleting all preexisting entries from the target database for the Item (the code is doing a complete overwrite for the Item’s IDTable entries in the target database).

I also added the following code to serve as an item:deleted event handler (if you would like to learn more about events and their handlers, check out John West‘s post about them, and also take a look at this page on the
Sitecore Developer Network (SDN)) to remove entries for the Item when it’s being deleted:

using System;
using System.Collections.Generic;
using System.Linq;

using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Events;
using Sitecore.Data.IDTables;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Events;

namespace Sitecore.Sandbox.Data.IDTables
{
    public class ItemEventHandler
    {
        private IEnumerable<string> _IDTablePrefixes;
        private IEnumerable<string> IDTablePrefixes
        {
            get
            {
                if (_IDTablePrefixes == null)
                {
                    _IDTablePrefixes = GetIDTablePrefixes();
                }

                return _IDTablePrefixes;
            }
        }

        private string IDTablePrefixesConfigPath { get; set; }

        protected void OnItemDeleted(object sender, EventArgs args)
        {
            if (args == null)
            {
                return;
            }

            Item item = Event.ExtractParameter(args, 0) as Item;
            if (item == null)
            {
                return;
            }

            DeleteItemEntries(item);
        }

        private void DeleteItemEntries(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            IDTableProvider provider = CreateNewIDTableProvider(item.Database.Name);
            foreach (IDTableEntry entry in GetAllEntries(provider, item.ID))
            {
                provider.Remove(entry.Prefix, entry.Key);
            }
        }

        protected virtual IEnumerable<IDTableEntry> GetAllEntries(IDTableProvider provider, ID itemId)
        {
            Assert.ArgumentNotNull(provider, "provider");
            Assert.ArgumentCondition(!ID.IsNullOrEmpty(itemId), "itemId", "itemId cannot be null or empty!");
            List<IDTableEntry> entries = new List<IDTableEntry>();
            foreach (string prefix in IDTablePrefixes)
            {
                IEnumerable<IDTableEntry> entriesForPrefix = provider.GetKeys(prefix, itemId);
                if (entriesForPrefix.Any())
                {
                    entries.AddRange(entriesForPrefix);
                }
            }

            return entries;
        }

        private static void RemoveEntries(IDTableProvider provider, IEnumerable<IDTableEntry> entries)
        {
            Assert.ArgumentNotNull(provider, "provider");
            Assert.ArgumentNotNull(entries, "entries");
            foreach (IDTableEntry entry in entries)
            {
                provider.Remove(entry.Prefix, entry.Key);
            }
        }

        protected virtual IDTableProvider CreateNewIDTableProvider(string databaseName)
        {
            return Factory.CreateObject(string.Format("IDTable[@id='{0}']", databaseName), true) as IDTableProvider;
        }

        protected virtual IEnumerable<string> GetIDTablePrefixes()
        {
            Assert.ArgumentNotNullOrEmpty(IDTablePrefixesConfigPath, "IDTablePrefixConfigPath");
            return Factory.GetStringSet(IDTablePrefixesConfigPath);
        }
    }
}

The above code retrieves all IDTable entries for the Item being deleted — filtered by the configuration defined IDTable prefixes — from its database’s IDTable, and calls the Remove method on the IDTableProvider instance that is created for the Item’s database for each entry.

I then registered all of the above in Sitecore using the following configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <events>
      <event name="item:deleted">
        <handler type="Sitecore.Sandbox.Data.IDTables.ItemEventHandler, Sitecore.Sandbox" method="OnItemDeleted">
          <IDTablePrefixesConfigPath>IDTablePrefixes/IDTablePrefix</IDTablePrefixesConfigPath>
        </handler>
      </event>
    </events>
    <IDTable type="Sitecore.Data.$(database).$(database)IDTable, Sitecore.Kernel" singleInstance="true">
      <patch:attribute name="id">master</patch:attribute>
      <param connectionStringName="master"/>
      <param desc="cacheSize">500KB</param>
    </IDTable>
    <IDTable id="web" type="Sitecore.Data.$(database).$(database)IDTable, Sitecore.Kernel" singleInstance="true">
      <param connectionStringName="web"/>
      <param desc="cacheSize">500KB</param>
    </IDTable>
    <IDTablePrefixes>
      <IDTablePrefix>IDTableTest</IDTablePrefix>
    </IDTablePrefixes>
    <pipelines>
      <publishItem>
        <processor type="Sitecore.Sandbox.Pipelines.Publishing.SynchronizeIDTables, Sitecore.Sandbox">
          <IDTablePrefixesConfigPath>IDTablePrefixes/IDTablePrefix</IDTablePrefixesConfigPath>
        </processor>
      </publishItem>
    </pipelines>
  </sitecore>
</configuration>

For testing, I quickly whipped up a web form to add a couple of IDTable entries using an IDTableProvider for the master database — I am omitting that code for brevity — and ran a query to verify the entries were added into the IDTable in my master database (I also ran another query for the IDTable in my web database to show that it contains no entries):

idtables-before-publish

I published both items, and queried the IDTable in the master and web databases:

idtables-after-publish-both-items

As you can see, both entries were inserted into the web database’s IDTable.

I then deleted one of the items from the master database via the Sitecore Content Editor:

idtables-deleted-from-master

It was removed from the IDTable in the master database.

I then published the deleted item’s parent with subitems:

idtables-published-deletion

As you can see, it was removed from the IDTable in the web database.

If you have any suggestions for making this code better, or have another solution for synchronizing IDTable entries across multiple Sitecore databases, please share in a comment.

Synchronize IDTable Entries Across Multiple Sitecore Databases Using a Composite IDTableProvider

The other day Sitecore MVP Kyle Heon asked:

This tweet got the wheels turning — more like got the hamster wheel spinning in my head — and I began experimenting on ways to synchronize IDTable entries across different Sitecore databases.

In this post, I will show the first solution I had come up with — yes I’ve come up with two solutions to this although no doubt there are more (if you have ideas for other solutions, or have tackled this problem in the past, please share in a comment) — but before I show that solution, I’d like to explain what the IDTable in Sitecore is, and why you might want to use it.

Assuming you’re using SQL Server for Sitecore, the “out of the box” IDTable in Sitecore is a database table that lives in all Sitecore databases — though Sitecore’s prepackaged configuration only points to the IDTable in the master database.

One has the ability to store key/value pairs in this table in the event you don’t want to “roll your own” custom database table — or use some other data store — and don’t want to expose these key/value pair relationships in Items in Sitecore.

The key must be a character string, and the value must be a Sitecore Item ID.

Plus, one has the ability to couple these key/value pairs with “custom data” — this, like the key, is stored in the database as a string which makes it cumbersome around storing complex data structures (having some sort of serialization/deserialization paradigm in place would be required to make this work).

Alex Shyba showed how one could leverage the IDTable for URLs for fictitious products stored in Sitecore in this post, and this article employed the IDTable for a custom Data Provider.

If you would like to know more about the IDTable, please leave a comment, and I will devote a future post to it.

Let’s take a look at the first solution I came up with:

using System;
using System.Collections.Generic;
using System.Xml;

using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.IDTables;
using Sitecore.Diagnostics;

using Sitecore.Sandbox.Data.IDTables;

namespace Sitecore.Sandbox.Data.SqlServer
{
    public class CompositeSqlServerIDTable : IDTableProvider
    {
        private IDictionary<string, IDTableProvider> _IDTableProviders;
        private IDictionary<string, IDTableProvider> IDTableProviders
        {
            get
            {
                if (_IDTableProviders == null)
                {
                    _IDTableProviders = new Dictionary<string, IDTableProvider>();
                }

                return _IDTableProviders;
            }
        }

        private string DatabaseName { get; set; }

        public CompositeSqlServerIDTable()
            : this(GetDefaultDatabaseName())
        {
        }

        public CompositeSqlServerIDTable(string databaseName)
        {
            SetDatabaseName(databaseName);
        }

        private void SetDatabaseName(string databaseName)
        {
            Assert.ArgumentNotNullOrEmpty(databaseName, "databaseName");
            DatabaseName = databaseName;
        }

        public override void Add(IDTableEntry entry)
        {
            foreach (IDTableProvider provider in IDTableProviders.Values)
            {
                provider.Add(entry);
            }
        }

        public override IDTableEntry GetID(string prefix, string key)
        {
            return GetContextIDTableProvider().GetID(prefix, key);
        }

        public override IDTableEntry[] GetKeys(string prefix)
        {
            return GetContextIDTableProvider().GetKeys(prefix);
        }

        public override IDTableEntry[] GetKeys(string prefix, ID id)
        {
            return GetContextIDTableProvider().GetKeys(prefix, id);
        }

        protected virtual IDTableProvider GetContextIDTableProvider()
        {
            IDTableProvider provider;
            if (IDTableProviders.TryGetValue(DatabaseName, out provider))
            {
                return provider;
            }

            return new NullIDTable();
        }

        public override void Remove(string prefix, string key)
        {
            foreach (IDTableProvider provider in IDTableProviders.Values)
            {
                provider.Remove(prefix, key);
            }
        }

        protected virtual void AddIDTable(XmlNode configNode)
        {
            if (configNode == null || string.IsNullOrWhiteSpace(configNode.InnerText))
            {
                return;
            }

            IDTableProvider idTable = Factory.CreateObject<IDTableProvider>(configNode);
            if (idTable == null)
            {
                Log.Error("Configuration invalid for an IDTable!", this);
                return;
            }

            XmlAttribute idAttribute = configNode.Attributes["id"];
            if (idAttribute == null || string.IsNullOrWhiteSpace(idAttribute.Value))
            {
                Log.Error("IDTable configuration should have an id attribute set!", this);
                return;
            }

            if(IDTableProviders.ContainsKey(idAttribute.Value))
            {
                Log.Error("Duplicate IDTable id encountered!", this);
                return;
            }

            IDTableProviders.Add(idAttribute.Value, idTable);
        }

        private static string GetDefaultDatabaseName()
        {
            if (Context.ContentDatabase != null)
            {
                return Context.ContentDatabase.Name;
            }

            return Context.Database.Name;
        }
    }
}

The above class uses the Composite design pattern — it adds/removes entries in multiple IDTableProvider instances (these instances are specified in the configuration file that is shown later in this post), and delegates calls to the GetKeys and GetID methods of the instance that is referenced by the database name passed in to the class’ constructor.

The CompositeSqlServerIDTable class also utilizes a Null Object by creating an instance of the following class when the context database does not exist in the collection:

using System.Collections.Generic;

using Sitecore.Data;
using Sitecore.Data.IDTables;

namespace Sitecore.Sandbox.Data.IDTables
{
    public class NullIDTable : IDTableProvider
    {
        public NullIDTable()
        {
        }

        public override void Add(IDTableEntry entry)
        {
            return;
        }

        public override IDTableEntry GetID(string prefix, string key)
        {
            return null;
        }

        public override IDTableEntry[] GetKeys(string prefix)
        {
            return new List<IDTableEntry>().ToArray();
        }

        public override IDTableEntry[] GetKeys(string prefix, ID id)
        {
            return new List<IDTableEntry>().ToArray();
        }

        public override void Remove(string prefix, string key)
        {
            return;
        }
    }
}

The NullIDTable class above basically has no behavior, returns null for the GetID method, and an empty collection for both GetKeys methods. Having a Null Object helps us avoid checking for nulls when performing operations on the IDTableProvider instance when an instance cannot be found in the IDTableProviders Dictionary property — although we lose visibility around the context database not being present in the Dictionary (I probably should’ve included some logging code to capture this).

I then glued everything together using the following configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <IDTable patch:instead="IDTable[@type='Sitecore.Data.$(database).$(database)IDTable, Sitecore.Kernel']" type="Sitecore.Sandbox.Data.$(database).Composite$(database)IDTable" singleInstance="true">
      <IDTables hint="raw:AddIDTable">
        <IDTable id="core" type="Sitecore.Data.$(database).$(database)IDTable, Sitecore.Kernel" singleInstance="true">
          <param connectionStringName="$(id)"/>
          <param desc="cacheSize">500KB</param>
        </IDTable>
        <IDTable id="master" type="Sitecore.Data.$(database).$(database)IDTable, Sitecore.Kernel" singleInstance="true">
          <param connectionStringName="$(id)"/>
          <param desc="cacheSize">500KB</param>
        </IDTable>
        <IDTable id="web" type="Sitecore.Data.$(database).$(database)IDTable, Sitecore.Kernel" singleInstance="true">
          <param connectionStringName="$(id)"/>
          <param desc="cacheSize">500KB</param>
        </IDTable>
      </IDTables>
    </IDTable>
  </sitecore>
</configuration>

To test the code above, I created the following web form which basically invokes add/remove methods on the instance of our IDTableProvider, and displays the entries in our IDTables after each add/remove operation:

using System;
using System.Collections.Generic;
using System.Linq;

using Sitecore.Configuration;
using Sitecore.Data.IDTables;

namespace Sandbox
{
    public partial class IDTableTest : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            AddEntries();
            ShowEntries();
            RemoveOneEntry();
            ShowEntries();
            RemoveAllEntries();
            ShowEntries();
        }

        private void AddEntries()
        {
            Response.Write("Adding two entries...<br /><br />");
            IDTable.Add("IDTableTest", "/mycoolhomepage1.aspx", Sitecore.Data.ID.Parse("{110D559F-DEA5-42EA-9C1C-8A5DF7E70EF9}"));
            IDTable.Add("IDTableTest", "/someotherpage1.aspx", Sitecore.Data.ID.Parse("{BAB78AE5-8118-4476-B1B5-F8981DAE1779}"));
        }

        private void RemoveOneEntry()
        {
            Response.Write("Removing one entry...<br /><br />");
            IDTable.RemoveKey("IDTableTest", "/mycoolhomepage1.aspx");
        }

        private void RemoveAllEntries()
        {
            Response.Write("Removing all entries...<br /><br />");
            foreach (IDTableEntry entry in IDTable.GetKeys("IDTableTest"))
            {
                IDTable.RemoveKey(entry.Prefix, entry.Key);
            }
        }

        private void ShowEntries()
        {
            ShowEntries("core");
            ShowEntries("master");
            ShowEntries("web");
        }

        private void ShowEntries(string databaseName)
        {
            IDTableProvider provider = CreateNewIDTableProvider(databaseName);
            IEnumerable<IDTableEntry> entries = IDTable.GetKeys("IDTableTest");
            Response.Write(string.Format("{0} entries in IDTable in the {1} database:<br />", entries.Count(), databaseName));
            foreach (IDTableEntry entry in provider.GetKeys("IDTableTest"))
            {
                Response.Write(string.Format("key: {0} id: {1}<br />", entry.Key, entry.ID));
            }
            Response.Write("<br />");
        }

        protected virtual IDTableProvider CreateNewIDTableProvider(string databaseName)
        {
            return Factory.CreateObject(string.Format("//IDTable[@id='{0}']", databaseName), true) as IDTableProvider;
        }
    }
}

When I loaded up the web form above in a browser, I saw that things were working as expected:

idtable-composite-test-run

Although I had fun in writing all of the code above, I feel this isn’t exactly ideal for synchronizing entries in IDTables across multiple Sitecore databases. I cannot see why one would want an entry for a Item in master that has not yet been published to the web database.

If you know of a scenario where the above code could be useful, or have suggestions on making it better, please drop a comment.

In my next post, I will show you what I feel is a better approach to synchronizing entries across IDTables — a solution that synchronizes entries via publishing and Item deletion.

Until next time, have a Sitecorelicious day!

Yet Another Way to Store Data Outside of the Sitecore Experience Platform

Last February, Sitecore MVP Nick Wesselman shared an awesome blog post on storing data outside of the Sitecore® Experience Platform™ using the NHibernate ORM framework — if you haven’t had a chance to read this, I strongly recommend that you do — which is complete magic, and a simple solution where you don’t have to worry about spinning up your own database tables for storing information.

But, let’s suppose you aren’t allowed to use an ORM like NHibernate in your solution for some reason — I won’t go into potential reasons but let’s make pretend there is one — and you have to find a way to store Sitecore specific information but don’t want to go through the trouble of spinning up a new Sitecore database due to the overhead involved. What can you do?

Well, you can still store information in a non-Sitecore database using the Sitecore API. The following “proof of concept” does this, and is basically modeled after how Sitecore manages data stored in the IDTable and Links Database.

The code in the following “proof of concept” adds/retrieves/deletes alternative URLs for Sitecore Items in the following custom database table:

itemurls-sql-table

I’m not going to talk much about the SQL table or SQL statements used in this “proof of concept” since it’s beyond the scope of this post.

Of course we all love things that are performant — and our clients love when we make things performant — so I decided to start off my solution using the following adapter — this includes the interface and concrete class — for an instance of Sitecore.Caching.Cache (this lives in Sitecore.Kernel.dll):

using System;
using System.Collections;

using Sitecore.Caching;
using Sitecore.Data;
using Sitecore.Diagnostics.PerformanceCounters;

namespace Sitecore.Sandbox.Caching
{
    public interface ICacheProvider
    {
        bool CacheWriteEnabled { get; set; }

        int Count { get; }

        CachePriority DefaultPriority { get; set; }

        bool Enabled { get; set; }

        AmountPerSecondCounter ExternalCacheClearingsCounter { get; set; }

        ID Id { get; }

        long MaxSize { get; set; }

        string Name { get; }

        long RemainingSpace { get; }

        bool Scavengable { get; set; }

        long Size { get; }

        object SyncRoot { get; }

        object this[object key] { get; }

        void Add(ID key, ICacheable data);

        void Add(ID key, string value);

        void Add(string key, ICacheable data);

        void Add(string key, ID value);

        Cache.CacheEntry Add(string key, string data);

        void Add(ID key, object data, long dataLength);

        void Add(object key, object data, long dataLength);

        void Add(string key, object data, long dataLength);

        void Add(object key, object data, long dataLength, DateTime absoluteExpiration);

        void Add(object key, object data, long dataLength, TimeSpan slidingExpiration);

        Cache.CacheEntry Add(string key, object data, long dataLength, DateTime absoluteExpiration);

        void Add(string key, object data, long dataLength, EventHandler<EntryRemovedEventArgs> removedHandler);

        Cache.CacheEntry Add(string key, object data, long dataLength, TimeSpan slidingExpiration);

        void Add(object key, object data, long dataLength, TimeSpan slidingExpiration, DateTime absoluteExpiration);

        void Clear();

        bool ContainsKey(ID key);

        bool ContainsKey(object key);

        ArrayList GetCacheKeys();

        ArrayList GetCacheKeys(string keyPrefix);

        Cache.CacheEntry GetEntry(object key, bool updateAccessed);

        object GetValue(object key);

        void Remove(object key);

        void Remove<TKey>(Predicate<TKey> predicate);

        void RemoveKeysContaining(string value);

        void RemovePrefix(string keyPrefix);

        void Scavenge();
    }
}
using System;
using System.Collections;

using Sitecore.Caching;
using Sitecore.Data;
using Sitecore.Diagnostics;
using Sitecore.Diagnostics.PerformanceCounters;

namespace Sitecore.Sandbox.Caching
{
    public class CacheProvider : ICacheProvider
    {
        private Cache Cache { get; set; }

        public CacheProvider(string cacheName, string cacheSize)
        {
            Assert.ArgumentNotNullOrEmpty(cacheName, "cacheName");
            Assert.ArgumentNotNullOrEmpty(cacheSize, "cacheSize");
            Cache = new Cache(cacheName, StringUtil.ParseSizeString(cacheSize));
        }

        public bool CacheWriteEnabled 
        {
            get
            {
                return Cache.CacheWriteEnabled;
            }
            set
            {
                Cache.CacheWriteEnabled = value;
            }
        }

        public int Count 
        {
            get
            {
                return Cache.Count;
            }
        }

        public CachePriority DefaultPriority 
        {
            get
            {
                return Cache.DefaultPriority;
            }
            set
            {
                Cache.DefaultPriority = value;
            }
        }

        public bool Enabled 
        {
            get
            {
                return Cache.Enabled;
            }
            set
            {
                Cache.Enabled = value;
            }
        }

        public AmountPerSecondCounter ExternalCacheClearingsCounter 
        {
            get
            {
                return Cache.ExternalCacheClearingsCounter;
            }
            set
            {
                Cache.ExternalCacheClearingsCounter = value;
            }
        }

        public ID Id 
        {
            get
            {
                return Cache.Id;
            }
        }

        public long MaxSize 
        {
            get
            {
                return Cache.MaxSize;
            }
            set
            {
                Cache.MaxSize = value;
            }
        }

        public string Name 
        {
            get
            {
                return Cache.Name;
            }
        }

        public long RemainingSpace
        {
            get
            {
                return Cache.RemainingSpace;
            }
        }

        public bool Scavengable 
        {
            get
            {
                return Cache.Scavengable;
            }
            set
            {
                Cache.Scavengable = value;
            }
        }

        public long Size
        {
            get
            {
                return Cache.Size;
            }
        }

        public object SyncRoot
        {
            get
            {
                return Cache.SyncRoot;
            }
        }

        public object this[object key] 
        { 
            get
            {
                return Cache[key];
            } 
        }

        public void Add(ID key, ICacheable data)
        {
            Cache.Add(key, data);
        }

        public void Add(ID key, string value)
        {
            Cache.Add(key, value);
        }

        public void Add(string key, ICacheable data)
        {
            Cache.Add(key, data);
        }

        public void Add(string key, ID value)
        {
            Cache.Add(key, value);
        }

        public Cache.CacheEntry Add(string key, string data)
        {
            return Cache.Add(key, data);
        }

        public void Add(ID key, object data, long dataLength)
        {
            Cache.Add(key, data, dataLength);
        }

        public void Add(object key, object data, long dataLength)
        {
            Cache.Add(key, data, dataLength);
        }

        public void Add(string key, object data, long dataLength)
        {
            Cache.Add(key, data, dataLength);
        }

        public void Add(object key, object data, long dataLength, DateTime absoluteExpiration)
        {
            Cache.Add(key, data, dataLength, absoluteExpiration);
        }

        public void Add(object key, object data, long dataLength, TimeSpan slidingExpiration)
        {
            Cache.Add(key, data, dataLength, slidingExpiration);
        }

        public Cache.CacheEntry Add(string key, object data, long dataLength, DateTime absoluteExpiration)
        {
            return Cache.Add(key, data, dataLength, absoluteExpiration);
        }

        public void Add(string key, object data, long dataLength, EventHandler<EntryRemovedEventArgs> removedHandler)
        {
            Cache.Add(key, data, dataLength, removedHandler);
        }

        public Cache.CacheEntry Add(string key, object data, long dataLength, TimeSpan slidingExpiration)
        {
            return Cache.Add(key, data, dataLength, slidingExpiration);
        }

        public void Add(object key, object data, long dataLength, TimeSpan slidingExpiration, DateTime absoluteExpiration)
        {
            Cache.Add(key, data, dataLength, slidingExpiration, absoluteExpiration);
        }

        public void Clear()
        {
            Cache.Clear();
        }

        public bool ContainsKey(ID key)
        {
            return Cache.ContainsKey(key);
        }

        public bool ContainsKey(object key)
        {
            return Cache.ContainsKey(key);
        }

        public ArrayList GetCacheKeys()
        {
            return Cache.GetCacheKeys();
        }

        public ArrayList GetCacheKeys(string keyPrefix)
        {
            return Cache.GetCacheKeys(keyPrefix);
        }

        public Cache.CacheEntry GetEntry(object key, bool updateAccessed)
        {
            return Cache.GetEntry(key, updateAccessed);
        }
        
        public object GetValue(object key)
        {
            return Cache.GetValue(key);
        }

        public void Remove(object key)
        {
            Cache.Remove(key);
        }

        public void Remove<TKey>(Predicate<TKey> predicate)
        {
            Cache.Remove<TKey>(predicate);
        }

        public void RemoveKeysContaining(string value)
        {
            Cache.RemoveKeysContaining(value);
        }

        public void RemovePrefix(string keyPrefix)
        {
            Cache.RemovePrefix(keyPrefix);
        }

        public void Scavenge()
        {
            Cache.Scavenge();
        }
    }
}

I’m not going to talk about the above interface or class since it just wraps Sitecore.Caching.Cache, and there isn’t much to talk about here.

Next, I spun up the following class that represents an entry in our custom SQL table:

using System;

using Sitecore.Caching;
using Sitecore.Data;
using Sitecore.Reflection;

using Newtonsoft.Json;

namespace Sitecore.Sandbox.Data.Providers.ItemUrls
{
    public class ItemUrlEntry : ICacheable, ICloneable
    {
        public ID ItemID { get; set; }

        public string Site { get; set; }
        
        public string Database { get; set; }
        
        public string Url { get; set; }

        bool cacheable;
        bool ICacheable.Cacheable
        {
            get
            {
                return cacheable;
            }
            set
            {
                cacheable = value;
            }
        }

        bool ICacheable.Immutable
        {
            get
            {
                return true;
            }
        }

        event DataLengthChangedDelegate ICacheable.DataLengthChanged
        {
            add
            {
            }
            remove
            {
            }
        }

        long ICacheable.GetDataLength()
        {
            return TypeUtil.SizeOfID()
                    + TypeUtil.SizeOfString(Site) 
                    + TypeUtil.SizeOfString(Database) 
                    + TypeUtil.SizeOfString(Url);
        }

        public object Clone()
        {
            return new ItemUrlEntry { ItemID = ItemID, Site = Site, Database = Database, Url = Url };
        }

        public override string ToString()
        {
            return JsonConvert.SerializeObject(this);
        }
    }
}

Entries can contain the ID of the Sitecore Item; the specific site we are storing this url for; and the target Database.

You’ll notice I’ve implemented the Sitecore.Caching.ICacheable interface. I’ve done this so I can store entries in cache for performance. I’m not going to go much into the details of how this works since there isn’t much to point out.

I also override the ToString() method for testing purposes. You’ll see this in action later on when we test this together.

Next, we need some sort of provider to manage these entries. I’ve defined the following interface for such a provider:

using System.Collections.Generic;

using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Sites;

namespace Sitecore.Sandbox.Data.Providers.ItemUrls
{
    public interface IItemUrlsProvider
    {
        void AddEntry(ItemUrlEntry entry);
        
        void RemoveEntry(ItemUrlEntry entry);

        Item GetItem(ItemUrlEntry entry);

        ItemUrlEntry GetEntry(ItemUrlEntry entry);

        IEnumerable<ItemUrlEntry> GetAllEntries();
    }
}

IItemUrlsProviders should have the ability to add/remove/retrieve entries. They should also offer the ability to get all entries — I need this for testing later on in this post.

Plus, as a “nice to have”, these providers should return a Sitecore Item for a given entry. Such would be useful when retrieving and setting the context Sitecore Item via a custom Item Resolver (you would typically have an <httpRequestBegin> pipeline processor that does this).

I then created the following class that implements the IItemUrlsProvider interface defined above. This class is specific to adding/removing/retrieving entries from a custom SQL database:

using System;
using System.Collections.Generic;

using Sitecore.Caching;
using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.DataProviders.Sql;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;

using Sitecore.Sandbox.Caching;

namespace Sitecore.Sandbox.Data.Providers.ItemUrls.SqlServer
{
    public class SqlServerItemUrlsProvider : IItemUrlsProvider
    {
        private SqlDataApi SqlDataApi { get; set; }

        protected ICacheProvider CacheProvider { get; private set; }

        protected string CachePrefix { get; private set; }

        public SqlServerItemUrlsProvider(SqlDataApi sqlDataApi, ICacheProvider cacheProvider, string cachePrefix)
        {
            Assert.ArgumentNotNull(sqlDataApi, "sqlDataApi");
            Assert.ArgumentNotNull(cacheProvider, "cacheProvider");
            Assert.ArgumentNotNullOrEmpty(cachePrefix, "cachePrefix");
            SqlDataApi = sqlDataApi;
            CacheProvider = cacheProvider;
            CachePrefix = cachePrefix;
        }

        public void AddEntry(ItemUrlEntry entry)
        {
            Assert.ArgumentNotNull(entry, "entry");
            Assert.ArgumentCondition(!ID.IsNullOrEmpty(entry.ItemID), "entry.ItemID", "entry.ItemID cannot be null or empty");
            Assert.ArgumentNotNullOrEmpty(entry.Site, "entry.Site");
            Assert.ArgumentNotNullOrEmpty(entry.Database, "entry.Database");
            Assert.ArgumentNotNullOrEmpty(entry.Url, "entry.Url");
            const string addEntrySql = "INSERT INTO {0}ItemUrls{1} ( {0}ItemID{1}, {0}Site{1}, {0}Database{1}, {0}Url{1} ) VALUES ( {2}itemID{3}, {2}site{3}, {2}database{3}, {2}url{3} )";
            var success = Factory.GetRetryer().Execute(() =>
            {
                object[] parameters = new object[] { "itemID", entry.ItemID, "site", entry.Site, "database", entry.Database, "url", entry.Url };
                return SqlDataApi.Execute(addEntrySql, parameters) > 0;
            });

            if (success)
            {
                AddToCache(entry);
            }
        }

        public void RemoveEntry(ItemUrlEntry entry)
        {
            const string deleteEntrySql = "DELETE FROM {0}ItemUrls{1} WHERE {0}Site{1} = {2}site{3} AND {0}Database{1} = {2}database{3} AND {0}Url{1} = {2}url{3}";
            var success = Factory.GetRetryer().Execute(() =>
            {
                object[] parameters = new object[] { "site", entry.Site, "database", entry.Database, "url", entry.Url };
                return SqlDataApi.Execute(deleteEntrySql, parameters) > 0;
            });

            if (success)
            {
                RemoveFromCache(entry);
            }
        }

        public Item GetItem(ItemUrlEntry entry)
        {
            ItemUrlEntry foundEntry = GetEntry(entry);
            if(foundEntry == null)
            {
                return null;
            }

            Database database = Factory.GetDatabase(foundEntry.Database);
            if(database == null)
            {
                return null;
            }

            try
            {
                return database.Items[foundEntry.ItemID];
            }
            catch(Exception ex)
            {
                Log.Error(ToString(), ex, this);
            }

            return null;
        }

        public ItemUrlEntry GetEntry(ItemUrlEntry entry)
        {
            ItemUrlEntry foundEntry = GetFromCache(entry);
            if (foundEntry != null)
            {
                return foundEntry;
            }

            const string getEntrySql = "SELECT {0}ItemID{1} FROM {0}ItemUrls{1} WHERE {2}Site = {2}site{3} AND {2}Database{3} = {2}database{3} AND {0}Url{1} = {2}url{3}";
            object[] parameters = new object[] { "site", entry.Site, "database", entry.Database, "url", entry.Url };
            using (DataProviderReader reader = SqlDataApi.CreateReader(getEntrySql, parameters))
            {
                if (!reader.Read())
                {
                    return null;
                }

                ID itemID = ID.Parse(SqlDataApi.GetGuid(0, reader));
                if (ID.IsNullOrEmpty(itemID))
                {
                    return null;
                }

                foundEntry = entry.Clone() as ItemUrlEntry;
                foundEntry.ItemID = itemID;
                AddToCache(entry);
                return foundEntry;
            }
        }

        public IEnumerable<ItemUrlEntry> GetAllEntries()
        {
            const string getAllEntriesSql = "SELECT {0}ItemID{1}, {0}Site{1}, {0}Database{1}, {0}Url{1} FROM {0}ItemUrls{1}";
            IList<ItemUrlEntry> entries = new List<ItemUrlEntry>();
            using (DataProviderReader reader = SqlDataApi.CreateReader(getAllEntriesSql, new object[0]))
            {
                while(reader.Read())
                {
                    ID itemID = ID.Parse(SqlDataApi.GetGuid(0, reader));
                    if (!ID.IsNullOrEmpty(itemID))
                    {
                        entries.Add
                        (
                            new ItemUrlEntry 
                            {
                                ItemID = itemID, 
                                Site = SqlDataApi.GetString(1, reader), 
                                Database = SqlDataApi.GetString(2, reader), 
                                Url =  SqlDataApi.GetString(3, reader)
                            }
                        );
                    } 
                }
            }

            return entries;
        }

        protected virtual void AddToCache(ItemUrlEntry entry)
        {
            CacheProvider.Add(GetCacheKey(entry), entry);
        }

        protected virtual void RemoveFromCache(ItemUrlEntry entry)
        {
            CacheProvider.Remove(GetCacheKey(entry));
        }

        protected virtual ItemUrlEntry GetFromCache(ItemUrlEntry entry)
        {
            return CacheProvider[GetCacheKey(entry)] as ItemUrlEntry;
        }

        protected virtual string GetCacheKey(ItemUrlEntry entry)
        {
            Assert.ArgumentNotNull(entry, "entry");
            Assert.ArgumentNotNull(entry.Site, "entry.Site");
            Assert.ArgumentNotNull(entry.Database, "entry.Database");
            Assert.ArgumentNotNull(entry.Url, "entry.Url");
            return string.Join("#", CachePrefix, entry.Site, entry.Database, entry.Url);
        }
    }
}

Sitecore.Data.DataProviders.Sql.SqlDataApi and ICacheProvider instances along with a cache prefix are injected into the class instance’s constructor using the Sitecore Configuration Factory (you’ll get a better idea of how this happens when you have a look at the patch configuration file towards the bottom of this post). These are saved to properties on the class instance so they can be leveraged by the methods on the class.

One thing I would like to point out is the Sitecore.Data.DataProviders.Sql.SqlDataApi class is an abstraction — it’s an abstract class that is subclassed by Sitecore.Data.SqlServer.SqlServerDataApi in Sitecore.Kernel.dll. This concrete class does most of the leg work on talking to the SQL Server database, and we just utilize methods on it for adding/deleting/removing entries.

The AddEntry() method delegates the database saving operation to the Sitecore.Data.DataProviders.Sql.SqlDataApi instance, and then uses the ICacheProvider instance for storing the entry in cache.

The RemoveEntry() method also leverages the Sitecore.Data.DataProviders.Sql.SqlDataApi instance for deleting the entry from the database, and then removes the entry from cache via the ICacheProvider instance.

The GetEntry() method does exactly what you think it does. It tries to get the entry first from cache via the ICacheProvider instance and then the database via the Sitecore.Data.DataProviders.Sql.SqlDataApi instance if the entry was not found in cache. If the Item was not in cache but was in the database, the GetEntry() method then saves the entry to cache.

I then created the following Singleton for testing:

using System;
using System.Collections.Generic;

using Sitecore.Configuration;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Data.Providers.ItemUrls
{
    public class ItemUrlsProvider : IItemUrlsProvider
    {
        private static readonly Lazy<IItemUrlsProvider> lazyInstance = new Lazy<IItemUrlsProvider>(() => new ItemUrlsProvider());

        public static IItemUrlsProvider Current { get { return lazyInstance.Value; } }

        private IItemUrlsProvider InnerProvider { get; set; }

        private ItemUrlsProvider()
        {
            InnerProvider = GetInnerProvider();
        }

        public void AddEntry(ItemUrlEntry entry)
        {
            InnerProvider.AddEntry(entry);
        }

        public void RemoveEntry(ItemUrlEntry entry)
        {
            InnerProvider.RemoveEntry(entry);
        }

        public Item GetItem(ItemUrlEntry entry)
        {
            return InnerProvider.GetItem(entry);
        }

        public ItemUrlEntry GetEntry(ItemUrlEntry entry)
        {
            return InnerProvider.GetEntry(entry);
        }

        public IEnumerable<ItemUrlEntry> GetAllEntries()
        {
            return InnerProvider.GetAllEntries();
        }

        protected virtual IItemUrlsProvider GetInnerProvider()
        {
            IItemUrlsProvider provider = Factory.CreateObject("itemUrlsProvider", true) as IItemUrlsProvider;
            Assert.IsNotNull(provider, "itemUrlsProvider must be set in configuration!");
            return provider;
        }
    }
}

The Singleton above basically decorates the IItemUrlsProvider instance defined in Sitecore configuration — see the configuration file below — and delegates method calls to it.

I then wired everything together using the following patch configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <itemUrlsProvider id="custom" type="Sitecore.Sandbox.Data.Providers.ItemUrls.$(database).$(database)ItemUrlsProvider, Sitecore.Sandbox" singleInstance="true">
      <param type="Sitecore.Data.$(database).$(database)DataApi, Sitecore.Kernel" desc="sqlDataApi">
        <param connectionStringName="$(id)"/>
      </param>
      <param type="Sitecore.Sandbox.Caching.CacheProvider, Sitecore.Sandbox" desc="cacheProvider">
        <param desc="cacheName">[ItemUrls]</param>
        <param desc="cacheSize">500KB</param>
      </param>
      <param desc="cachePrefix">ItemUrlsEntry</param>
    </itemUrlsProvider>
  </sitecore>
</configuration>

For testing, I whipped up a standalone ASP.NET Web Form (yes, there are more elegant ways to do this but it’s Sunday so cut me some slack 😉 ):

using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;

using Sitecore.Data.Items;

using Sitecore.Sandbox.Data.Providers.ItemUrls;

namespace Sitecore.Sandbox.Web.tests
{
    public partial class ItemUrlsProviderTest : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            IItemUrlsProvider provider = ItemUrlsProvider.Current;
            Item home = Sitecore.Context.Database.GetItem("/sitecore/content/home");
            StringBuilder output = new StringBuilder();
            
            ItemUrlEntry firstEntry = new ItemUrlEntry { ItemID = home.ID, Site = Sitecore.Context.Site.Name, Database = Sitecore.Context.Database.Name, Url = "/this/does/not/exist" };
            output.AppendFormat("Adding {0} as an entry.<br />", firstEntry);
            provider.AddEntry(firstEntry);

            ItemUrlEntry secondEntry = new ItemUrlEntry { ItemID = home.ID, Site = Sitecore.Context.Site.Name, Database = Sitecore.Context.Database.Name, Url = "/fake/url" };
            output.AppendFormat("Adding {0} as an entry.<br />", secondEntry);
            provider.AddEntry(secondEntry);

            ItemUrlEntry thirdEntry = new ItemUrlEntry { ItemID = home.ID, Site = Sitecore.Context.Site.Name, Database = Sitecore.Context.Database.Name, Url = "/another/fake/url" };
            output.AppendFormat("Adding {0} as an entry.<hr />", thirdEntry);
            provider.AddEntry(thirdEntry);

            ItemUrlEntry fourthEntry = new ItemUrlEntry { ItemID = home.ID, Site = Sitecore.Context.Site.Name, Database = Sitecore.Context.Database.Name, Url = "/blah/blah/blah" };
            output.AppendFormat("Adding {0} as an entry.<hr />", fourthEntry);
            provider.AddEntry(fourthEntry);

            ItemUrlEntry fifthEntry = new ItemUrlEntry { ItemID = home.ID, Site = Sitecore.Context.Site.Name, Database = Sitecore.Context.Database.Name, Url = "/i/am/a/url" };
            output.AppendFormat("Adding {0} as an entry.<hr />", fifthEntry);
            provider.AddEntry(fifthEntry);

            output.AppendFormat("Current saved entries:<br /><br />{0}<hr />", string.Join("<br />", provider.GetAllEntries().Select(entry => entry.ToString())));

            output.AppendFormat("Removing entry {0}.<br /><br />", firstEntry.ToString());
            provider.RemoveEntry(firstEntry);

            output.AppendFormat("Current saved entries:<br /><br />{0}", string.Join("<br />", provider.GetAllEntries().Select(entry => entry.ToString())));
            litResults.Text = output.ToString();
        }
    }
}

The test above adds five entries, and then deletes one. It also outputs what’s in the database after specific operations.

After doing a build, I pulled up the above Web Form in my browser and saw this once the page was done rendering:

ItemUrlsProviderTest

If you have any thoughts on this, please share in a comment.

Until next time, have a Sitecoredatalicious day!

Augment Functionality in Sitecore Using the Decorator Design Pattern

Over the past few days, I’ve been trying to come up with a good idea for a blog post showing the usage of the Decorator design pattern in Sitecore.

During this time of cogitation, I was having difficulties coming up with a good example despite having had used this pattern in Sitecore on many past projects — I can’t really share those solutions since they are owned by either previous employers or clients.

However, I finally had an “EUREKA!” moment after John West — CTO of Sitecore USA — wrote a blog post earlier today where he shared an <httpRequestBegin> pipeline processor which redirects to a canonical URL for an Item.

So, what exactly did I come up with?

I built the following example which simply “decorates” the “out of the box” ItemResolver — Sitecore.Pipelines.HttpRequest.ItemResolver in Sitecore.Kernel.dll — which is used as an <httpRequestBegin> pipeline processor to figure out what the context Item should be from the URL being requested by looking for an entry in the IDTable in Sitecore (note: this idea is adapted from a blog post that Alex Shyba — Director of Platform Innovation and Engineering at Sitecore — wrote a few years ago):

using Sitecore;
using Sitecore.Data;
using Sitecore.Data.IDTables;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.HttpRequest;

namespace Sitecore.Sandbox.Pipelines.HttpRequest
{
    public class IDTableItemResolver : HttpRequestProcessor
    {
        private string Prefix { get; set; }

        private HttpRequestProcessor InnerProcessor { get; set; }

        public override void Process(HttpRequestArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            AssertProperties();
            
            Item item = GetItem(args.Url.FilePath);
            if (item == null)
            {
                InnerProcessor.Process(args);       
                return;
            }

            Context.Item = item; 
        }

        protected virtual void AssertProperties()
        {
            Assert.IsNotNullOrEmpty(Prefix, "Prefix", "Prefix must be set in configuration!");
            Assert.IsNotNull(InnerProcessor, "InnerProcessor", "InnerProcessor must be set in configuration!");
        }

        protected virtual Item GetItem(string url)
        {
            IDTableEntry entry = IDTable.GetID(Prefix, url);
            if (entry == null || entry.ID.IsNull)
            {
                return null;
            }

            return GetItem(entry.ID);
        }

        protected Item GetItem(ID id)
        {
            Database database = GetDatabase();
            if (database == null)
            {
                return null;
            }

            return database.GetItem(id);
        }

        protected virtual Database GetDatabase()
        {
            return Context.Database;
        }
    }
}

What is the above class doing? It’s basically seeing if it can find an Item for the passed relative URL — this is passed via the FilePath property of the Url property of the HttpRequestArgs instance taken in by the Process() method — by delegating to a method that looks up an entry in the IDTable for the URL — the URL would be the key into the IDTable — and return the Item from the context database if an entry is found. If no entry is found, it just returns null.

If null is returned, that pretty much means there is no entry in the IDTable for the given relative URL so a delegation to the Process() method of the InnerProcessor is needed in order to preserve “out of the box” Sitecore functionality for Item URL resolution.

I then replaced the “out of the box” ItemResolver with the above in the following patch include configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <httpRequestBegin>
        <processor patch:instead="*[@type='Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel']" 
                   type="Sitecore.Sandbox.Pipelines.HttpRequest.IDTableItemResolver, Sitecore.Sandbox">
          <Prefix>UrlRewrite</Prefix>
          <InnerProcessor type="Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel" />
        </processor>  
      </httpRequestBegin>
    </pipelines>
  </sitecore>
</configuration>

In the above configuration file, we are setting the “out of the box” ItemResolver to be injected into the class above so that its Process() method can be “decorated”.

Let’s see this in action!

Let’s try this out with the following page Item:

mario-content-editor

In order to see the above <httpRequestBegin> pipeline processor in action, I had to add an entry into my IDTable — let’s make pretend an UrlRewrite module on the Sitecore Marketplace added this entry for us:

url-rewrite-id-table

I loaded up another browser window; navigated to the relative URL specified in the IDTable entry; and then saw the following:

resolved-url

As you can see, it worked.

We can also navigate to the same page using its true URL — the one resolved by Sitecore “out of the box”:

mario-resolved

The above worked because the inner processor resolved it.

Let’s now go to a completely different page Item altogether. Let’s use this one:

cat-page-five-sitecore

As you can see, that also worked:

cat-page-five

If you have any thoughts on this, or have other ideas around using the Decorator pattern in Sitecore, please share in a comment.

Augment Configuration-defined Sitecore Functionality using the Composite Design Pattern

In a couple of my past posts — Synchronize IDTable Entries Across Multiple Sitecore Databases Using a Composite IDTableProvider and
Chain Together Sitecore Client Commands using a Composite Command — I used the Composite design pattern to chain together functionality in two or more classes with the same interface — by interface here, I don’t strictly mean a C# interface but any class that servers as a base class for others where all instances share the same methods — and had a thought: how could I go about making a generic solution to chain together any class type defined in Sitecore configuration?

As a “proof of concept” I came up with the following solution while taking a break from my judgely duties reviewing 2015 Sitecore Hackathon modules.

I first defined an interface for classes that will construct objects using the Sitecore Configuration Factory:

using System.Collections.Generic;
using System.Xml;

namespace Sitecore.Sandbox.Shared
{
    public interface IConfigurationFactoryInstances<TInstance>
    {
        void AddInstance(XmlNode source);

        IEnumerable<TInstance> GetInstances();
    }
}

The following class implements the methods defined in the interface above:

using System.Collections.Generic;
using System.Xml;

using Sitecore.Configuration;

namespace Sitecore.Sandbox.Shared
{
    public class ConfigurationFactoryInstances<TInstance> : IConfigurationFactoryInstances<TInstance> where TInstance : class
    {
        private IList<TInstance> Instances { get; set; }

        public ConfigurationFactoryInstances()
        {
            Instances = new List<TInstance>();
        }

        public virtual IEnumerable<TInstance> GetInstances()
        {
            return Instances;
        }

        public void AddInstance(XmlNode configNode)
        {
            TInstance instance = CreateInstance(configNode);
            if (instance == null)
            {
                return;
            }

            Instances.Add(instance);
        }

        protected virtual TInstance CreateInstance(XmlNode configNode)
        {
            if (configNode == null)
            {
                return null;
            }

            TInstance instance = Factory.CreateObject(configNode, true) as TInstance;
            if (instance == null)
            {
                return null;
            }

            return instance;
        }
    }
}

The AddInstance() method in the class above — along with the help of the CreateInstance() method — takes in a System.Xml.XmlNode instance and attempts to create an instance of the type denoted by TInstance using the Sitecore Configuration Factory. If the instance was successfully created (i.e. it’s not null), it is added to a list.

The GetInstances() method in the class above just returns the list of the instances that were added by the AddInstance() method.

Since I’ve been posting a lot of meme images on Twitter lately — you can see the evidence here — I’ve decided to have a little fun tonight with this “proof of concept”, and created the following composite MediaProvider:

using System.Xml;

using Sitecore.Data.Items;
using Sitecore.Resources.Media;

using Sitecore.Sandbox.Shared;

namespace Sitecore.Sandbox.Resources.Media
{
    public class CompositeMediaProvider : MediaProvider
    {
        private static IConfigurationFactoryInstances<MediaProvider> Instances { get; set; }

        static CompositeMediaProvider()
        {
            Instances = new ConfigurationFactoryInstances<MediaProvider>();
        }

        public override string GetMediaUrl(MediaItem item, MediaUrlOptions options)
        {
            foreach (MediaProvider mediaProvider in Instances.GetInstances())
            {
                string url = mediaProvider.GetMediaUrl(item, options);
                if(!string.IsNullOrWhiteSpace(url))
                {
                    return url;
                }
            }

            return base.GetMediaUrl(item, options);
        }

        protected virtual void AddMediaProvider(XmlNode configNode)
        {
            Instances.AddInstance(configNode);
        }
    }
}

The AddMediaProvider() method in the class above adds new instances of Sitecore.Resources.Media.MediaProvider through delegation to an instance of the ConfigurationFactoryInstances class. The Sitecore Configuration Factory will call the AddMediaProvider() method since it’s defined in the patch include configuration file shown later in this post

The GetMediaUrl() method iterates over all instances of Sitecore.Resources.Media.MediaProvider that were created and stored by the ConfigurationFactoryInstances instance, and calls each of their GetMediaUrl() methods. The first non-null or empty URL from one of these “inner” instances is returned to the caller. If none of the instances return a URL, then the class above returns the value given by its base class’ GetMediaUrl() method.

I then spun up three MediaProvider classes to serve up specific image URLs of John West — I found these somewhere on the internet 😉 — when they encounter media Items with specific names (I am not advocating that anyone hard-codes anything like this — these classes are only here to serve as examples):

using System;

using Sitecore.Data.Items;
using Sitecore.Resources.Media;

namespace Sitecore.Sandbox.Resources.Media
{
    public class JohnWestOneMediaProvider : MediaProvider
    {
        public override string GetMediaUrl(MediaItem item, MediaUrlOptions options)
        {
            if(item.Name.Equals("john-west-1", StringComparison.CurrentCultureIgnoreCase))
            {
                return "http://cdn.meme.am/instances/500x/43030540.jpg";
            }

            return string.Empty;
        }
    }
}

using System;

using Sitecore.Data.Items;
using Sitecore.Resources.Media;

namespace Sitecore.Sandbox.Resources.Media
{
    public class JohnWestTwoMediaProvider : MediaProvider
    {
        public override string GetMediaUrl(MediaItem item, MediaUrlOptions options)
        {
            if (item.Name.Equals("john-west-2", StringComparison.CurrentCultureIgnoreCase))
            {
                return "http://cdn.meme.am/instances/500x/43044627.jpg";
            }

            return string.Empty;
        }
    }
}
using System;

using Sitecore.Data.Items;
using Sitecore.Resources.Media;

namespace Sitecore.Sandbox.Resources.Media
{
    public class JohnWestThreeMediaProvider : MediaProvider
    {
        public override string GetMediaUrl(MediaItem item, MediaUrlOptions options)
        {
            if (item.Name.Equals("john-west-3", StringComparison.CurrentCultureIgnoreCase))
            {
                return "http://cdn.meme.am/instances/500x/43030625.jpg";
            }

            return string.Empty;
        }
    }
}

I then registered all of the above in Sitecore using a patch configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <mediaLibrary>
      <mediaProvider>
        <patch:attribute name="type">Sitecore.Sandbox.Resources.Media.CompositeMediaProvider, Sitecore.Sandbox</patch:attribute>
        <mediaProviders hint="raw:AddMediaProvider">
          <mediaProvider type="Sitecore.Sandbox.Resources.Media.JohnWestOneMediaProvider, Sitecore.Sandbox" />
          <mediaProvider type="Sitecore.Sandbox.Resources.Media.JohnWestTwoMediaProvider, Sitecore.Sandbox" />
          <mediaProvider type="Sitecore.Sandbox.Resources.Media.JohnWestThreeMediaProvider, Sitecore.Sandbox" />
          <mediaProvider type="Sitecore.Resources.Media.MediaProvider, Sitecore.Kernel" />
        </mediaProviders>
      </mediaProvider>
    </mediaLibrary>
  </sitecore>
</configuration>

Let’s see this in action!

To test, I uploaded four identical photos of John West to the Media Library:

four-jw-media-library

I then inserted these into a Rich Text field on my home Item:

rte-jw-times-four

I saved my home Item and published everything. Once the publish was finished, I navigated to my home page and saw the following:

four-jw-home-page

As you can see, the three custom John West MediaProvider class instances served up their URLs, and the “out of the box” Sitecore.Resources.Media.MediaProvider instance served up its URL on the last image.

If you have any thoughts on this, please share in a comment.

Until next time, have a Sitecoretastic day!

Embedded Tweets in Sitecore: A Proof of Concept

In a previous post, I showcased a “proof of concept” for shortcodes in Sitecore — this is a shorthand notation for embedding things like YouTube videos in your webpages without having to type up a bunch of HTML — and felt I should follow up with another “proof of concept” around incorporating Embedded Tweets in Sitecore.

You might be asking “what’s an Embedded Tweet?” An Embedded Tweet is basically the process of pasting a Tweet URL from Twitter into an editable content area of your website/blog/whatever (think Rich Text field in Sitecore), and let the code that builds the HTML for your site figure out how to display it.

For example, I had used an Embedded Tweet in a recent post:

tweet-url-wordpress

This is what is seen on the rendered page:

tweet-embedded

While doing some research via Google on how to do this in Sitecore, I found this page from Twitter that discusses how you could go about accomplishing this, and discovered how to get JSON containing information about a Tweet — including its HTML — using one of Twitter’s API URLs:

tweet-api-json

The JSON above drove me to build the following POCO class to represent data returned by that URL:

using System.Runtime.Serialization;

namespace Sitecore.Sandbox.Pipelines.RenderField.Tweets
{
    public class Tweet
    {
        [DataMember(Name = "cache_age")]
        public int CacheAgeMilliseconds { get; set; }

        [DataMember(Name = "url")]
        public string Url { get; set; }

        [DataMember(Name = "html")]
        public string Html { get; set; }
    }
}

I decided to omit some of the JSON properties returned by the Twitter URL from my class above — width and height are examples — since I felt I did not need to use them for this “proof of concept”.

I then leveraged the class above in the following class that will serve as a <renderField> pipeline processor to embed Tweets:

using System;
using System.IO;
using System.Net;
using System.Text.RegularExpressions;
using System.Web;

using Sitecore.Caching;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.RenderField;

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Sitecore.Sandbox.Pipelines.RenderField.Tweets
{
    public class ExpandTweets
    {
        private string TwitterWidgetScriptTag {get ; set; }

        private string TwitterApiUrlFormat { get; set; }

        private string _TweetPattern;
        private string TweetPattern 
        {
            get
            {
                return _TweetPattern;
            }
            set
            {
                _TweetPattern = value;
                if (!string.IsNullOrWhiteSpace(_TweetPattern))
                {
                    _TweetPattern = HttpUtility.HtmlDecode(_TweetPattern);
                }
            }
        }

        private HtmlCache _HtmlCache;
        private HtmlCache HtmlCache
        {
            get
            {
                if (_HtmlCache == null)
                {
                    _HtmlCache = CacheManager.GetHtmlCache(Context.Site);
                }

                return _HtmlCache;
            }
        }

        public void Process(RenderFieldArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            AssertRequired();
            if(!ShouldFieldBeProcessed(args))
            {
                return;
            }

            args.Result.FirstPart = ExpandTweetUrls(args.Result.FirstPart);
        }

        private static bool ShouldFieldBeProcessed(RenderFieldArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.FieldTypeKey, "args.FieldTypeKey");
            string fieldTypeKey = args.FieldTypeKey.ToLower();
            return fieldTypeKey == "text"
                    || fieldTypeKey == "rich text"
                    || fieldTypeKey == "single-line text"
                    || fieldTypeKey == "multi-line text";
        }

        private void AssertRequired()
        {
            Assert.IsNotNullOrEmpty(TwitterWidgetScriptTag, "TwitterWidgetScriptTag must be set! Check your configuration!");
            Assert.IsNotNullOrEmpty(TwitterApiUrlFormat, "TwitterApiUrlFormat must be set! Check your configuration!");
            Assert.IsNotNullOrEmpty(TweetPattern, "TweetPattern must be set! Check your configuration!");
        }

        protected virtual string ExpandTweetUrls(string html)
        {
            string htmlExpanded = html;
            MatchCollection matches = Regex.Matches(htmlExpanded, TweetPattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);
            foreach (Match match in matches)
            {
                string tweetHtml = GetTweetHtml(match.Groups["id"].Value);
                if (!string.IsNullOrWhiteSpace(tweetHtml))
                {
                    htmlExpanded = htmlExpanded.Replace(match.Value, tweetHtml);
                }
            }

            if (matches.Count > 0)
            {
                htmlExpanded = string.Concat(htmlExpanded, TwitterWidgetScriptTag);
            }

            return htmlExpanded;
        }

        protected virtual string GetTweetHtml(string id)
        {
            string html = GetTweetHtmlFromCache(id);
            if (!string.IsNullOrWhiteSpace(html))
            {
                return html;
            }

            Tweet tweet = GetTweetFromApi(id);
            AddTweetHtmlToCache(id, tweet);
            return tweet.Html;
        }

        private string GetTweetHtmlFromCache(string id)
        {
            return HtmlCache.GetHtml(id);
        }

        private void AddTweetHtmlToCache(string id, Tweet tweet)
        {
            if (string.IsNullOrWhiteSpace(tweet.Html))
            {
                return;
            }

            if (tweet.CacheAgeMilliseconds > 0)
            {
                HtmlCache.SetHtml(id, tweet.Html, DateTime.Now.AddMilliseconds(tweet.CacheAgeMilliseconds));
                return;
            }

            HtmlCache.SetHtml(id, tweet.Html);
        }

        protected virtual Tweet GetTweetFromApi(string id)
        {
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(string.Format(TwitterApiUrlFormat, id));
            try
            {
                HttpWebResponse response = (HttpWebResponse)request.GetResponse();
                using (StreamReader reader = new StreamReader(response.GetResponseStream()))
                {
                    var result = reader.ReadToEnd();
                    JObject jObject = JObject.Parse(result);
                    return JsonConvert.DeserializeObject<Tweet>(jObject.ToString());
                }
            }
            catch (Exception ex)
            {
                Log.Error(this.ToString(), ex, this);
            }

            return new Tweet { Html = string.Empty };
        }
    }
}

Methods in the class above find all Tweet URLs in the Rich Text, Single-Line Text, or Multi-Line Text field being processed — the code determines if it’s a Tweet URL based on a pattern that is supplied by a configuration setting (you will see this below in this post); extract Tweets’ Twitter identifiers (these are located at the end of the Tweet URLs); and attempt to find the Tweets’ HTML in Sitecore’s HTML cache.

If the HTML is found in cache for a Tweet, we return it. Otherwise, we make a request to Twitter’s API to get it, put it in cache one we have it (it is set to expire after a specified number of milliseconds from the time it was retrieved: Twitter returns the number of milliseconds in one full year by default), and then we return it.

If the returned HTML is not empty, we replace it in the field’s value for display.

If the HTML returned is empty — this could happen when an exception is encountered during the Twitter API call (of course we log the exception in the Sitecore log when this happens 😉 ) — we don’t touch the Tweet URL in the field’s value.

Once all Tweet URLs have been processed, we append a script tag referencing Twitter’s widget.js file — this is supplied through a configuration setting, and it does the heavy lifting on making the Tweet HTML look Twitterific 😉 — to the field’s rendered HTML.

I then tied everything together using the following patch configuration file:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <renderField>
        <processor type="Sitecore.Sandbox.Pipelines.RenderField.Tweets.ExpandTweets, Sitecore.Sandbox"
					patch:after="processor[@type='Sitecore.Pipelines.RenderField.GetTextFieldValue, Sitecore.Kernel']">
          <TwitterWidgetScriptTag>&lt;script async src="//platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;</TwitterWidgetScriptTag>
          <TwitterApiUrlFormat>https://api.twitter.com/1/statuses/oembed.json?id={0}&amp;omit_script=true</TwitterApiUrlFormat>
          <TweetPattern>https://twitter.com/.+/status/(?&lt;id&gt;\d*)</TweetPattern>
        </processor>
      </renderField>
    </pipelines>
  </sitecore>
</configuration>

Let’s see this in action!

I created a test Item, and added some legitimate and bogus Tweet URLs into one of its Rich Text fields (please pardon the grammatical issues in the following screenshots :-/):

tweets-rich-text

This isn’t the most aesthetically pleasing HTML, but it will serve its purpose for testing:

tweets-rich-text-html

After saving and publishing, I navigated to my test Item’s page, and saw this:

tweets-front-end

If you have any suggestions on making this better, or have other ideas for embedding Tweets in Sitecore, please share in a comment.