Sitecore OData Item Api extensions for reference type fields
Sitecore OData Item Api provides readonly access to Sitecore items. The OData Item Api is particularly useful when you need to pull some data from Sitecore to show on a non-Sitecore websites. In my case, I needed to pull a list of current job opening in my organization to show them on our company's non-Sitecore website's careers page.
For more information on OData Item Api and how to query the data, check the Sitecore documentation page. One thing that particularly bothered me when using OData Item Api service to get an item and its field values using a query like below is, the lack of support for getting the link field target URL in one query.
/sitecore/api/ssc/aggregate/content/Items('{F7SDEBB9-6AB1-4ACC-A790-FE627DD18722}')?$expand=FieldValues&sc_apikey={ADGT918E-8F0F-4F7E-8C77-DEE3F4F2E1Y1}
When you run the above query against an item with General link field, the output for the field values looks something like this
{ | |
"@odata.context": "https://scm.dev.local/sitecore/api/ssc/aggregate/content/$metadata#Items/$entity", | |
"Name": " Fact Box", | |
"DisplayName": "", | |
"Path": "/sitecore/content/scm/scm-web-app/home/SamplePage/Data/CTAs/Image Fact Box", | |
"Language": "en", | |
"Version": 1, | |
"IsLatestVersion": true, | |
"TemplateId": "120b86d4-6bc1-4198-9610-6f4452b61efc", | |
"TemplateName": "ImageCTA", | |
"Created": "2021-04-08T07:30:53Z", | |
"Updated": "2021-04-14T04:16:41Z", | |
"ParentId": "5aeec3ac-4103-4eeb-a3a7-2dad6b39d250", | |
"Id": "fcd5ebb9-6ab1-4acc-a790-fe627dd18722", | |
"Url": "https://scm.dev.local/sitecore/api/ssc/aggregate/content/Items('fcd5ebb9-6ab1-4acc-a790-fe627dd18722')", | |
"FieldValues": { | |
"$count": 5, | |
"description": "Short description of the module", | |
"title": "Fact Box", | |
"image": "<image mediaid=\"{D0E4A177-9CE1-447D-9E92-245653E4D18E}\" ></image>", | |
"image__Url": "https://scm.dev.local/-/media/project/scmadeeasy/scm-website/images/flexible-content-cards/scm-health-navigator.jpg", | |
"image__Alt": "SCM Logo", | |
"link": "<link linktype=\"internal\" text=\"Button Text\" querystring=\"\" target=\"\" id=\"{4CD42D73-EE42-48F1-977F-2046CE29AA98}\" />" | |
} | |
} |
As you can see, the link field value is just the raw value stored in the field. To get the target item URL the link is pointing to, you need to extract the target item id and make another OData query to the Api. It is not practical to make one call per item to get the target URL when you are fetching multiple items in one query.
Luckily, we can easily extend the OData Item Api to include our own fields in the output. This can be done by overwriting the ItemDataModelFactory used by the Item Api to resolve the item contents with our own ItemDataModelFactory implementation. To do this, we need to create a custom ItemDataModelFactory that inherits from Sitecore.Content.Services.Items.OData.ItemDataModelFactory class and overriding the GetFieldValues method.
Here is how the custom ItemDataModelFactory implementation that extends General Link field output to include the Url and few other fields looks like
using Sitecore.Abstractions; | |
using Sitecore.Content.Services.Models; | |
using Sitecore.Diagnostics; | |
using Sitecore.Data.Items; | |
using System; | |
using System.Runtime.CompilerServices; | |
using Sitecore.Data.Fields; | |
using Sitecore.Configuration; | |
using Sitecore.Data.Templates; | |
using System.Linq; | |
using System.Collections.Generic; | |
using Sitecore.Links; | |
using Sitecore.Resources.Media; | |
using Sitecore.Data; | |
using Sitecore; | |
using Sitecore.Links.UrlBuilders; | |
using Sitecore.ContentSearch; | |
using Sitecore.ContentSearch.SearchTypes; | |
namespace SCM.Foundation.OData.Services | |
{ | |
public class ItemDataModelFactory : Sitecore.Content.Services.Items.OData.ItemDataModelFactory | |
{ | |
private const string TransformationIndicator = "__"; | |
private readonly BaseTemplateManager _templateManager; | |
private readonly BaseFieldTypeManager _fieldTypeManager; | |
private readonly BaseMediaManager _mediaManager; | |
public ItemDataModelFactory( | |
BaseTemplateManager templateManager, | |
BaseFieldTypeManager fieldTypeManager, | |
BaseMediaManager mediaManager) : base(templateManager, fieldTypeManager, mediaManager) | |
{ | |
Assert.ArgumentNotNull((object)templateManager, nameof(templateManager)); | |
Assert.ArgumentNotNull((object)fieldTypeManager, nameof(fieldTypeManager)); | |
Assert.ArgumentNotNull((object)mediaManager, nameof(mediaManager)); | |
this._templateManager = templateManager; | |
this._fieldTypeManager = fieldTypeManager; | |
this._mediaManager = mediaManager; | |
} | |
public override ICollection<FieldDataModel> GetFields(Item item, bool standardFields) | |
{ | |
return (ICollection<FieldDataModel>)this.GetItemFields(item, standardFields).Select<Field, FieldDataModel>(new Func<Field, FieldDataModel>(this.CreateFieldModel)).ToList<FieldDataModel>(); | |
} | |
private ICollection<Field> GetItemFields(Item item, bool standardFields) | |
{ | |
if (item == null) | |
return (ICollection<Field>)new List<Field>(); | |
Template standardTemplate = this._templateManager.GetTemplate(Settings.DefaultBaseTemplate, item.Database); | |
Assert.IsNotNull((object)standardTemplate, FormattableString.Invariant(FormattableStringFactory.Create("{0} template is not found null.", (object)"DefaultBaseTemplate"))); | |
return (ICollection<Field>)(standardFields ? item.Fields.Where<Field>((Func<Field, bool>)(f => standardTemplate.ContainsField(f.ID))) : item.Fields.Where<Field>((Func<Field, bool>)(f => !standardTemplate.ContainsField(f.ID)))).ToList<Field>(); | |
} | |
public override NameValueCollectionModel GetFieldValues( | |
Item item, | |
bool standardFields) | |
{ | |
return this.CreateFieldValuesModel(this.GetItemFields(item, standardFields)); | |
} | |
private FieldDataModel CreateFieldModel(Field sitecoreField) | |
{ | |
FieldDataModel defaultFieldModel = CustomItemDataModelFactory.CreateDefaultFieldModel(sitecoreField); | |
if (this._fieldTypeManager.GetField(sitecoreField) is LinkField linkField) | |
{ | |
defaultFieldModel.Properties.Add("Url", GetLinkTargetUrl(linkField)); | |
defaultFieldModel.Properties.Add("Title", (object)linkField.Title ?? (object)string.Empty); | |
defaultFieldModel.Properties.Add("Text", (object)linkField.Text ?? (object)string.Empty); | |
defaultFieldModel.Properties.Add("Class", (object)linkField.Class ?? (object)string.Empty); | |
defaultFieldModel.Properties.Add("TargetItemId", (object)linkField.TargetID.Guid.ToString()); | |
defaultFieldModel.Properties.Add("Target", (object)linkField.Target ?? (object)string.Empty); | |
} | |
return defaultFieldModel; | |
} | |
public string GetLinkTargetUrl(LinkField linkField) | |
{ | |
string url = String.Empty; | |
if (linkField != null) | |
{ | |
switch (linkField.LinkType) | |
{ | |
case "internal": | |
if(linkField.TargetItem != null) | |
url = LinkManager.GetItemUrl(linkField.TargetItem, new ItemUrlBuilderOptions { SiteResolving = true, AlwaysIncludeServerUrl = true }); | |
break; | |
case "external": | |
case "mailto": | |
case "javascript": | |
case "tel": | |
url = linkField.Url; | |
break; | |
case "anchor": | |
url = $"#{linkField.Url}"; | |
break; | |
case "media": | |
MediaItem media = new MediaItem(linkField.TargetItem); | |
url = MediaManager.GetMediaUrl(mediaItem, new MediaUrlBuilderOptions() { AlwaysIncludeServerUrl = true }); | |
break; | |
case "": | |
break; | |
default: | |
string message = $"{this.GetType()} : Unknown link type {linkField.LinkType}"; | |
Log.Error(message, this); | |
break; | |
} | |
} | |
return url; | |
} | |
private static FieldDataModel CreateDefaultFieldModel(Field sitecoreField) | |
{ | |
Assert.ArgumentNotNull((object)sitecoreField, nameof(sitecoreField)); | |
return new FieldDataModel() | |
{ | |
Id = sitecoreField.ID.Guid, | |
Name = sitecoreField.Name, | |
DisplayName = sitecoreField.DisplayName, | |
Description = sitecoreField.Description, | |
ContainsStandardValue = sitecoreField.ContainsStandardValue, | |
Title = sitecoreField.Title, | |
Tooltip = sitecoreField.ToolTip, | |
Type = sitecoreField.Type, | |
Validation = sitecoreField.Validation, | |
ValidationText = sitecoreField.ValidationText, | |
Value = sitecoreField.Value | |
}; | |
} | |
private NameValueCollectionModel CreateFieldValuesModel( | |
ICollection<Field> fields) | |
{ | |
NameValueCollectionModel valueCollectionModel = new NameValueCollectionModel() | |
{ | |
Key = fields.Count | |
}; | |
foreach (Field field in (IEnumerable<Field>)fields) | |
{ | |
string str = field.Name.Replace(' ', '_'); | |
valueCollectionModel.Elements.Add(str, (object)field.Value); | |
this.AddLinkFieldTransformation(valueCollectionModel.Elements, field, str); | |
} | |
return valueCollectionModel; | |
} | |
private void AddLinkFieldTransformation( | |
Dictionary<string, object> dictionary, | |
Field field, | |
string fieldName) | |
{ | |
var obj = this._fieldTypeManager.GetField(field) is LinkField field1 ? field1.Url : null; | |
if (obj == null) | |
return; | |
CustomItemDataModelFactory.AddTransformation(dictionary, fieldName, "Url", this.GetLinkTargetUrl(field)); | |
CustomItemDataModelFactory.AddTransformation(dictionary, fieldName, "Title", ((LinkField)field).Title ?? string.Empty); | |
CustomItemDataModelFactory.AddTransformation(dictionary, fieldName, "Text", ((LinkField)field).Text ?? string.Empty); | |
CustomItemDataModelFactory.AddTransformation(dictionary, fieldName, "Target", ((LinkField)field).Target ?? string.Empty); | |
} | |
private static void AddTransformation( | |
Dictionary<string, object> dictionary, | |
string fieldName, | |
string transformationKey, | |
string transformationValue) | |
{ | |
string key = FormattableString.Invariant(FormattableStringFactory.Create("{0}{1}{2}", (object)fieldName, (object)"__", (object)transformationKey)); | |
if (dictionary.ContainsKey(key)) | |
return; | |
dictionary.Add(key, (object)transformationValue); | |
} | |
} | |
} |
We are overriding both GetFields and GetFieldValues methods. While GetFields is used by /sitecore/api/ssc/aggregate/content/Items('{ItemID}')?$expand=Fields query, GetFieldValues is used by /sitecore/api/ssc/aggregate/content/Items('{ItemID}')?$expand=FieldValues query.
To register this custom ItemDataModelFactory implementation we need to write a custom configurator class for OData Item Api service.
using System.Reflection; | |
using Microsoft.Extensions.DependencyInjection; | |
using Sitecore.Content.Services; | |
using Sitecore.Content.Services.Items.OData; | |
using Sitecore.Content.Services.Items.OData.Search; | |
using Sitecore.ContentSearch.Utilities; | |
using Sitecore.DependencyInjection; | |
using Sitecore.Services.Infrastructure.DependencyInjection; | |
using Sitecore.Services.Infrastructure.Sitecore.Data; | |
using Sitecore.Services.Infrastructure.Sitecore.Services; | |
namespace SCM.Foundation.OData | |
{ | |
public class CustomConfigurator : IServicesConfigurator | |
{ | |
public void Configure(IServiceCollection serviceCollection) | |
{ | |
Assembly[] assemblyArray = new Assembly[1] | |
{ | |
Assembly.Load("Sitecore.Content.Services") | |
}; | |
serviceCollection.AddWebApiControllers(assemblyArray); | |
serviceCollection.AddSingleton<IItemRepository, ItemRepository>() | |
.AddSingleton<ApiKeyHelper>() | |
.AddSingleton<ContentSearchManagerWrapper>() | |
.AddScoped<QueryBuilder>() | |
.AddScoped<Sitecore.Content.Services.Items.OData.Search.FilterBinder>() | |
.AddScoped<ODataItemSearch>() | |
.AddSingleton<ComparisonExpressionBuilder>() | |
.AddScoped<FieldNameResolver>().AddScoped<OrderByBinder>() | |
.AddSingleton<Sitecore.Content.Services.Items.OData.Search.SearchHelper>() | |
.AddSingleton<ItemServiceDescriptor>(); | |
} | |
} | |
} |
Now that our custom configurator is ready, we just need to override the default implementation using a config patch.
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> | |
<sitecore> | |
<services> | |
<configurator type="SCM.Foundation.OData.CustomConfigurator, SCM.Foundation.OData" patch:instead="*[@type='Sitecore.Content.Services.Configurator, Sitecore.Content.Services']" /> | |
<register serviceType="Sitecore.Content.Services.Items.OData.ItemDataModelFactory, Sitecore.Content.Services" implementationType="SCM.Foundation.OData.Services.ItemDataModelFactory, SCM.Foundation.OData" lifetime="Singleton" /> | |
</services> | |
</sitecore> | |
</configuration> |
With the custom implementation in place we can now see the additional fields we added for General Link fields in the output.
{ | |
"@odata.context": "https://scm.dev.local/sitecore/api/ssc/aggregate/content/$metadata#Items/$entity", | |
"Name": " Fact Box", | |
"DisplayName": "", | |
"Path": "/sitecore/content/scm/scm-web-app/home/SamplePage/Data/CTAs/Image Fact Box", | |
"Language": "en", | |
"Version": 1, | |
"IsLatestVersion": true, | |
"TemplateId": "120b86d4-6bc1-4198-9610-6f4452b61efc", | |
"TemplateName": "ImageCTA", | |
"Created": "2021-04-08T07:30:53Z", | |
"Updated": "2021-04-14T04:16:41Z", | |
"ParentId": "5aeec3ac-4103-4eeb-a3a7-2dad6b39d250", | |
"Id": "fcd5ebb9-6ab1-4acc-a790-fe627dd18722", | |
"Url": "https://scm.dev.local/sitecore/api/ssc/aggregate/content/Items('fcd5ebb9-6ab1-4acc-a790-fe627dd18722')", | |
"FieldValues": { | |
"$count": 5, | |
"description": "Short description of the module", | |
"title": "Fact Box", | |
"image": "<image mediaid=\"{D0E4A177-9CE1-447D-9E92-245653E4D18E}\" ></image>", | |
"image__Url": "https://scm.dev.local/-/media/project/scmadeeasy/scm-website/images/flexible-content-cards/scm-health-navigator.jpg", | |
"image__Alt": "SCM Logo", | |
"link": "<link linktype=\"internal\" text=\"Button Text\" querystring=\"\" target=\"\" id=\"{4CD42D73-EE42-48F1-977F-2046CE29AA98}\" />", | |
"link__Url": "https://scmadeeasy.com/blogs/blog1", | |
"link__Title": "Blog 1" | |
"link__Target": "" | |
"link_Text": "Button Text" | |
} | |
} |
We can also add additional fields for other reference type fields like Multi List, Droplink, Image field etc. by adding the properties and transforms for those fields in CreateFieldModel and CreateFieldValuesModel methods respectively.
Comments
Post a Comment