This page is a work in progress.You can help improve it. →

Add support for reviews

Add support for reviews

We can now add support for restaurant reviews.

Database model

The first thing we will do is add a POCO model representing the Review. Notice we include the RestaurantId since one Review belongs to one Restaurant. We also set a default date for the CreatedAt and make its set method private so that it cannot be set via the API.

using System;
namespace TacoTuesday.Models
{
public class Review
{
public int Id { get; set; }
public string Summary { get; set; }
public string Body { get; set; }
public int Stars { get; set; }
public DateTime CreatedAt { get; private set; } = DateTime.Now;
public int RestaurantId { get; set; }
public Restaurant Restaurant { get; set; }
}
}

After adding this model we also add the review to the DatabaseContext

// Tell the context about the Review collection/table
public DbSet<Review> Reviews { get; set; }

Now we can create the migration.

dotnet ef migrations add AddReviews

and we can update the database

dotnet ef database update

Relationship

We also need to indicate that the Restaurant has many Reviews. We do this by adding a List based property to the Restaurant

public List<Review> Reviews { get; set; }

The Reviews property will allow us to navigate in code from a single restaurant to the list of associated reviews.

Restaurant API should return their associated reviews

On the main listing, we want the count of reviews and on an individual restaurant page, we want the list of all reviews.

To achieve these, we will use the Include method of EF Core to incorporate this data into our API response.

Change the logic in GetRestaurants to:

if (filter == null)
{
return await _context.Restaurants.OrderBy(restaurant => restaurant.Id).Include(restaurant => restaurant.Reviews).ToListAsync();
}
else
{
return await _context.Restaurants.OrderBy(restaurant => restaurant.Id).Where(restaurant => restaurant.Name.Contains(filter)).Include(restaurant => restaurant.Reviews).ToListAsync();
}

So that now each restaurant will also include an array of reviews.

NOTE: This is not the most efficient implementation. Imagine if our site became popular and started to receive heavy traffic. To display our homepage we would be fetching all the restaurants AND the complete list of reviews to count them. We'll review more efficient approaches in a later step. For now we'll go with this approach. Remember to focus on working code before perfect code.

Update the exampledata.sql to generate reviews

Add the following SQL to our seeds and rerun them to populate the database with a few reviews.

-- Ensure we truncate the table and restart the identity so our Id column starts at 1 each time
TRUNCATE TABLE "Restaurants", "Reviews" RESTART IDENTITY;
INSERT INTO "Restaurants" ("Name", "Description", "Address", "Telephone") VALUES ('Thoughtbeat', 'Inverse zero administration benchmark', '07 Meadow Vale Drive', '314-651-9791');
INSERT INTO "Restaurants" ("Name", "Description", "Address", "Telephone") VALUES ('Dabtype', 'Organized stable firmware', '7 Miller Park', '523-760-6681');
INSERT INTO "Restaurants" ("Name", "Description", "Address", "Telephone") VALUES ('Topdrive', 'Object-based interactive application', '65 Eliot Lane', '650-993-7074');
INSERT INTO "Restaurants" ("Name", "Description", "Address", "Telephone") VALUES ('Avaveo', 'Persistent zero defect process improvement', '2 Clarendon Junction', '715-663-5265');
INSERT INTO "Reviews" ("RestaurantId", "CreatedAt", "Summary", "Body", "Stars") VALUES (1, '2020-01-01 14:23:55', 'Yummy Food', 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Minima modi impedit quisquam sit, saepe enim placeat a vero voluptas asperiores atque laudantium in, nobis sunt blanditiis dignissimos. Deleniti, esse optio!', 3);
INSERT INTO "Reviews" ("RestaurantId", "CreatedAt", "Summary", "Body", "Stars") VALUES (1, '2020-01-01 18:23:55', 'Mmmmm, good', 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Minima modi impedit quisquam sit, saepe enim placeat a vero voluptas asperiores atque laudantium in, nobis sunt blanditiis dignissimos. Deleniti, esse optio!', 4);
psql --file=Models/exampledata.sql TacoTuesdayDatabase

Updating the interface to return the list of reviews

First we need to add a type definition for a review in types.ts

export type ReviewType = {
id?: number
summary: string
body: string
stars: number
createdAt?: string
restaurantId: number
}

NOTE: createdAt is a string because there is no Date type in JSON.

Then we can update the RestaurantType to reflect that it has an array of ReviewType

export type RestaurantType = {
id?: number
name: string
description: string
address: string
telephone: string
reviews: ReviewType[]
}

Then in the Restaurant.tsx we will add reviews: [] to our NullRestaurant so that our initial state will have an empty array of reviews.

const NullRestaurant: RestaurantType = {
name: '',
address: '',
description: '',
telephone: '',
reviews: [],
}

And in NewRestaurant.tsx we will need to update the default state:

const [newRestaurant, setNewRestaurant] = useState<RestaurantType>({
name: '',
description: '',
address: '',
telephone: '',
reviews: [],
})

We can now use that to count these in the user interface in SingleRestaurantFromList.tsx

Change the hardcoded:

2,188
({props.restaurant.reviews.length})

Now add similar Include code in the controller with GetRestaurant

// Find the restaurant in the database using Include to ensure we have the associated reviews
var restaurant = await _context.Restaurants.Include(restaurant => restaurant.Reviews).Where(restaurant => restaurant.Id == id).FirstOrDefaultAsync();

We are going to use the reviews property of the restaurant object to render each individual review. This is why having an "empty" representation of the restaurant in the state is useful.

Change the hardcoded:

(2,188)
({restaurant.reviews.length})

Updating the JSX for showing the list of reviews, we use map to loop over the restaurant.reviews (again, why that default state of reviews: [] is so important). We also only generate this part of the JSX if there are more than zero reviews.

Replace the contents of <ul className="reviews"> with:

<ul className="reviews">
{restaurant.reviews.map(review => (
<li key={review.id}>
<div className="author">
Gavin said: <em>{review.summary}</em>
</div>
<div className="body">
<p>{review.body}</p>
</div>
<div className="meta">
<span
className="stars"
style={{ '--rating': review.stars } as CSSStarsProperties}
aria-label={`Star rating of this location is ${review.stars} out of 5.`}
></span>
<time>{review.createdAt}</time>
</div>
</li>
))}
</ul>

We should see one restaurant that has two reviews (based on our seed data)

You will also notice that there are some features we haven't finished.

  • Each entry still says "Gavin says" -- We can fix this once we introduce the idea of users
  • The date formatting needs improvement.

We will come back and revise this in a later step.

© 2017 - 2021; Built with ♥ in St. Petersburg, Florida.