My Favorites List

The My Favorites Feature

Add a Movie to your Favorites

Clicking the My Favorites link in the main navigation will take you to page which contains a list of Movies that you can want to revisit later.

When a logged in user hovers their mouse over a Movie on the website, a bookmark icon appears in the top left hand corner. Clicking this icon will either add or remove the Movie from the user’s Favorites list.

When adding a Movie to the list, a POST request it sent to the /api/favorites/{id} endpoint. When this happens, the following chain of events will occur:

  1. The server directs the request to the route handler in the AccountController, which verifies the user’s JWT token and stores it in the request, before handling the request

  2. The route handler uses an instance of the FavoriteService.

  3. The AddAsync() method is then called on the FavoriteService instance with the user ID taken from the JWT token, and the ID of the movie that has been extracted from the request URL.

  4. It is then the responsibility of the FavoriteService to persist this information and format a response.

Likewise, when it is clicked for a Movie that has already been favorited, a DELETE request is sent to the same URL, and the Movie is removed from the list.

When adding a Movie to a User’s Favorite list, a relationship is created between the User and Movie with the type HAS_FAVORITE.

Adding a Movie to My Favorites

For the first part of this challenge, modify the AddAsync() method to open a new database session, run the Cypher statement to create te relationship, close the session and return the movie details along with an additional favorite property.

1. Open the Session

Call the AsyncSession() method on the Driver instance to open a new session:

c#
// Open a new session
using var session = _driver.AsyncSession();
// Do something with the session...

2. Create the Relationship

In a write transaction, run a Cypher statement to create the HAS_FAVORITE relationship between the User and Movie nodes, with a createdAt property to represent when the relationship was created.

Click here to reveal the Cypher statement
cypher
MATCH (u:User {userId: $userId})
MATCH (m:Movie {tmdbId: $movieId})

MERGE (u)-[r:HAS_FAVORITE]->(m)
ON CREATE SET r.createdAt = datetime()

RETURN m {
    .*,
    favorite: true
} AS movie
c#
var query = @"
    MATCH (u:User {userId: $userId})
    MATCH (m:Movie {tmdbId: $tmdbId})
    
    MERGE (u)-[r:HAS_FAVORITE]->(m)
    ON CREATE SET u.createdAt = datetime()

    RETURN m {
        .*,
        favorite: true
    } AS movie";
var cursor = await tx.RunAsync(query, new { userId, tmdbId });

3. Does the Movie exist?

If no records are returned, you can safely assume that the either the User or Movie do not exist. In this case, throw a NotFoundException with an appropriate error message.

c#
if (!await cursor.FetchAsync())
{
    throw new NotFoundException($"Couldn't create a favorite relationship for User {userId} and Movie {tmdbId}");
}

4. Return results

Then, finally return the movie value from the first record returned from the database.

c#
return cursor.Current["movie"].As<Dictionary<string, object>>();

Working Solution

Click here to reveal the completed AddAsync() method
c#
Neoflix/Services/FavoriteService.cs
public async Task<Dictionary<string, object>> AddAsync(string userId, string tmdbId)
{
    await using var session = _driver.AsyncSession();

    // Create HAS_FAVORITE relationship within a Write Transaction
    return await session.ExecuteWriteAsync(async tx =>
    {
        var query = @"
            MATCH (u:User {userId: $userId})
            MATCH (m:Movie {tmdbId: $tmdbId})
            
            MERGE (u)-[r:HAS_FAVORITE]->(m)
            ON CREATE SET u.createdAt = datetime()

            RETURN m {
                .*,
                favorite: true
            } AS movie";
        var cursor = await tx.RunAsync(query, new { userId, tmdbId });

        if (!await cursor.FetchAsync())
        {
            throw new NotFoundException($"Couldn't create a favorite relationship for User {userId} and Movie {tmdbId}");
        }

        return cursor.Current["movie"].As<Dictionary<string, object>>();
    });
}

Removing a Movie from My Favorites

The second part of this challenge is to write the code to remove a movie from the My Favorites list.

The code for deleting the HAS_FAVORITE relationship will be similar, only the Cypher statement will change.

Instead of two separate MATCH clauses, we can instead attempt to find the pattern within a single clause. If the relationship (with an variable of r) exists, we will delete it and then return the movie information with favorite set to false.

cypher
MATCH (u:User {userId: $userId})-[r:HAS_FAVORITE]->(m:Movie {tmdbId: $movieId})
DELETE r

RETURN m {
    .*,
    favorite: false
} AS movie

Use the code from the AddAsync() method above to implement the remove() function. If you get stuck, you can reveal the completed method below.

Working Solution

