Try our conversational search powered by Generative AI!

Niklas Melinder
Nov 14, 2010
(1 votes)

EPiSocial – Using PageObjects

After my previous post on the EPiSocial plugin, Paul Smith pointed out that I should consider using Page Objects (PO) rather than plain Dynamic Data Store (DDS) to store page related data. The upside of using PO is that it provides support for mirroring and import/export. The downside being that it handles things on a page by page basis which makes queries over multiple pages a bit tricky. This is necessary when doing things like getting pages with the highest rating or getting pages with most comments.

Luckily, the process of changing the plugin from using DDS to PO is as simple as creating new implementations of the ICommentRepository and IRatingRepository interfaces.

After some minor changes from the previous version they look like this:

   1: public interface ICommentRepository
   2:     {
   3:         void AddComment(PageComment pageComment);   
   4:         void RemoveComment(string commentId);
   5:         void RemoveAllCommentsForPage(int pageId);
   6:         IEnumerable<PageComment> GetCommentsForPage(int pageId, string pageLanguage);
   7:         IEnumerable<PageComment> GetLatestComments(int count, string pageLanguage);
   8:         IEnumerable<int> GetMostCommentedPageIds(int count, string pageLanguage);
   9:     }
   1: public interface IRatingRepository
   2:     {
   3:         void AddRating(PageRating pageRating);
   4:         void RemoveAllRatingsForPage(int pageId);
   5:         double GetAverageRatingForPage(int pageId, string pageLanguage);
   6:         int GetNumberOfRatingsForPage(int pageId, string pageLanguage);
   7:         IEnumerable<int> GetHighestRatedPageIds(int count, string pageLanguage);        
   8:     }

In this post I will focus on the PO implementation of ICommentRepository.

Page Objects

The first thing I noted was that the documentation for PO was rather slim. Nothing in the SDK, all I found was a single Tech Note that described the features briefly.

One thing that differs when using PO instead of DDS is that a PO is stored as a single object per page. To be able to store multiple comments for a single page I created a container object to hold the comments.

   1: public class PageObjectCommentContainer : IDynamicData
   2:     {
   3:         public Identity Id { get; set; }
   4:         public List<DynamicDataStoreComment> Comments { get; set; }
   6:         public PageObjectCommentContainer()
   7:         {
   8:             this.Comments = new List<DynamicDataStoreComment>();
   9:         }
  10:     }


The PageObjectCommentRepository takes two dependencies. The first being a CommentMapper that handles the mapping described below. The second one is a PageObjectManagerFactory that handles creation of PageObjectManager objects based on page id.

   1: private const string CommentKey = "PageComments";
   2: private readonly CommentMapper commentMapper;
   3: private readonly PageObjectManagerFactory pageObjectManagerFactory;
   4: private readonly DynamicDataStore commentStore;
   6: public PageObjectCommentRepository(CommentMapper commentMapper, PageObjectManagerFactory pageObjectManagerFactory)
   7:         {
   8:             this.commentMapper = commentMapper;
   9:             this.pageObjectManagerFactory = pageObjectManagerFactory;
  10:             this.commentStore = DynamicDataStoreFactory.Instance.GetStore(typeof(DynamicDataStoreComment));
  11:         }

About mapping

Before saving comments to the DDS the PageComment object is mapped to a DynamicDataStoreComment object that implements the necessary IDynamicData interface. The reverse thing happens when getting comments from the store. This mapping might seem unnecessary, why don´t use the DynamicDataStoreComment directly instead? There are good reasons.

One being that I want the plugin to be extendable. If I or someone else wanted to create a different implementation using a different storage, that implementation probably won´t care about EPiServer.Data.

Another reason is that testing code using the API should not have to know about EPiServer.Data either.


   1: public void AddComment(PageComment pageComment)
   2:         {
   3:             var pageObjectManager = this.pageObjectManagerFactory.GetPageObjectManagerForPage(pageComment.PageId, pageComment.PageLanguage);
   4:             var ddsComment = this.commentMapper.MapToDynamicDataStoreComment(pageComment);
   5:             var poCommentContainer = pageObjectManager.Load<PageObjectCommentContainer>(CommentKey);
   7:             if (poCommentContainer == null)
   8:             {
   9:                 poCommentContainer = new PageObjectCommentContainer();                
  10:             }
  12:             poCommentContainer.Comments.Add(ddsComment);
  13:             pageObjectManager.Save(CommentKey, poCommentContainer);               
  14:         }


