Sitecore for Developers
Sitecore's History Engine part 2
Last year I wrote a post quickly describing the use of Sitecore’s HistoryManager. Well a lot’s happened since then and I thought I’d post a quick update.
I’ve re-worked some of the code that’s been used and re-used as an extension of Sitecore’s Database class and thought I would share in case others find it as a useful starting point. The extension methods follow Daniel Cazzulino’s ideas on extension methods and testing. I’ll link to his blog post below (or here), it’s a good read, especially if you’re in a group that tends to go a little extension method crazy.Before we get started I should probably mention that any database using the HistoryEngine will need to have it enabled. This is usually done via a config patch.
[sourcecode language="xml"]<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <databases> <database id="web"> <Engines.HistoryEngine.Storage> <obj type="Sitecore.Data.SqlServer.SqlServerHistoryStorage, Sitecore.Kernel"> <param connectionStringName="$(id)"/> <EntryLifeTime>30.00:00:00</EntryLifeTime> </obj> </Engines.HistoryEngine.Storage> </database> </databases> </sitecore> </configuration>[/sourcecode]
Now into the code…First up we’re going to make an entry point extension method for our code.
[sourcecode language="csharp"]public static class DatabaseExtensions { internal static Func<Database, IDatabaseHistory> DatabaseHistoryFactory = x => new DatabaseHistory(x);
public static IDatabaseHistory DatabaseHistory(this Database db) { return DatabaseHistoryFactory(db); } }[/sourcecode]
This will allow you to change out the DatabaseHistoryFactory while running tests in order to supply a mock IDatabaseHistory that doesn't have a real database dependency. It also has the added feature of allowing us to group our extension method so intellisense doesn't look like a dog’s breakfast by the end of a project.Next we’ll create our IDatabaseHistory interface. This defines 4 methods used to manage history entries.
[sourcecode language="csharp"]public interface IDatabaseHistory { /// <summary> /// Gets the newest history entries since the last read. /// </summary> /// <param name="key">The key used while storing and retrieving the last updated date from the dataabase</param> /// <returns></returns> IEnumerable<HistoryEntry> GetLatestHistoryEntries(string key);
/// <summary> /// Returns all history entries that match the supplied parameters. /// </summary> /// <param name="startTime">The earliest hisory entry we're interested in. Defaults to DateTime.MinValue</param> /// <param name="endTime">The last history entry we're interested in. Defaults to DateTime.Now</param> /// <param name="category">Limits the results to HisotryEntries of this HistoryCategory. Defaults to HistoryCategory.Item</param> /// <returns></returns> IEnumerable<HistoryEntry> ProcessHistory(DateTime? startTime = null, DateTime? endTime = null, HistoryCategory category = HistoryCategory.Item);
/// <summary> /// Records the last read date for the given key. /// </summary> /// <param name="key"></param> /// <param name="startTime"></param> void SetLastReadDate(string key, DateTime? startTime = null);
/// <summary> /// Retrieves the last read date for the given key /// </summary> /// <param name="key"></param> /// <returns></returns> DateTime GetLastRecordedUpdateTime(string key); }[/sourcecode]
And finally we’ll create an implementation of this interface. This is the code where we’ll actually be reading from the database and returning results
[sourcecode language="csharp"]public class DatabaseHistory : IDatabaseHistory { public Database Database { get; set; }
public DatabaseHistory(Database database) { Assert.ArgumentNotNull(database, "Database"); Database = database; }
public IEnumerable<HistoryEntry> GetLatestHistoryEntries(string key) { Assert.ArgumentNotNull(key, "key"); Assert.IsNotNull(Database, "Unable to process history from null database"); DateTime fromDate = GetLastRecordedUpdateTime(key); DateTime toDate = DateTime.UtcNow;
IEnumerable<HistoryEntry> results; try { results = ProcessHistory(fromDate, toDate); } catch (Exception ex) { Log.Error("Error reading database history", ex, this); return Enumerable.Empty<HistoryEntry>(); }
SetLastReadDate(key, toDate);
return results; }
public IEnumerable<HistoryEntry> ProcessHistory(DateTime? startTime = null, DateTime? endTime = null, HistoryCategory category = HistoryCategory.Item) { Assert.IsNotNull(Database, "Unable to process history from null database");
DateTime fromDate = startTime.HasValue ? startTime.Value : DateTime.MinValue; DateTime toDate = endTime.HasValue ? endTime.Value : DateTime.UtcNow;
var historyEntries = HistoryManager.GetHistory(Database, fromDate, toDate) .Where(x => x.Category == category);
return historyEntries; }
public void SetLastReadDate(string key, DateTime? startTime = null) { Assert.ArgumentNotNullOrEmpty(key, "key"); Assert.IsNotNull(Database, "Unable to process history from null database");
var utcNow = startTime.HasValue ? startTime.Value : DateTime.UtcNow;
// writing back the date flag of our last operation Database.Properties[key] = DateUtil.ToIsoDate(utcNow, true); }
public virtual DateTime GetLastRecordedUpdateTime(string lastUpdatePropertyName) { Assert.IsNotNull(Database, "Unable to process history from null database"); DateTime lastUpdateTime;
if (!DateTime.TryParse(Database.Properties[lastUpdatePropertyName], out lastUpdateTime)) { lastUpdateTime = DateUtil.ParseDateTime(Database.Properties[lastUpdatePropertyName], DateTime.MinValue); }
return lastUpdateTime; } }[/sourcecode]
From here you’re all set to go. An example use would be:
[sourcecode language="csharp"]Sitecore.Context.Database.DatabaseHistory().GetLatestHistoryEntries("myKey")[/sourcecode]
One thing I’d like to mention before I go is to pick your key carefully. A common use case I've found for the history engine is to find all of the items that have been published since the last time a task was run. If the same key is used in a load balanced environment the first server to run the code will also update the last read data for that key, leaving the other servers with nothing. In this case I've found it helpful to append either a specific configuration setting, unique to each web server or something like Environment.MachineName.
Other reading:http://blogs.clariusconsulting.net/kzu/making-extension-methods-amenable-to-mocking/http://hiblog.wpengine.com/2014/03/21/a-brief-look-at-the-sitecore-historymanager/