Movie Details

There are two methods remaining in the MovieService that are currently returning hardcoded data.

  • FindByIdAsync() - should return information about a movie, including a list of actors, director, and genres.

  • GetSimilarMoviesAsync() - should return a list of similar movies, ordered by a score generated by Neoflix’s recommendation algorithm.

In this challenge, you will update these methods to query Neo4j.

First, let’s take a look at how these methods are used.

Movie Page

If you click on any movie, you’ll see that the API currently only returns information a limited set of movies from the fixtures in popular.json.

The page itself is populated by three API calls:

  1. The details about the movie are loaded via the api/movies/{id} endpoint. This endpoint gets its data from the FindByIdAsync() method.

  2. The similar movies list is loaded by a call to the api/movies/{id}/similar endpoint, which gets its data from the GetSimilarMoviesAsync() method.

  3. The ratings on the right hand side of the page are loaded by a call to the api/movies/{id}/ratings endpoint. You will update this method in the next lesson.

FindByIdAsync()

Movie information is retrieved by the FindByIdAsync() method.

c#
Neoflix/Services/MovieService.cs
public async Task<Dictionary<string, object>> FindByIdAsync(string id, string userId = null)
{
    // TODO: Find a movie by its ID
    // MATCH (m:Movie {tmdbId: $id})

    return await Task.FromResult(Fixtures.Goodfellas);
}

Most of the important information is held as properties on the Movie node, but the payload should also return some additional information. We can use a Cypher subquery and the count() aggregate function to get the number of ratings for the movie and use a Pattern Comprehension to get lists of actors, directors and genres.

As with the other movie methods in the MovieService, we should also provide a favorite value to represent whether the user has added this movie to their My Favorites list or not.

The following Cypher statement should be run within a read transaction:

cypher
Movie Details
MATCH (m:Movie {tmdbId: $id})
RETURN m {
  .*,
  actors: [ (a)-[r:ACTED_IN]->(m) | a { .*, role: r.role } ],
  directors: [ (d)-[:DIRECTED]->(m) | d { .* } ],
  genres: [ (m)-[:IN_GENRE]->(g) | g { .name }],
  ratingCount: count{ (m)<-[:RATED]-() },
  favorite: m.tmdbId IN $favorites
} AS movie
LIMIT 1

Your Task

  • Modify the FindByIdAsync() method to query Neo4j and return details for the requested movie.

  • The returned object should include a list of actors, directors, genres, and a boolean flag to represent whether the movie exists on the current user’s My Favorites List.

  • If no records are returned, the method should throw a NotFoundError (via the single() method on Result).

  • Remember to close the session and use the ToListAsync() function to convert the result object into native C# types.

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

    var records = await session.ExecuteReadAsync(async tx =>
    {
        var favorites = await GetUserFavoritesAsync(tx, userId);

        var query = @"
            MATCH (m:Movie {tmdbId: $id})
            RETURN m {
                .*,actors: [ (a)-[r:ACTED_IN]->(m) | a { .*, role: r.role } ],
                    directors: [ (d)-[:DIRECTED]->(m) | d { .* } ],
                    genres: [ (m)-[:IN_GENRE]->(g) | g { .name }],
                    ratingCount: count { (m)<-[:RATED]-() },
              favorite: m.tmdbId IN $favorites
            } AS movie
            LIMIT 1";
        var cursor = await tx.RunAsync(query, new { favorites, id });
        return await cursor.ToListAsync();
    });

    if (records.Count == 0)
    {
        throw new NotFoundException($"Could not find a Movie with tmdbId {id}");
    }

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

GetSimilarMoviesAsync()

Similar movies are found using the GetSimilarMoviesAsync() method.

To provide a simple set of similar movies, the Cypher statement below uses the number of neighbors in common and their IMDB rating to generate a similarity score.

The following Cypher statement should be run within a read transaction:

cypher
Find Similar Movies
MATCH (:Movie {tmdbId: $id})-[:IN_GENRE|ACTED_IN|DIRECTED]->()<-[:IN_GENRE|ACTED_IN|DIRECTED]-(m)
WHERE m.imdbRating IS NOT NULL

WITH m, count(*) AS inCommon
WITH m, inCommon, m.imdbRating * inCommon AS score
ORDER BY score DESC

SKIP $skip
LIMIT $limit

RETURN m {
    .*,
    score: score,
    favorite: m.tmdbId IN $favorites
} AS movie

Your Task

  • Modify the GetSimilarMoviesAsync() method to query Neo4j and return a list of similar movies.

  • The returned objects should include a list of actors, directors, genres, and a boolean flag to represent whether the movie exists on the current user’s My Favorites List.

  • Remember to close the session and use the ToListAsync() function result to convert the values to native C# types.

Click here to reveal the completed GetSimilarMoviesAsync() method
c#
Neoflix/Services/MovieService.cs
public async Task<Dictionary<string, object>[]> GetSimilarMoviesAsync(string id, int limit, int skip, string userId = null)
{
    await using var session = _driver.AsyncSession();

    var records = await session.ExecuteReadAsync(async tx =>
    {
        var favorites = await GetUserFavoritesAsync(tx, userId);

        var query = @"
            MATCH (:Movie {tmdbId: $id})-[:IN_GENRE|ACTED_IN|DIRECTED]->()<-[:IN_GENRE|ACTED_IN|DIRECTED]-(m)
            WHERE m.imdbRating IS NOT NULL
            WITH m, count(*) AS inCommon
            WITH m, inCommon, m.imdbRating * inCommon AS score
            ORDER BY score DESC
            SKIP $skip
            LIMIT $limit
            RETURN m {
                .*,
                score: score,
                favorite: m.tmdbId IN $favorites
            } AS movie";
        var cursor = await tx.RunAsync(query, new { id, skip, limit, favorites });
        return await cursor.ToListAsync();
    });

    return records
        .Select(x => x["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._12_MovieDetails"

The test file is located at Neoflix.Challenges/12-MovieDetails.cs.

Are you stuck? Click here for help

If you get stuck, you can see a working solution by checking out the 12-movie-details branch by running:

sh
Check out the 12-movie-details branch
git checkout 12-movie-details

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

What is the title of the most similar movie to Lock, Stock and Two Smoking Barrels?

As part of the test suite, the final test will log the title of the most similar movie to Lock, Stock & Two Smoking Barrels (tmdbId: 100).

Paste the title of the movie into the box below without quotes or whitespace and click Check Answer.

  • ✓ Pulp Fiction

Hint

You can also find the answer by running the following Cypher statement:

cypher
MATCH (:Movie {tmdbId: '100'})-[:IN_GENRE|ACTED_IN|DIRECTED]->()<-[:IN_GENRE|ACTED_IN|DIRECTED]-(m)
WHERE m.imdbRating IS NOT NULL

WITH m, count(*) AS inCommon
WITH m, inCommon, m.imdbRating * inCommon AS score

RETURN m.title
ORDER BY score DESC
LIMIT 1

Copy the answer without any quotes or whitespace.

Lesson Summary

In this Challenge, you modified the FindByIdAsync() and GetSimilarMoviesAsync() methods to return movie details from the Neo4j database.

In the next Challenge, you will complete the Movie page implementation by retrieving movie ratings from Neo4j.