A simple read-only Sitecore data provider

An external data source can be integrated into Sitecore and represented as items by implementing a custom data provider. The custom data provider interacts with the external data source to define items, item hierarchies, handle actions and queries against those items. A custom data provider is built by inheriting from Sitecore's  abstract DataProvider class and how much work the custom data provider should do depends on the problem you are addressing. You may only need to implement a data provider which overrides a single virtual method or possibly all of them.

One of the most basic forms of data provider is a read-only data provider and to demonstrate how easy data can be integrated into Sitecore we'll take a look at a very simple example, a data provider which takes some data from an in memory collection and transforms the data into items under a specific item in Sitecore's content tree.

This will involve the following steps:

  • Create a Sitecore template which will be used for items generated by the data provider  
  • Create a root item in Sitecore to serve as a container for the items created by the data provider 
  • Create an in memory repository containing some data to integrate into Sitecore
  • Create a custom data provider which will:
    • Retrieve the data to be integrated into Sitecore 
    • Transform data into items in the Sitecore content tree

 

Sitecore templates

In this example we create two templates in Sitecore. One which will be used to create the root item for the data being integrated with the data provider and one for the actual items containing the data.  

Templates for integrated data

Data provider root item

Using the root item template an item is created in the Sitecore content tree which will be the parent item for our external data.

Data provider root item

The external data

In this example the external data is coming from a simple in memory repository which simple provides a collections of objects. 

01.namespace SitecoreCms.CodeSamples.DataProviders.SimpleReadOnlyData
02.{
03.    public class Rocket
04.    {
05.        public string ParentId { get; set; }
06.        public string RocketId { get; set; }       
07.        public string Name { get; set; }
08.        public string Title { get; set; }
09.        public string Description { get; set; }
10.    }
11.}

My in memory repository is simply returns a collection of rockets:

using System.Collections.Generic;
  
namespace SitecoreCms.CodeSamples.DataProviders.SimpleReadOnlyData
{
    public class RocketRepository
    {
        public IEnumerable<Rocket> GetSimpleRocketCollection()
        {
            var rockets = new List<Rocket>()
            {
                new Rocket()
                    {
                        ParentId = null,
                        RocketId = "1001",
                        Name = "Falcon 9",
                        Title = "The Falcon 9",
                        Description = "Falcon 9 is ...."
                    },
                new Rocket()
                    {
                        ParentId = null,
                        RocketId = "1002",
                        Name = "Titan",
                        Title = "The Titan Rocket",
                        Description = "Titan was ...."
                    }
            };
            return rockets;
        }
    }
}

The read-only data provider

Create a new class

Create a class which inherits from Sitecore.Data.DataProviders.DataProvider

using Sitecore.Data.DataProviders;
 
namespace SitecoreCms.CodeSamples.DataProviders.SimpleReadOnlyData
{
    public class RocketReadOnlyDataProvider : DataProvider
    {
    }
}

Add a constructor

I would like to be able to configure the data provider using a config file which passes the database name, IDTablePrefix, rockets root item template an rocket template IDs so I create a constructor with some parameters and create an instance of the rocket repository:

using System;
using System.Linq;
using Sitecore.Data;
using Sitecore.Data.DataProviders;
using Sitecore.Diagnostics;
using Sitecore.Data.IDTables;
using Sitecore.Data.Items;
using System.Collections.Generic;
 
namespace SitecoreCms.CodeSamples.DataProviders.SimpleReadOnlyData
{
    public class RocketReadOnlyDataProvider : DataProvider
    {
        private readonly string _targetDatabaseName;
        private readonly string _idTablePrefix;
        private readonly ID _rocketTemplateID;
        private readonly ID _rocketRootTemplateID;
 
        private readonly IEnumerable<Rocket> _rockets;
 
