Expand New Tokens Added to Standard Values on All Items Using Its Template in Sitecore
If you have read some of my older posts, you probably know by now how much I love writing code that expands tokens on Items in Sitecore, and decided to build another solution that expands new tokens added to Standard Values Items of Templates — out of the box, these aren’t expanded on preexisting Items that use the Template of the Standard Values Item, and end up making their way in fields on those preexisting Items (for an alternative solution, check out this older post I wrote some time ago).
In the following solution — this solution is primarily composed of a custom pipeline — tokens that are added to fields on the Standard Values Item will be expanded on all Items that use the Template of the Standard Values Item after the Standard Values Item is saved in the Sitecore client (I hook into the <saveUI> pipeline for this action on save).
We first need a class whose instance serves as the custom pipeline’s argument object:
using Sitecore.Data.Items; using Sitecore.Pipelines; using System.Collections.Generic; namespace Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems { public class ExpandNewTokensOnAllItemsArgs : PipelineArgs { public Item StandardValuesItem { get; set; } private List<Item> items; public List<Item> Items { get { if(items == null) { items = new List<Item>(); } return items; } set { items = value; } } } }
The caller of the custom pipeline is required to pass the Standard Values Item that contains the new tokens. One of the processors of the custom pipeline will collect all Items that use its Template — these are stored in the Items collection property.
The instance of the following class serves as the first processor of the custom pipeline:
using System; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Data.Managers; using Sitecore.Diagnostics; namespace Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems { public class EnsureStandardValues { public void Process(ExpandNewTokensOnAllItemsArgs args) { Assert.ArgumentNotNull(args, "args"); Assert.ArgumentNotNull(args.StandardValuesItem, "args.StandardValuesItem"); if(IsStandardValues(args.StandardValuesItem)) { return; } args.AbortPipeline(); } protected virtual bool IsStandardValues(Item item) { Assert.ArgumentNotNull(item, "item"); return StandardValuesManager.IsStandardValuesHolder(item); } } }
This processor basically just ascertains whether the Item passed as the Standard Values Item is indeed a Standard Values Item — the code just delegates to the static IsStandardValuesHolder() method on Sitecore.Data.StandardValuesManager (this lives in Sitecore.Kernel.dll).
The instance of the next class serves as the second step of the custom pipeline:
using System.Collections.Generic; using System.Linq; using Sitecore.Data.Fields; using Sitecore.Diagnostics; namespace Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems { public class EnsureUnexpandedTokens { private List<string> Tokens { get; set; } public EnsureUnexpandedTokens() { Tokens = new List<string>(); } public void Process(ExpandNewTokensOnAllItemsArgs args) { Assert.ArgumentNotNull(args, "args"); Assert.ArgumentNotNull(args.StandardValuesItem, "args.StandardValuesItem"); if (!Tokens.Any()) { args.AbortPipeline(); return; } args.StandardValuesItem.Fields.ReadAll(); foreach(Field field in args.StandardValuesItem.Fields) { if(HasUnexpandedTokens(field)) { return; } } args.AbortPipeline(); } protected virtual bool HasUnexpandedTokens(Field field) { Assert.ArgumentNotNull(field, "field"); foreach(string token in Tokens) { if(field.Value.Contains(token)) { return true; } } return false; } } }
A collection of tokens are injected into the class’ instance via the Sitecore Configuration Factory — see the patch configuration file further down in this post — and determines if tokens exist in any of its fields. If no tokens are found, then the pipeline is aborted. Otherwise, we exit the Process() method immediately.
The instance of the following class serves as the third processor of the custom pipeline:
using System; using System.Collections.Generic; using System.Linq; using Sitecore.ContentSearch; using Sitecore.ContentSearch.SearchTypes; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Data.Managers; using Sitecore.Diagnostics; namespace Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems { public class CollectAllItems { public void Process(ExpandNewTokensOnAllItemsArgs args) { Assert.ArgumentNotNull(args, "args"); Assert.ArgumentNotNull(args.StandardValuesItem, "args.StandardValuesItem"); args.Items = GetAllItemsByTemplateID(args.StandardValuesItem.TemplateID); if(args.Items.Any()) { return; } args.AbortPipeline(); } protected virtual List<Item> GetAllItemsByTemplateID(ID templateID) { Assert.ArgumentCondition(!ID.IsNullOrEmpty(templateID), "templateID", "templateID cannot be null or empty!"); using (var context = ContentSearchManager.GetIndex("sitecore_master_index").CreateSearchContext()) { var query = context.GetQueryable<SearchResultItem>().Where(i => i.TemplateId == templateID); return query.ToList().Select(result => result.GetItem()).ToList(); } } } }
This class uses the Sitecore.ContentSearch API to find all Items that use the Template of the Standard Values Item. If at least one Item is found, we exit the Process() method immediately. Otherwise, we abort the pipeline.
The instance of the class below serves as the fourth processor of the custom pipeline:
using System; using System.Collections.Generic; using System.Linq; using Sitecore.ContentSearch; using Sitecore.ContentSearch.SearchTypes; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Data.Managers; using Sitecore.Diagnostics; namespace Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems { public class FilterStandardValuesItem { public void Process(ExpandNewTokensOnAllItemsArgs args) { Assert.ArgumentNotNull(args, "args"); Assert.ArgumentNotNull(args.Items, "args.Items"); if(!args.Items.Any()) { return; } args.Items = args.Items.Where(item => !IsStandardValues(item)).ToList(); } protected virtual bool IsStandardValues(Item item) { Assert.ArgumentNotNull(item, "item"); return StandardValuesManager.IsStandardValuesHolder(item); } } }
The code in this class ensures the Stardard Values Item is not in the collection of Items. It’s probably not a good idea to expand tokens on the Standard Values Item. 🙂
The instance of the next class serves as the final processor of the custom pipeline:
using System.Linq; using Sitecore.Configuration; using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.Data; namespace Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems { public class ExpandTokens { private MasterVariablesReplacer TokenReplacer { get; set; } public ExpandTokens() { TokenReplacer = GetTokenReplacer(); } public void Process(ExpandNewTokensOnAllItemsArgs args) { Assert.ArgumentNotNull(args, "args"); Assert.ArgumentNotNull(args.Items, "args.Items"); if (!args.Items.Any()) { args.AbortPipeline(); return; } foreach(Item item in args.Items) { ExpandTokensOnItem(item); } } protected virtual void ExpandTokensOnItem(Item item) { Assert.ArgumentNotNull(item, "item"); item.Fields.ReadAll(); item.Editing.BeginEdit(); TokenReplacer.ReplaceItem(item); item.Editing.EndEdit(); } protected virtual MasterVariablesReplacer GetTokenReplacer() { return Factory.GetMasterVariablesReplacer(); } } }
The code above uses the instance of Sitecore.Data.MasterVariablesReplacer (subclass or otherwise) — this is defined in your Sitecore configuration at settings/setting[@name=”MasterVariablesReplacer”] — and passes all Items housed in the pipeline argument instance to its ReplaceItem() method — each Item is placed in an editing state before having their tokens expanded.
I then built the following class to serve as a <saveUI> pipeline processor (this pipeline is triggered when someone saves an Item in the Sitecore client):
using Sitecore; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.Pipelines; using Sitecore.Pipelines.Save; using Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems; namespace Sitecore.Sandbox.Pipelines.SaveUI { public class ExpandNewStandardValuesTokens { private string ExpandNewTokensOnAllItemsPipeline { get; set; } public void Process(SaveArgs args) { Assert.IsNotNullOrEmpty(ExpandNewTokensOnAllItemsPipeline, "ExpandNewTokensOnAllItemsPipeline must be set in configuration!"); foreach (SaveArgs.SaveItem saveItem in args.Items) { Item item = GetItem(saveItem); if(IsStandardValues(item)) { ExpandNewTokensOnAllItems(item); } } } protected virtual Item GetItem(SaveArgs.SaveItem saveItem) { Assert.ArgumentNotNull(saveItem, "saveItem"); return Client.ContentDatabase.Items[saveItem.ID, saveItem.Language, saveItem.Version]; } protected virtual bool IsStandardValues(Item item) { Assert.ArgumentNotNull(item, "item"); return StandardValuesManager.IsStandardValuesHolder(item); } protected virtual void ExpandNewTokensOnAllItems(Item standardValues) { CorePipeline.Run(ExpandNewTokensOnAllItemsPipeline, new ExpandNewTokensOnAllItemsArgs { StandardValuesItem = standardValues }); } } }
The code above invokes the custom pipeline when the Item being saved is a Standard Values Item — the Standard Values Item is passed to the pipeline via a new ExpandNewTokensOnAllItemsArgs instance.
I then glued all of the pieces above together in the following patch configuration file:
<?xml version="1.0" encoding="utf-8" ?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <pipelines> <expandNewTokensOnAllItems> <processor type="Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems.EnsureStandardValues, Sitecore.Sandbox" /> <processor type="Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems.EnsureUnexpandedTokens, Sitecore.Sandbox"> <Tokens hint="list"> <Token>$name</Token> <Token>$id</Token> <Token>$parentid</Token> <Token>$parentname</Token> <Token>$date</Token> <Token>$time</Token> <Token>$now</Token> </Tokens> </processor> <processor type="Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems.CollectAllItems, Sitecore.Sandbox" /> <processor type="Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems.FilterStandardValuesItem, Sitecore.Sandbox" /> <processor type="Sitecore.Sandbox.Pipelines.ExpandNewTokensOnAllItems.ExpandTokens, Sitecore.Sandbox" /> </expandNewTokensOnAllItems> </pipelines> <processors> <saveUI> <processor patch:before="saveUI/processor[@type='Sitecore.Pipelines.Save.Save, Sitecore.Kernel']" mode="on" type="Sitecore.Sandbox.Pipelines.SaveUI.ExpandNewStandardValuesTokens"> <ExpandNewTokensOnAllItemsPipeline>expandNewTokensOnAllItems</ExpandNewTokensOnAllItemsPipeline> </processor> </saveUI> </processors> </sitecore> </configuration>
Let’s see this in action!
I added three new fields to a template, and added some tokens in them:
After clicking save, I navigated to one of the content Items that use this Template:
As you can see, the tokens were expanded. 🙂
If you have any thoughts on this, please drop a comment.
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:
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:
If you have any thoughts on this, please share in a comment.
Until next time, have a Sitecoredatalicious day!