Click here to reveal the completed remove() method
c#
Neoflix/Services/FavoriteService.cs
public async Task<Dictionary<string, object>> RemoveAsync(string userId, string tmdbId)
{
    await using var session = _driver.AsyncSession();

    // Delete the HAS_FAVORITE relationship within a Write Transaction
    return await session.ExecuteWriteAsync(async tx =>
    {
        var query = @"
            MATCH (u:User {userId: $userId})-[r:HAS_FAVORITE]->(m:Movie {tmdbId: $tmdbId})
            DELETE r
            RETURN m {
              .*,
              favorite: false
            } AS movie";
        var cursor = await tx.RunAsync(query, new { userId, tmdbId });

        if (!await cursor.FetchAsync())
        {
            throw new NotFoundException($"Couldn't delete a favorite relationship for User {userId} and Movie {tmdbId}");
        }

        return cursor.Current["movie"].As<Dictionary<string, object>>();
    });
}

Listing My Favorites

Finally, the AllAsync() method in the FavoriteService currently returns a hardcoded list of Movies from the fixture popular.json.

c#
/Neoflix/Services/FavoriteService.cs
public async Task<Dictionary<string, object>[]> AllAsync(string userId, string sort = "title",
    Ordering order = Ordering.Asc, int limit = 6, int skip = 0)
{
    await using var session = _driver.AsyncSession();

    return await session.ExecuteReadAsync(async tx =>
    {
        var query = $@"
            MATCH (u:User {{userId: $userId}})-[r:HAS_FAVORITE]->(m:Movie)
            RETURN m {{
                .*,
                favorite: true
            }} AS movie
            ORDER BY m.{sort} {order.ToString("G").ToUpper()}
            SKIP $skip
            LIMIT $limit";

        var cursor = await tx.RunAsync(query, new {userId, skip, limit});
        var records = await cursor.ToListAsync();
        return records
            .Select(record => record["movie"].As<Dictionary<string, object>>())
            .ToArray();
    });
}

Update this method to return a paginated list of movies that the user has added to their My Favorites list.

Click here to reveal the Cypher statement
cypher
MATCH (:User {userId: $userId})-[:HAS_FAVORITE]->(m)

RETURN m {
    .*,
    favorite: true
} AS movie

In the query the method we should also add dynamic sorting and pagination, with the string substitution for the sorting like we did in other places.

You have already written similar code a few times, so try to implement this one on your own. If you get stuck, you can view the full solution below.

Click here to reveal the completed AllAsync() method
c#
Neoflix/Services/FavoriteService.cs
public async Task<Dictionary<string, object>[]> AllAsync(string userId, string sort = "title",
    Ordering order = Ordering.Asc, int limit = 6, int skip = 0)
{
    await using var session = _driver.AsyncSession();

    return await session.ExecuteReadAsync(async tx =>
    {
        var query = $@"
            MATCH (u:User {{userId: $userId}})-[r:HAS_FAVORITE]->(m:Movie)
            RETURN m {{
                .*,
                favorite: true
            }} AS movie
            ORDER BY m.{sort} {order.ToString("G").ToUpper()}
            SKIP $skip
            LIMIT $limit";

        var cursor = await tx.RunAsync(query, new {userId, skip, limit});
        var records = await cursor.ToListAsync();
        return records
            .Select(record => record["movie"].As<Dictionary<string, object>>())
            .ToArray();
    });
}

Testing

To test that this functionality has been correctly implemented, run the following code in a new terminal session:

sh
Running the test
dotnet test --logger "console;verbosity=detailed" --filter "Neoflix.Challenges._07_FavoritesList"

The test file is located at Neoflix.Challenges/07-FavoritesList.cs.

Are you stuck? Click here for help

If you get stuck, you can see a working solution by checking out the 07-favorites-list branch by running:

sh
Check out the 07-favorites-list branch
git checkout 07-favorites-list

You may have to commit or stash your changes before checking out this branch. You can also click here to expand the Support pane.

Verifying the Test

If the test has run successfully, a user with the email address graphacademy.favorite@neo4j.com will have added the movie Toy Story to their list of favorites.

Hint

You can run the following query to check for the user within the database. If the shouldVerify value returns true, the verification should be successful.

cypher
MATCH (u:User {email: "graphacademy.favorite@neo4j.com"})-[:HAS_FAVORITE]->(:Movie {title: 'Toy Story'})
RETURN true AS shouldVerify

Solution

The following statement will mimic the behaviour of the test, merging a new :User node with the email address graphacademy.favorite@neo4j.com and ensuring that a node exists for the movie Toy Story. The test then merges a :HAS_FAVORITE relationship between the user and movie nodes.

cypher
MERGE (u:User {userId: '9f965bf6-7e32-4afb-893f-756f502b2c2a'})
SET u.email = 'graphacademy.favorite@neo4j.com'

MERGE (m:Movie {tmdbId: '862'})
SET m.title = 'Toy Story'

MERGE (u)-[r:HAS_FAVORITE]->(m)

RETURN *

Once you have run this statement, click Try again…​* to complete the challenge.

Module Summary

In this Challenge, you have written the code to manage a HAS_FAVORITE relationship between a User and a Movie within a write transaction.

In the next Challenge, you will write code to execute multiple queries in the same transaction.