Now for another challenge.
In this challenge, you will use the knowledge gained so far in this course to add new functionality to the API.
You will modify the AllAsync()
method of the MovieService
to do the following:
Once you have completed the challenge, you will be asked to run a unit test to verify that the code has been correctly implemented. If the test runs correctly, the title of the highest rated movie will be logged.
You will need this value to verify that the test has run correctly.
Exploring the Code
Before you start, let’s take a look at the code.
If you are not interested in the code, you can skip straight to Implementing Read Transactions.
If you start the application and access the app at http://localhost:3000, you will see two lists on the home page; one for Popular Movies and one for Latest Releases.
Both of these lists are populated by a request to http://localhost:3000/api/movies
with some additional request query parameters, feel free to open that URL in your browser, you should see the JSON response.
Route Handler
You can find the route handler, the function that handles the request, in Neoflix/Controllers/MoviesController.cs
:
[HttpGet]
public async Task<IActionResult> ListAsync()
{
var (_, sort, order, limit, skip) = HttpRequestUtils
.GetPagination(Request.Query, SortType.Movies);
var userId = HttpRequestUtils.GetUserId(Request);
var driver = Neo4j.Driver;
var movieService = new MovieService(driver);
var movies = await movieService.AllAsync(sort, order, limit, skip, userId);
return Ok(movies);
}
Within the route handler, you can see that:
-
The
sort
,order
,limit
andskip
values are extracted from the request. This allows us to apply sorting and pagination to the results. -
The
userId
is also extracted from the request. -
A new instance of the
MovieService
is created with the Driver instance that you created in Adding the Driver passed to the constructor. -
The query results are retrieved via the
AllAsync()
method and are returned by the handler as JSON.
The Movie Service
The magic happens in the MovieService
, located at Neoflix/Services/MovieService.cs
.
For this route, we are concerned with the AllAsync()
method.
If we take a closer look at the AllAsync()
method, we can see that it currently returns a hardcoded list of popular
movies from the popular.json
fixture.
public async Task<Dictionary<string, object>[]> AllAsync(string sort = "title",
Ordering order = Ordering.Asc, int limit = 6, int skip = 0, string userId = null)
{
// TODO: Open an Session
// TODO: Execute a query in a new Read Transaction
// TODO: Get a list of Movies from the Result
// TODO: Close the session
return await Task.FromResult(Fixtures.Popular.Skip(skip).Take(limit).ToArray());
}
You will need to replace these TODO
comments with working code to complete the challenge.
Implementing Read Transactions
As you learned in the Sessions and Transactions lesson, you will complete code to open a new session and run the query within a Read Transaction.
Then, finally you add code to extract and return the results.
Open a new Session
Within the AllAsync()
method, first open a new session:
// Open a new session
using var session = _driver.AsyncSession();
// Do something with the session...
Execute a Cypher statement within a new Read Transaction
The Cypher statement itself is similar to this one, only that the property that’s ordered by will be dynamically inserted based on user interaction from the API query parameters.
// ordering will be dynamic in implementation
MATCH (m:Movie)
WHERE m.title IS NOT NULL
RETURN m {
.*
} AS movie
ORDER BY m.title ASC
SKIP $skip
LIMIT $limit
This session then provides an ExecuteReadAsync()
method for which you pass a callback/lambda to represent the unit of work.
The function will have one argument passed to it, a Transaction
instance that you can use to execute a Cypher statement using the run()
method.
The run()
method accepts two arguments:
-
The Cypher statement as a string, using query parameters (placeholders prefixed with
$
). -
An object containing the names and values of the parameters.
var cursor = await tx.RunAsync(@$"
MATCH (m:Movie)
WHERE m.{sort} IS NOT NULL
RETURN m {{ .* }} AS movie
ORDER BY m.{sort} {order.ToString("G").ToUpper()}
SKIP $skip
LIMIT $limit", new {skip, limit});
%s
instead, which we would love to avoid.Extract a list of Movies from the Result
Now that you have an IResultCursor
object assigned to the cursor
variable:
-
use the
ToListAsync()
function to consume each result, -
grab each
movie
node from eachIRecord
using.Select
-
convert the values to an array using
ToArray()
.
var records = await cursor.ToListAsync();
var movies = records
.Select(x => x["movie"].As<Dictionary<string, object>>())
.ToArray();
Return the Results
Finally, update the return
statement to return the movies
list extracted above.
return movies;
Working Solution
Click here to reveal the completed AllAsync()
method
public async Task<Dictionary<string, object>[]> AllAsync(string sort = "title",
Ordering order = Ordering.Asc, int limit = 6, int skip = 0, string userId = null)
{
// Open a new session.
await using var session = _driver.AsyncSession();
// Execute a query in a new Read Transaction.
return await session.ExecuteReadAsync(async tx =>
{
var cursor = await tx.RunAsync(@$"
MATCH (m:Movie)
WHERE m.{sort} IS NOT NULL
RETURN m {{ .* }} AS movie
ORDER BY m.{sort} {order.ToString("G").ToUpper()}
SKIP $skip
LIMIT $limit", new {skip, limit});
var records = await cursor.ToListAsync();
var movies = records
.Select(x => x["movie"].As<Dictionary<string, object>>())
.ToArray();
return movies;
});
}
Testing
To test that this functionality has been correctly implemented, run the following code in a new terminal session:
dotnet test --logger "console;verbosity=detailed" --filter "Neoflix.Challenges._02_MovieList"
The test file is located at Neoflix.Challenges/02-MovieList.cs
.
Are you stuck? Click here for help
If you get stuck, you can see a working solution by checking out the 02-movie-lists
branch by running:
git checkout 02-movie-lists
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
Highest Rated Movie
The final test in the suite logs out the name of the highest rated movie according to the imdbRating
property on each movie.
Enter the title of the highest rated movie.
-
✓ Band of Brothers
Hint
You can also find the answer by running the following Cypher statement:
MATCH (m:Movie)
WHERE m.imdbRating IS NOT NULL
RETURN m.title
ORDER BY m.imdbRating DESC LIMIT 1
Copy the answer without any quotes or whitespace.
Solution
The answer is Band of Brothers.
Lesson Summary
In this Challenge, you used your knowledge of sessions and transactions to retrieve a list of Movie nodes from the database.
If you now open the API endpoint or the application UI for popular movies you should see the list coming from your database. Congratulations 🎉.
In the next Challenge about user management, you will learn to write data to the database.