React and EPiServer: Aggregated output with EPiServer.ContentDeliveryApi

If you read my previous article on
React and EPiServer: Moving to EPiServer Headless (EPiServer.ContentDeliveryApi) with friendly URLs – Quick POC, you’ll know I wanted to output EPiServer Content information by friendly URL to a page rather than using the headless API’s own paths. This I did, but what about aggreagated information? You know, the stuff that are using for instance content from other EPiServer properties to combine into a new value, or just random processed data that you might calculate in a page controller and pass to a Razor view via a model.

Aggregated content data with limited number of controllers using the JOS Content Serializer

When you’re using the JOS Content Serializer (The EPiServer’s Content Delivery Api seems to work about the same way), you pass a content object into a method and get a serializable object back. The method maps the IContent into the new object. You could of course do this in controllers, but that’d mean you would need lots of partial block controllers. So setting this up in a base controller you will rarely have to touch it again.

public abstract class PageControllerSiteBase<TModel, TJsonModel> : PageController<TModel>
    where TModel : SiteBasePage
    where TJsonModel : SiteBaseJsonModel
{
  private readonly Lazy<IPageRouteHelper> _pageRouteHelper = new Lazy<IPageRouteHelper>(() => ServiceLocator.Current.GetInstance<IPageRouteHelper>());
  private readonly Lazy<IPropertyManager> _propertyManager = new Lazy<IPropertyManager>(() => ServiceLocator.Current.GetInstance<IPropertyManager>());

  protected override void OnActionExecuting(ActionExecutingContext filterContext)
  {
    base.OnActionExecuting(filterContext);

    var jsonViewModel = Activator.CreateInstance<TJsonModel>();
    var currentPage = _pageRouteHelper.Value.Page as TModel;

    var settings = new ContentSerializerSettings
    {
      UseCustomPropertiesHandler = true,
      GlobalWrapContentAreaItems = false
    };
    jsonViewModel.Content = _propertyManager.Value.GetStructuredData(currentPage, settings);

I removed all the noise from the above snippet, but that’s one way of doing it. The information from the currentPage object will be mapped into a serialized object placed in the Content dictionary property of the model. This will later be serialized into JSON before being passed to React.

So in order to automatically have the JOS Content Serializer map your aggreagated data into it’s dictionary and treat it as any EPiServer property, you could do as below. Of course you could also add a controller putting more stuff into the view model before serializing it, but that’s probably better kept for edge cases.

[ContentType( ... )]
public class ArticlePage : SiteBasePage
{
  private readonly Lazy<ISomeService> _service = new Lazy<ISomeService>(() => ServiceLocator.Current.GetInstance<ISomeService>());

  [Display(Name = "Real EPiServer Property")]
  public virtual string SomeRealProperty { get; set; }

  [Ignore]
  [ContentSerializerInclude]
  public virtual Dictionary<string, string> AggregatedStuff => _service.Value.GetStuff(this);

The above page type class has a real EPiServer property that is put into the database and may contain data entered by the website editors. The AggregatedStuff property on the other hand has the IgnoreAttribute, and will not be inserted into the EPiServer database. It should only be outputted into the JSON.

With JOS Content Serializer, this is done using the ContentSerializerIncludeAttribute. As of yet I’ve seen no such attribute in the EPiServer ContentDeliveryApi, however, it doesn’t matter. In the code above the GetStuff method will be called with the current page as it is being mapped by the JOS Content Serializer’s mapping method.

Aggregated content data using EPiServer’s Content Delivery Api

So how do we achieve the same with the EPiServer ContentDeliveryApi? Well, let’s go back to the base controller class.

public abstract class PageControllerSiteBase<TModel, TJsonModel> : PageController<TModel>
    where TModel : SiteBasePage
    where TJsonModel : SiteBaseJsonModel
{
  private readonly Lazy<IPageRouteHelper> _pageRouteHelper = new Lazy<IPageRouteHelper>(() => ServiceLocator.Current.GetInstance<IPageRouteHelper>());
  private readonly Lazy<IContentModelMapper> _contentModelMapper = new Lazy<IContentModelMapper>(() => ServiceLocator.Current.GetInstance<IContentModelMapper>());

  protected override void OnActionExecuting(ActionExecutingContext filterContext)
  {
    base.OnActionExecuting(filterContext);

    var jsonViewModel = Activator.CreateInstance<TJsonModel>();
    var currentPage = _pageRouteHelper.Value.Page as TModel;

    var epiModel = _contentModelMapper.Value.TransformContent(currentPage);

    foreach (var info in currentPage.GetType().GetProperties())
    {
      // Using the JOS Content Serializer attribute for this,
      // but you probably want to create your own.
      if (!Attribute.IsDefined(info, typeof(ContentSerializerIncludeAttribute)))
      {
        continue;
      }
      epiModel.Properties.Add(info.Name, info.GetValue(currentPage));
    }

    jsonViewModel.Content = epiModel.Properties;

I’ve updated the OnActionExecuting method from the previous example to make it use the EPiServer Content Delivery Api instead.

What happens is that EPiServer’s mapping method is used on the page in order to get it’s mapped object. Then the page’s properties are looped through in order to find all the ones decorated with a certain Include-attribute. For this example I’ve reused the ContentSerializerIncludeAttribute from the JOS Content Serializer just to make things more clear, however, you will probably have to create your own.

All the properties that are decorated with the include attribute will get added to the EPiServer model’s Properties dictionary. This is also where the property getter will be called, and therefore also the service responsible for aggregating the output data.

Since we in my client’s case don’t need any of the EPiServer page meta data outside of the Properties dictionary, I just pass that into the Content property of our own model.

2 Comments

    • Mathias Kunto April 10, 2018