        public RocketReadOnlyDataProvider(string targetDatabaseName, string rocketRootTemplateID, string rocketTemplateID, string idTablePrefix)
        {
            Assert.ArgumentNotNullOrEmpty(targetDatabaseName, "targetDatabaseName");
            Assert.ArgumentNotNullOrEmpty(rocketRootTemplateID, "rootTemplateId");
            Assert.ArgumentNotNullOrEmpty(rocketTemplateID, "simpleReadOnlyDataTemplateId");
            Assert.ArgumentNotNullOrEmpty(idTablePrefix, "idTablePrefix");
 
            _targetDatabaseName = targetDatabaseName;
            _idTablePrefix = idTablePrefix;
 
            if (!ID.TryParse(rocketRootTemplateID, out _rocketRootTemplateID))
                throw new InvalidOperationException(string.Format("Invalid rocket root template ID {0}", rocketRootTemplateID));
 
            if (!ID.TryParse(rocketTemplateID, out _rocketTemplateID))
                throw new InvalidOperationException(string.Format("Invalid rocket template ID {0}", rocketTemplateID));
 
            _rockets = new RocketRepository().GetSimpleRocketCollection();
        }       
    }
}

Override GetItemDefinition 

This method is responsible for building an item definition based on the data to be integrated into Sitecore.

public override ItemDefinition GetItemDefinition(ID itemID, CallContext context)
{
    Assert.ArgumentNotNull(itemID, "itemID");
 
    // Retrieve the rocket id from Sitecore's IDTable
    var rocketId = GetRocketIdFromIDTable(itemID);
 
    if (!string.IsNullOrEmpty(rocketId))
    {
        // Retrieve the rocket data from the rockets collection
        var rocket = _rockets.FirstOrDefault(o => o.RocketId == rocketId);
 
        if (rocket != null)
        {
            // Ensure the rocket item name is valid for the Sitecore content tree
            var itemName = ItemUtil.ProposeValidItemName(rocket.Name);
 
            // Return a Sitecore item definition for the rocket using the rocket template
            return new ItemDefinition(itemID, itemName, ID.Parse(_rocketTemplateID), ID.Null);
        }
    }
 
    return null;
}
 
private string GetRocketIdFromIDTable(ID itemID)
{
    var idTableEntries = IDTable.GetKeys(_idTablePrefix, itemID);
 
    if (idTableEntries.Any())
        return idTableEntries[0].Key.ToString();
 
    return null;
} 

Override GetItemVersions

We need to provide some information which tells Sitecore which versions content is available in and in what languages. This is done by overriding the GetItemVersions method, here using just a little hack for sake of example.   

public override VersionUriList GetItemVersions(ItemDefinition item, CallContext context)
{
    if (CanProcessItem(item.ID))
    {
        VersionUriList versions = new VersionUriList();     
 
        // Just a little hack
        versions.Add(Language.Current, Sitecore.Data.Version.First);
 
        context.Abort();
 
        return versions;
    }
 
    return base.GetItemVersions(item, context);
}
 

 

Override GetChildIDs

This method creates a list of item IDs representing the children of an item.

public override IDList GetChildIDs(ItemDefinition parentItem, CallContext context)
{
    if (CanProcessParent(parentItem.ID))
    {
        var itemIdList = new IDList();
 
        foreach (var rocket in _rockets)
        {
            var rocketId = rocket.RocketId;
 
            // Retrieve the Sitecore item ID mapped to his rocket
            IDTableEntry mappedID = IDTable.GetID(_idTablePrefix, rocketId);
 
            if (mappedID == null)
            {
                // Map this rocket to a Sitecore item ID
                mappedID = IDTable.GetNewID(_idTablePrefix, rocketId, parentItem.ID);
            }
 
            itemIdList.Add(mappedID.ID);
        }
 
        context.DataManager.Database.Caches.DataCache.Clear();
 
        return itemIdList;
    }
 
    return base.GetChildIDs(parentItem, context);
}
 
private bool CanProcessParent(ID id)
{
    var item = Factory.GetDatabase(_targetDatabaseName).Items[id];
 
    bool canProcess = false;
 
    if (item.Paths.IsContentItem && item.TemplateID == _rocketRootTemplateID)
    {
        canProcess = true;
    }
 
    return canProcess;
}

Override GetParentID

This method returns the parent id stored in the IDTable.

public override ID GetParentID(ItemDefinition itemDefinition, CallContext context)
{
    var idTableEntries = IDTable.GetKeys(_idTablePrefix, itemDefinition.ID);
 
    if (idTableEntries.Any())
    {
        return idTableEntries.First().ParentID;
    }
 
    return base.GetParentID(itemDefinition, context);
}

Override GetItemFields 

In order to populate the item fields with rocket data, we need to build a collection of item fields and map the rocket property values to these fields:  

