Using MongoDb to provide items as a Cache for Sitecore.

Ok, so I finally managed to get my MongoDB hat back on recently and first thing that struck me is that I forgot how much I love it as a product, the driver is easy to get to grips with, its a doddle to set up and hyper fast :D.

Below I have listed a starting point that I have been working on to serve the latest version of content from Mongo, in part this is to help out in the content editor (and in particular, that item.GetChildren() causes the default item provider to return the latest version from the database, but I expect to extend this to be able to serve all of the web content from such a provider.

This post at present is the basis of a working idea that I have been playing with, but I think with refinement (and certainly until Sitecore provide us a full blown MongoDB based version) it might have mileage to enable us to squeeze a little more performance out of our existing implementations.

Data provider agnostic implementation

Below is the crux of what we want to do, this is to control the behaviour. Eventually it would be nice to include reference to a Graph database also such as Neo / OrientDb, however, at present its still just an idea.

    // This is the basis on which the item provider will work, its not tied to mongodb at this point
    public class HybridItemProvider : ItemProvider
    {
        protected HybridItemProvider(ILatestVersionStorageManager latestVersionStorageManager)
        {
            LatestVersionStorageManager = latestVersionStorageManager;
        }

        protected ILatestVersionStorageManager LatestVersionStorageManager { get; private set; }

        protected override ItemList GetChildren(Item item, ChildListOptions options)
        {
            ItemList list = LatestVersionStorageManager.GetChildItems(item);
            if (list != null)
            {
                return list;
            }

            ItemList children = base.GetChildren(item, options);
            LatestVersionStorageManager.StoreChildren(item, children);
            return children;
        }

        protected override Item GetItem(ID itemId, Language language, Version version, Database database)
        {
            Item item;

            if (version == Version.Latest)
            {
                // attempt to retrieve from the nosql provider
                item = LatestVersionStorageManager.GetItem(itemId, language, database);
                if (item != null)
                {
                    return item;
                }
            }

            item = base.GetItem(itemId, language, version, database);
            if (version == Version.Latest)
            {
                LatestVersionStorageManager.SaveItem(item, database);
            }

            return item;
        }

        public override bool SaveItem(Item item)
        {
            bool result = base.SaveItem(item);
            LatestVersionStorageManager.RemoveItem(item);
            return result;
        }

        public override bool MoveItem(Item item, Item destination, SecurityCheck securityCheck)
        {
            LatestVersionStorageManager.RemoveItem(item);
            LatestVersionStorageManager.RemoveItem(item.Parent);
            return base.MoveItem(item, destination, securityCheck);
        }

        protected override Item GetParent(Item item)
        {
            Item parentItem = LatestVersionStorageManager.GetItem(item.ParentID, item.Language, item.Database);
            if (parentItem != null)
            {
                return parentItem;
            }

            return base.GetParent(item);
        }
    }
    // This is to allow us to return the latest version in code - eventually will contain appropriate logging etc.
    public class LatestVersionStorageManager : ILatestVersionStorageManager
    {
        private readonly ILatestVersionStorageRepository latestVersionStorageRepository;

        public LatestVersionStorageManager(ILatestVersionStorageRepository latestVersionStorageRepository)
        {
            this.latestVersionStorageRepository = latestVersionStorageRepository;
        }

        public Item GetItem(ID itemId, Language language, Database database)
        {
            return latestVersionStorageRepository.GetItem(itemId, language, database);
        }

        public ItemList GetChildItems(Item item)
        {
            return latestVersionStorageRepository.GetChildren(item);
        }

        public bool SaveItem(Item item, Database database)
        {
            return latestVersionStorageRepository.SaveItem(item, database);
        }

        public bool RemoveItem(Item item)
        {
            return latestVersionStorageRepository.RemoveItem(item);
        }

        public bool StoreChildren(Item item, ItemList items)
        {
            return latestVersionStorageRepository.StoreChildren(item, items);
        }
    }

Mongo Specific
Now we have the basis to provide our items – lets consider Mongo 😀

    // ok - so now we tie this together using mongo db
    public class MongoItemProvider : HybridItemProvider
    {
        public MongoItemProvider() : base(new LatestVersionStorageManager(new MongoLatestVersionStorageRepository()))
        {
        }
    }

The MongoConnectivity class currently is a work in progress as I am finalising its usage, but the principles still the same.

    // And finally !!
    public class MongoLatestVersionStorageRepository : ILatestVersionStorageRepository
    {
        private readonly string connectionString =
            ConfigurationManager.ConnectionStrings["master_mongo"].ConnectionString;

        private readonly MongoServer mongoServer;

        public MongoLatestVersionStorageRepository()
        {
            var client = new MongoClient();
            mongoServer = client.GetServer();
        }

        public Item GetItem(ID itemId, Language language, Database database)
        {
            using (var connection = new MongoConnectivity(mongoServer, connectionString))
            {
                MongoCollection collection =
                    connection.Database.GetCollection(GetCollectionName(database));
                StoredSitecoreItem storedItem = collection.FindOneById(GetBsonId(itemId, language));
                if (storedItem == null)
                {
                    return null;
                }

                return GetItemFromStored(storedItem);
            }
        }

        public ItemList GetChildren(Item item)
        {
            using (var connection = new MongoConnectivity(mongoServer, connectionString))
            {
                MongoCollection collection =
                    connection.Database.GetCollection(GetCollectionName(item.Database));
                StoredSitecoreItem storedItem = collection.FindOneById(GetBsonId(item));
                if (storedItem == null)
                {
                    return null;
                }

                if (storedItem.ChildItems == null)
                {
                    return null;
                }

                var childItems = new ItemList();
                foreach (string childId in storedItem.ChildItems)
                {
                    StoredSitecoreItem storedChildItem = collection.FindOneById(childId);
                    if (storedChildItem == null)
                    {
                        continue;
                    }

                    childItems.Add(GetItemFromStored(storedChildItem));
                }

                collection.Save(storedItem);


                return childItems;
            }
        }

        public bool StoreChildren(Item item, ItemList items)
        {
            using (var connection = new MongoConnectivity(mongoServer, connectionString))
            {
                MongoCollection collection =
                    connection.Database.GetCollection(GetCollectionName(item.Database));
                StoredSitecoreItem storedItem = collection.FindOneById(GetBsonId(item));
                if (storedItem == null)
                {
                    return true;
                }

                storedItem.ChildItems = new List();

                foreach (Item childItem in items)
                {
                    storedItem.ChildItems.Add(GetBsonId(childItem));
                }

                collection.Save(storedItem);

                return true;
            }
        }

        public bool RemoveItem(Item item)
        {
            if (item == null)
            {
                return true;
            }

            using (var connection = new MongoConnectivity(mongoServer, connectionString))
            {
                MongoCollection collection =
                    connection.Database.GetCollection(GetCollectionName(item.Database));
                collection.Remove(Query.EQ(x => x.Id, GetBsonId(item.ID, item.Language)));
                return true;
            }
        }

        public bool SaveItem(Item item, Database database)
        {
            if (item == null)
            {
                return true;
            }

            using (var connection = new MongoConnectivity(mongoServer, connectionString))
            {
                MongoCollection collection =
                    connection.Database.GetCollection(GetCollectionName(database));
                StoredSitecoreItem storedItem = GetStoredItem(item);
                collection.Insert(storedItem);
                return true;
            }
        }

        protected virtual string GetCollectionName(Database database)
        {
            return string.Format("items_{0}", database.Name.ToLowerInvariant());
        }

        protected virtual void AddChildren(StoredSitecoreItem storedItem, Item item)
        {
        }

        protected virtual StoredSitecoreItem GetStoredItem(Item item)
        {
            if (item == null)
            {
                return null;
            }

            var storedItem = new StoredSitecoreItem
            {
                Id = GetBsonId(item),
                SitecoreId = item.ID.ToGuid(),
                Name = item.Name,
                TemplateId = item.TemplateID.ToGuid(),
                Version = item.Version.Number,
                Language = item.Language != null ? item.Language.Name.ToLowerInvariant() : "un",
                Database = item.Database.Name.ToLowerInvariant()
            };

            foreach (Field field in item.Fields)
            {
                var storedField = new StoredSitecoreField
                {
                    FieldId = field.ID.ToGuid(),
                    Value = field.Value
                };
                storedItem.Fields.Add(storedField);
            }
            return storedItem;
        }

        protected virtual Item GetItemFromStored(StoredSitecoreItem storedItem)
        {
            if (storedItem == null)
            {
                return null;
            }

            var itemId = new ID(storedItem.SitecoreId);
            var templateId = new ID(storedItem.TemplateId);
            var branchId = new ID(storedItem.TemplateId);

            var definition = new ItemDefinition(itemId, storedItem.Name, templateId, branchId);
            var version = new Version(storedItem.Version);
            var fieldList = new FieldList();
            foreach (StoredSitecoreField field in storedItem.Fields)
            {
                fieldList.Add(new ID(field.FieldId), field.Value);
            }

            Language language = null;
            if (storedItem.Language != null)
            {
                Language.TryParse(storedItem.Language, out language);
            }

            Database database = Factory.GetDatabase(storedItem.Database);

            var itemData = new ItemData(definition, language, version, fieldList);
            var item = new Item(itemId, itemData, database);
            return item;
        }

        protected string GetBsonId(Item item)
        {
            return GetBsonId(item.ID, item.Language);
        }

        protected string GetBsonId(ID id, Language language)
        {
            string languageString = language != null ? language.Name.ToLowerInvariant() : string.Empty;
            if (string.IsNullOrEmpty(languageString))
            {
                languageString = "unknown";
            }
            return String.Format("{0}_{1}", id.ToGuid(), languageString);
        }
    }
Advertisements

One thought on “Using MongoDb to provide items as a Cache for Sitecore.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s