When removing a comment we first have to load the object, remove the comment from the list and save it back again. I also found that I had to delete the comment directly from the DDS for it to disappear completely, otherwise it will still be around when making queries like getting the latest comments below.

   1: public void RemoveComment(string commentId)
   2:         {
   3:             Identity identity;
   5:             if (!Identity.TryParse(commentId, out identity))
   6:             {
   7:                 throw new InvalidOperationException("Comment id is not an Identity.");
   8:             }
  10:             var ddsComment = commentStore.Load<DynamicDataStoreComment>(identity);
  11:             var pageObjectManager = this.pageObjectManagerFactory.GetPageObjectManagerForPage(ddsComment.PageId, ddsComment.PageLanguage);
  12:             var poCommentContainer = pageObjectManager.Load<PageObjectCommentContainer>(CommentKey);
  14:             if (poCommentContainer == null)
  15:             {
  16:                 return;
  17:             }
  19:             poCommentContainer.Comments
  20:                 .RemoveAll(c => c.Id == identity);
  22:             pageObjectManager.Save(CommentKey, poCommentContainer);
  23:             this.commentStore.Delete(identity);
  24:         }


   1: public void RemoveAllCommentsForPage(int pageId)
   2:        {
   3:            var pageObjectManager = pageObjectManagerFactory.GetPageObjectManagerForPage(pageId);
   4:            pageObjectManager.Delete(CommentKey);
   5:        }


   1: public IEnumerable<PageComment> GetCommentsForPage(int pageId, string pageLanguage)
   2:         {
   3:             var pageObjectManager = this.pageObjectManagerFactory.GetPageObjectManagerForPage(pageId, pageLanguage);
   4:             var poCommentContainer = pageObjectManager.Load<PageObjectCommentContainer>(CommentKey);
   6:             if (poCommentContainer == null)
   7:             {
   8:                 return new List<PageComment>();
   9:             }
  11:             return poCommentContainer.Comments
  12:                 .Select(c => this.commentMapper.MapToPageComment(c));
  13:         }


So far it´s been pretty straight forward. But since the PageObjectManager works on a per page basis, this kind of query cannot be done using it. But since PO uses DDS in the background, we can create a query using DDS directly to get the latest comments:

   1: public IEnumerable<PageComment> GetLatestComments(int count, string pageLanguage)
   2:         {
   3:             return this.commentStore.Items<DynamicDataStoreComment>()
   4:                 .Where(c => c.PageLanguage == pageLanguage)
   5:                 .OrderByDescending(c => c.Created)
   6:                 .Select(c => commentMapper.MapToPageComment(c))
   7:                 .Take(count);
   8:         }


The same thing applies here, we have to use DDS directly to get what we are looking for.

   1: public IEnumerable<int> GetMostCommentedPageIds(int count, string pageLanguage)
   2:         {
   3:             return this.commentStore.Items<DynamicDataStoreComment>()
   4:                 .Where(c => c.PageLanguage == pageLanguage)
   5:                 .GroupBy(c => c.PageId)
   6:                 .OrderByDescending(c => c.Count())
   7:                 .Select(c => c.Key)
   8:                 .ToList()
   9:                 .Take(count);
  10:         }

Unfortunately, due to a bug in the DDS implementation we cannot call Take() directly but have to call ToList() first. This is not optimal and I´m hoping this is fixed in the next release of EPiServer.

Wire it up

Now that we have our new PO repository ready we have to tell the application that we wish to use it instead of the default DDS implementation.

EPiSocial uses StructureMap to wire up it´s dependencies. To override the default setup we have to add a StructureMap registry of our own and the plugin will automatically pick it up during it´s initialization. So we add the following registry to the web application:
   1: public class PageObjectRegistry : Registry
   2:     {
   3:         public PageObjectRegistry()
   4:         {
   5:             For<ICommentRepository>()
   6:                 .Use<PageObjectCommentRepository>();
   8:             For<IRatingRepository>()
   9:                 .Use<PageObjectRatingRepository>();
  10:         }
  11:     }

For more on StructureMap check out this post by Stefan.

That´s all, we are now ready to use the PO version of the plugin.

An updated version of the plugin can be downloaded here. It now contains a EPiSocial.PageObjects assembly with the new PO implementation.

Nov 14, 2010


Please login to comment.
Latest blogs
Optimizely and the never-ending story of the missing globe!

I've worked with Optimizely CMS for 14 years, and there are two things I'm obsessed with: Link validation and the globe that keeps disappearing on...

Tomas Hensrud Gulla | Apr 18, 2024 | Syndicated blog

Visitor Groups Usage Report For Optimizely CMS 12

This add-on offers detailed information on how visitor groups are used and how effective they are within Optimizely CMS. Editors can monitor and...

Adnan Zameer | Apr 18, 2024 | Syndicated blog

Azure AI Language – Abstractive Summarisation in Optimizely CMS

In this article, I show how the abstraction summarisation feature provided by the Azure AI Language platform, can be used within Optimizely CMS to...

Anil Patel | Apr 18, 2024 | Syndicated blog

Fix your Search & Navigation (Find) indexing job, please

Once upon a time, a colleague asked me to look into a customer database with weird spikes in database log usage. (You might start to wonder why I a...

Quan Mai | Apr 17, 2024 | Syndicated blog