public override FieldList GetItemFields(ItemDefinition itemDefinition, VersionUri version, CallContext context)
{
    var fields = new FieldList();
 
    var idTableEntries = IDTable.GetKeys(_idTablePrefix, itemDefinition.ID);
 
    if (idTableEntries.Any())
    {
        if (context.DataManager.DataSource.ItemExists(itemDefinition.ID))
        {
            ReflectionUtil.CallMethod(typeof(ItemCache), CacheManager.GetItemCache(context.DataManager.Database), "RemoveItem", true, true, new object[] { itemDefinition.ID });
        }
 
        var template = TemplateManager.GetTemplate(_rocketTemplateID, Factory.GetDatabase(_targetDatabaseName));
 
        if (template != null)
        {
            var rocketId = GetRocketIdFromIDTable(itemDefinition.ID);
 
            if (!string.IsNullOrEmpty(rocketId))
            {               
                var rocket = _rockets.FirstOrDefault(o => o.RocketId == rocketId);
 
                if (rocket != null)
                {
                    foreach (var field in GetDataFields(template))
                    {
                        fields.Add(field.ID, GetFieldValue(field, rocket));
                    }
                }
            }
        }
    }
 
    return fields;
}
 
protected virtual IEnumerable<TemplateField> GetDataFields(Template template)
{
    return template.GetFields().Where(ItemUtil.IsDataField);
}
 
private string GetFieldValue(TemplateField field, Rocket rocket)
{
    string fieldValue = string.Empty;
 
    switch (field.Name)
    {
        case "Title":
            fieldValue = rocket.Title;
            break;
        case "Description":
            fieldValue = rocket.Description;
            break;
        default:
            break;
    }
 
    return fieldValue;
}

Override GetLanguages(CallContext context)

Our simple data provider is only being used to grab some external data and integrate it into the Sitecore content tree but a data provider also has the responsibility of retrieving the languages content could be made available in. If we do not do anything to disable this in our data provider then our data provider will get the languages defined in Sitecore along with any other data providers configured. This results in duplicate languages appearing in the client.

Data provider produces duplicate languages

To get around this we simply need to override the GetLanguages method in our data provider.

public override LanguageCollection GetLanguages(CallContext context)
{
    return null;
}

And now the duplicate languages are gone.

Data provider produces duplicate languages

Sitecore configuration

All that is left now is to register the new provider with Sitecore by creating a config file in the /App_Config/Include folder:

<configuration xmlns:patch="https://www.sitecore.net/xmlconfig/">
  <sitecore>
    <dataProviders>
      <rocketReadOnlyDataProvider type="SitecoreCms.CodeSamples.DataProviders.SimpleReadOnlyData.RocketReadOnlyDataProvider, SitecoreCms.CodeSamples.DataProviders">
        <param desc="targetDatabaseName">master</param>
        <param desc="rocketRootTemplateID">{FBE22B26-1774-4F61-8DD4-434F691D0C8B}</param>
        <param desc="rocketTemplateID">{8DED8CEB-7140-4100-892D-52DD840FAF34}</param>
        <param desc="idTablePrefix">Rockets</param>
      </rocketReadOnlyDataProvider>
    </dataProviders>
    <databases>
      <database id="master" singleInstance="true" type="Sitecore.Data.Database, Sitecore.Kernel">
        <dataProviders hint="list:AddDataProvider">
          <dataProvider patch:before="*[1]" ref="dataProviders/rocketReadOnlyDataProvider"/>
        </dataProviders>
      </database>
    </databases>
  </sitecore>
</configuration>

Open up the Sitecore content editor to verify the data has been integrated into the content tree.

Rocket data integrated in Sitecore

And that's it job done! Well not quite, this is a very simple example of how to integrate external data in Sitecore with a data provider and there is a lot more to address such as generating an hierarchical structure of items, editing external data from the Sitecore client, performance, caching and publishing.

Source code

You can find the source code for this simple data provider in the code sample repository and navigate to the RocketReadOnlyDataProvider class:

Sitecore7CodeSamples / SitecoreCms.CodeSamples.DataProviders / SimpleReadOnlyData / RocketReadOnlyDataProvider.cs

I am always fiddling around with the code so it may vary a little from the above but I am sure you'll get the idea.

 

Latest articles