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; }
5:
6: public PageObjectCommentContainer()
7: {
8: this.Comments = new List<DynamicDataStoreComment>();
9: }
10: }
PageObjectCommentRepository
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;
5:
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.
AddComment
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);
6:
7: if (poCommentContainer == null)
8: {
9: poCommentContainer = new PageObjectCommentContainer();
10: }
11:
12: poCommentContainer.Comments.Add(ddsComment);
13: pageObjectManager.Save(CommentKey, poCommentContainer);
14: }
RemoveComment
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;
4:
5: if (!Identity.TryParse(commentId, out identity))
6: {
7: throw new InvalidOperationException("Comment id is not an Identity.");
8: }
9:
10: var ddsComment = commentStore.Load<DynamicDataStoreComment>(identity);
11: var pageObjectManager = this.pageObjectManagerFactory.GetPageObjectManagerForPage(ddsComment.PageId, ddsComment.PageLanguage);
12: var poCommentContainer = pageObjectManager.Load<PageObjectCommentContainer>(CommentKey);
13:
14: if (poCommentContainer == null)
15: {
16: return;
17: }
18:
19: poCommentContainer.Comments
20: .RemoveAll(c => c.Id == identity);
21:
22: pageObjectManager.Save(CommentKey, poCommentContainer);
23: this.commentStore.Delete(identity);
24: }
RemoveAllCommentsForPage
1: public void RemoveAllCommentsForPage(int pageId)
2: {
3: var pageObjectManager = pageObjectManagerFactory.GetPageObjectManagerForPage(pageId);
4: pageObjectManager.Delete(CommentKey);
5: }
GetCommentsForPage
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);
5:
6: if (poCommentContainer == null)
7: {
8: return new List<PageComment>();
9: }
10:
11: return poCommentContainer.Comments
12: .Select(c => this.commentMapper.MapToPageComment(c));
13: }
GetLatestComments
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: }
GetMostCommentedPageIds
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>();
7:
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.
Comments