Building product reviews with Episerver Social
Product reviews are a very common form of user-generated content (UGC) in commerce sites. If I were to ask you to describe to me what a “product review” is, however, I suspect that it would differ significantly from what anyone else might envision.
- It's simple content -- maybe threaded, maybe linear.
- Perhaps there are one or more ratings to express the quality of various facets of the product.
- Maybe there are additional business details, such as a purchase date, to be captured.
The fact is that every organization has a different perspective on what comprises a product review.
As a UGC platform, Episerver Social is tailored toward implementing these types of solutions. The developer has creative control to model their UGC to suit their particular use cases. They can leverage the platform's native social constructs, then can compose them and customize them to their needs, and do so in a system tailored to supporting the demands of delivering UGC.
The Episerver Social team has recently released a demo implementation of product reviews on GitHub. This has been built in a clone of Episerver's Quicksilver commerce demonstration and is intended to illustrate:
- Modeling user-generated content
- Defining relationships between UGC and site resources
- Use of the Episerver Social framework to manage and deliver content
I encourage you to download it, configure it with your Episerver Social account, and play with the reviews found on its product pages.
Composing reviews
Episerver Social provides fundamental building blocks for modeling social content. These building blocks can be applied in any number of ways and may be further enhanced with data of your own design. Before setting out to implement your social features, it’s important to consider how you intend your social content to be composed.
Consider the mockup of product reviews below to be a visual representation of our requirements for this implementation.
When considering this content, we can see several things:
- The focal point of an individual review appears to be the contributor's message.
- A review displays a contributor's:
- Nickname
- Location
- Date of contribution
- Review title
- Review body
- The reviews are linear.
- The most recent N reviews are being displayed.
- Each review appears to have an accompanying rating.
- An average rating appears atop the list of reviews.
These considerations factor into how we ultimately model our social content. Each Episerver Social feature can be thought of more generally as basic social data structures. A comment is hierarchical content, a rating is appraised content, moderation is stateful content, and so on.
With that in mind, how might we apply Episerver Social to model the reviews detailed in the mockup above?
- If the message is the focal point, a comment may be suitable as the core data structure from which to shape our content.
- The native model already accommodates the notion of a content body and contribution date.
- Its hierarchical nature may suit us well if reviews are intended to allow for replies (now or in the future).
- It can be easily queried to deliver the most recent N contributions.
- Ratings provide an efficient means of accumulating values and tabulating averages.
- By allowing us to define and store a custom data model alongside the native data structures, Episerver Social’s Composites feature provides a convenient extensibility point for capturing the review's remaining attributes.
The diagram below demonstrates how Episerver Social features might be composed with extended data to design a product review. In subsequent activities, we will implement this model.
Referencing content and users
Social content is largely supplementary in the sense that it compliments some other application entity. For example, a comment is contributed to an article, an image, etc. A comment is also contributed by some user or site visitor.
These users and content items may take many different forms and may reside in many different repositories. For that reason, Episerver Social maintains these relationships by reference.
A Reference establishes a relationship between social content and the resources or users to which it applies. It enables you to associate social content with any uniquely identifiable entity. Episerver Social does not need to know anything about what your application entities are or where they reside.
So, we need to define a reference scheme for a couple of different application entities:
- The product being reviewed
- An Episerver Social Comment must be associated with some parent entity (the thing being commented upon).
- An Episerver Social Rating must be attributed to some target entity (the thing being rated).
- The contributor of the review
- An Episerver Social Comment may optionally be assigned an author.
- An Episerver Social Rating must identify the rater.
A good Reference is one that is capable of uniquely identifying your application’s entities and should be easily interpreted by your application. A custom URI often provides a suitable scheme.
Take a look at the ReviewService, found in the demo implementation of product reviews. This service encapsulates the business logic supporting our product review implementation.
Within the Add(ReviewSumissionViewModel) method, which is responsible for saving new reviews, we must first identify the product being reviewed and the contributor of the review. To do so, we must construct a Reference.
/// <summary>
/// Creates a reference identifying a review contributor.
/// </summary>
/// <param name="nickname">Nickname identifying the review contributor</param>
/// <returns>Reference identifying a review contributor</returns>
private static EPiServer.Social.Common.Reference CreateContributorReference(string nickname)
{
return EPiServer.Social.Common.Reference.Create($"visitor://{nickname}");
}
/// <summary>
/// Creates a reference identifying a product.
/// </summary>
/// <param name="nickname">Content code identifying the product</param>
/// <returns>Reference identifying a product</returns>
private static EPiServer.Social.Common.Reference CreateProductReference(string productCode)
{
return EPiServer.Social.Common.Reference.Create($"product://{productCode}");
}
The supporting CreateContributorReference(string) and CreateProductReference(string) methods in this class demonstrate the construction of these references. You'll notice, in both cases, that we are using a simple URI format for a scheme.
For the contributor, we are leveraging the contributor's nickname as their identifier. Admittedly, this is not a good unique identifier. You might consider a visitor's user ID, username, and the type of membership provider in defining your reference scheme.
For the product, we are leveraging its product code as an identifier. Depending on your requirements, a URI might also account for variant identifiers or product facets such as fit, size, and feel.
Adding a rating
We have just defined how to represent relationships between our application and our social content. Now, we can begin to store that social content. First, we’ll add the rating that is submitted with a review.
Ratings are described with a simple model, called Rating, and are managed through the Episerver Social framework’s IRatingService interface.
To construct a rating, we must provide:
- A target (Reference) - the thing being rated
- A rater (Reference) - the contributor of the rating
- A value (RatingValue) - the rating's numeric value
We previously defined References for the product being reviewed (the target) and the contributor of the review (the rater). We will use those references now, as we construct and store a rating.
In the ReviewService's Add(ReviewSubmissionViewModel) method, you'll notice the following:
// Add the contributor's rating for the product
var submittedRating = new Rating(contributor, product, new RatingValue(review.Rating));
var storedRating = this.ratingService.Add(submittedRating);
With this code, we constructed a Rating and submitted it with the Add(Rating) method of the IRatingService. The method returns a new instance of Rating, which has been populated with additional system-generated (e.g. an ID, timestamp, etc.). We will use some of that data shortly.
As each Rating is added to the system, Episerver Social is automatically tabulating the statistics for the target of that rating (our product). We will retrieve those statistics when we present the reviews later.
Note: The Ratings feature allows one rating per contributor (rater) for a target. If you encounter a DuplicateRatingException while submitting a review, you are using the nickname of a visitor who has already contributed a review for the product. Choose an alternative nickname.
Modeling a composite comment
Our application is now capable of storing the rating that accompanies a review, but we have yet to deal with the "meat" of the review.
Episerver Social’s Comment gives us a convenient starting place. It accommodates a message body, an author, a contribution date, and a relationship to application content (e.g. the products). However, our requirements dictate that we must display additional information. This includes a review title as well as the contributor’s location and nickname.
Episerver Social allows you to extend any of its core data models by pairing them with data of your own design. This pairing is called a Composite. The data that you design for a Composite is referred to as extension data. Extension data is simply a serializable .NET class defined within your application.
The Composites that you define can be stored and retrieved just like native Episerver Social models. The values contained with your extension data can even be applied in custom filtering and sorting.
Examine the Review and ReviewRating classes, defined in our demo implementation.
public class Review
{
public string Title { get; set; }
public string Nickname { get; set; }
public string Location { get; set; }
public ReviewRating Rating { get; set; }
}
public class ReviewRating
{
public int Value { get; set; }
public string Reference { get; set; }
}
Compare the implementation of these extension data classes and the definition of the Episerver Social Comment class with the logical model of a product review (illustrated above in "Composing reviews"). Notice how the Comment and extension data complement each other to fulfil the logical model that we brainstormed.
Adding a composite comment
With our extension data defined, we’re finally ready to store the review. We will store this as a Composite of Comment and Review (our extension data class). Comments are managed through the Episerver Social framework’s ICommentService interface.
To construct a Comment, we must provide:
- A parent (Reference) – the thing being commented upon
- An author (Reference) – the comment contributor
- A body (String) – the content of the comment
- A visibility flag (Boolean) – the initial status of the comment
We have previously defined References for the product being reviewed (the parent) and the contributor of the review (the author). We will use those references again to construct our Comment. We will use our Review extension data class to supply the remaining details.
Again, in the ReviewService's Add(ReviewSubmissionViewModel) method, you'll notice the following:
// Compose a comment representing the review
var comment = new Comment(product, contributor, review.Body, true);
var extension = new Review
{
Title = review.Title,
Location = review.Location,
Nickname = review.Nickname,
Rating = new ReviewRating
{
Value = review.Rating,
Reference = storedRating.Id.Id
}
};
// Add the composite comment for the product
this.commentService.Add(comment, extension);
Here, we instantiated both a Comment and our Review extension data. We have populated with the inputs from the review submission, and stored it using Episerver Social's ICommentService.
Retrieving rating statistics
In addition to managing ratings, the IRatingService also allows you to retrieve the aggregate statistics for a target (the thing being rated). These statistics are automatically tabulated by Episerver Social as ratings accumulate in the system.
These statistics allow us to calculate the overall rating for a product, as shown highlighted in blue in the mockup below.
To retrieve the statistics for a product, we will use the Get(Criteria<RatingStatisticsFilter>) method of IRatingService. This method accepts criteria, which allows us to specify the parameters of our query, and returns a page of RatingStatistics. Those parameters include filters, sorting rules, and paging information.
The ReviewService's GetProductStatistics(Reference) method implements the logic necessary to retrieve the tabulated rating statistics for the product:
/// <summary>
/// Gets the rating statistics for the identified product
/// </summary>
/// <param name="product">Reference identifying the product</param>
/// <returns>Rating statistics for the product</returns>
private RatingStatistics GetProductStatistics(EPiServer.Social.Common.Reference product)
{
var statisticsCriteria = new Criteria<RatingStatisticsFilter>()
{
Filter = new RatingStatisticsFilter()
{
Targets = new[] { product }
},
PageInfo = new PageInfo()
{
PageSize = 1
}
};
return this.ratingService.Get(statisticsCriteria).Results.FirstOrDefault();
}
The method accepts a Reference identifying the product, for which statistics should be retrieved, and uses it to define a query Criteria identifying it as the target. The Criteria also includes paging information dictating that a single result should be returned. Finally, it passes the criteria to the Get(Criteria<RatingStatisticsFilter>) method of IRatingService to retrieve the product's statistics.
Retrieving composite comments
All that is remaining in our implementation now is the retrieval of the Composite Comments, which represent our reviews. Much like IRatingService, ICommentService also allows you to specify criteria to retrieve the content that it manages.
For retrieving Composites, all Episerver Social services provide an overloaded method Get(CompositeCriteria<TFilter, TExtension>). This criteria provides you access to all of the native filtering and sorting capabilities while adding the ability to define custom filters against your extension data fields.
We will use CompositeCriteria<CommentFilter,Review> to retrieve the Composite Comments for our application.
The ReviewService's GetProductReviews(Reference) method implements the logic necessary to retrieve the comments that have been contributed for the product:
/// <summary>
/// Gets a collection of reviews for the identified product.
/// </summary>
/// <param name="product">Reference identifying the product</param>
/// <returns>Collection of reviews for the product</returns>
private IEnumerable<Composite<Comment, Review>> GetProductReviews(EPiServer.Social.Common.Reference product)
{
var commentCriteria = new CompositeCriteria<CommentFilter, Review>()
{
Filter = new CommentFilter
{
Parent = product
},
PageInfo = new PageInfo
{
PageSize = 20
},
OrderBy = new List<SortInfo>
{
new SortInfo(CommentSortFields.Created, false)
}
};
return this.commentService.Get(commentCriteria).Results;
}
This method defines criteria that will retrieve the most recent twenty Composite Comments for the specified product. The criteria includes a parent filter, identifying the product. It includes paging information, dictating that up to 20 results should be returned. It also includes a sorting rule, indicating that results should be ordered by creation date, in descending order.
Presenting reviews
Finally, our application must adapt the comments and statistical data that have been retrieved into a form appropriate for presentation in the view. This demonstration implements a simple adapter pattern to perform this translation.
internal static class ViewModelAdapter
{
public static ReviewStatisticsViewModel Adapt(RatingStatistics statistics)
{
var viewModel = new ReviewStatisticsViewModel();
if (statistics != null)
{
viewModel.OverallRating = Convert.ToDouble(statistics.Sum) / Convert.ToDouble(statistics.TotalCount);
viewModel.TotalRatings = statistics.TotalCount;
}
return viewModel;
}
public static IEnumerable<ReviewViewModel> Adapt(IEnumerable<Composite<Comment, Review>> reviews)
{
return reviews.Select(Adapt);
}
private static ReviewViewModel Adapt(Composite<Comment, Review> review)
{
return new ReviewViewModel
{
AddedOn = review.Data.Created,
Body = review.Data.Body,
Location = review.Extension.Location,
Nickname = review.Extension.Nickname,
Rating = review.Extension.Rating.Value,
Title = review.Extension.Title
};
}
}
Voilà! You've implemented a product review with Episerver Social.
Comments