Skip to content

Commit

Permalink
Update driver version; use executeQuery API in repository (#28)
Browse files Browse the repository at this point in the history
* Update driver version; use executeQuery API in repository

* Fix version < 4

* Add wait-on for cypress tests
  • Loading branch information
RichardIrons-neo4j authored Feb 12, 2024
1 parent 4eaff8d commit ff79ebc
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 135 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,5 @@ jobs:
with:
working-directory: e2e
browser: chrome
start: dotnet run --no-launch-profile --project ..
start: dotnet run --no-launch-profile --project ..
wait-on: 'http://localhost:8080'
7 changes: 6 additions & 1 deletion Controllers/MoviesController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,18 @@ public MoviesController(IMovieRepository movieRepository)
[HttpGet]
public Task<Movie> GetMovieDetails([FromRoute] string title)
{
if (title == "favicon.ico")
return null;

title = System.Net.WebUtility.UrlDecode(title);
return _movieRepository.FindByTitle(title);
}

[Route("{title}/vote")]
[HttpPost]
public Task<int> VoteInMovie([FromRoute] string title)
{
title = System.Net.WebUtility.UrlDecode(title);
return _movieRepository.VoteByTitle(title);
}
}
}
8 changes: 6 additions & 2 deletions Model/Movie.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,9 @@

namespace MoviesDotNetCore.Model;

public record Movie(string Title, IEnumerable<Person> Cast = null, long? Released = null, string Tagline = null,
long? Votes = null);
public record Movie(
string Title,
IEnumerable<Person> Cast = null,
long? Released = null,
string Tagline = null,
long? Votes = null);
2 changes: 1 addition & 1 deletion MoviesDotNetCore.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Neo4j.Driver" Version="5.6.0"/>
<PackageReference Include="Neo4j.Driver" Version="5.17.0" />
</ItemGroup>

<ItemGroup>
Expand Down
24 changes: 1 addition & 23 deletions Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -1,33 +1,11 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:23224",
"sslPort": 44355
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"NEO4J_URI": "neo4j+s://demo.neo4jlabs.com",
"NEO4J_USER": "movies",
"NEO4J_PASSWORD": "movies",
"NEO4J_DATABASE": "movies",
"NEO4J_VERSION": "4",
"PORT": "8080"
}
},
"MoviesDotNetCore": {
"commandName": "Project",
"dotnetRunMessages": "true",
"launchBrowser": true,
"launchUrl": "",
"launchUrl": "http://localhost:8080",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"NEO4J_URI": "neo4j+s://demo.neo4jlabs.com",
Expand Down
203 changes: 96 additions & 107 deletions Repositories/MovieRepository.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices.ComTypes;
using System.Threading.Tasks;
using MoviesDotNetCore.Model;
using Neo4j.Driver;
Expand All @@ -18,140 +20,127 @@ public interface IMovieRepository
public class MovieRepository : IMovieRepository
{
private readonly IDriver _driver;
private readonly QueryConfig _queryConfig;

public MovieRepository(IDriver driver)
{
var versionStr = Environment.GetEnvironmentVariable("NEO4J_VERSION") ?? "";
if( double.TryParse(versionStr, out var version) && version >= 4.0)
{
_queryConfig = new QueryConfig(database: Environment.GetEnvironmentVariable("NEO4J_DATABASE") ?? "movies");
}
else
{
_queryConfig = new QueryConfig();
}

_driver = driver;
}

public async Task<Movie> FindByTitle(string title)
{
if (title == "favicon.ico")
return null;

await using var session = _driver.AsyncSession(WithDatabase);

return await session.ExecuteReadAsync(async transaction =>
{
var cursor = await transaction.RunAsync(@"
MATCH (movie:Movie {title:$title})
OPTIONAL MATCH (movie)<-[r]-(person:Person)
RETURN movie.title AS title,
collect({
name:person.name,
job: head(split(toLower(type(r)),'_')),
role: reduce(acc = '', role IN r.roles | acc + CASE WHEN acc='' THEN '' ELSE ', ' END + role)}
) AS cast",
new {title}
);

return await cursor.SingleAsync(record => new Movie(
record["title"].As<string>(),
MapCast(record["cast"].As<List<IDictionary<string, object>>>())
));
});
var (queryResults, _) = await _driver
.ExecutableQuery(@"
MATCH (movie:Movie {title:$title})
OPTIONAL MATCH (movie)<-[r]-(person:Person)
RETURN movie.title AS title,
collect({
name:person.name,
job: head(split(toLower(type(r)),'_')),
role: reduce(acc = '', role IN r.roles | acc + CASE WHEN acc='' THEN '' ELSE ', ' END + role)}
) AS cast")
.WithParameters(new { title })
.WithConfig(_queryConfig)
.ExecuteAsync();

return queryResults
.Select(
record => new Movie(
record["title"].As<string>(),
MapCast(record["cast"].As<List<IDictionary<string, object>>>())))
.Single();
}

public async Task<int> VoteByTitle(string title)
{
await using var session = _driver.AsyncSession(WithDatabase);
return await session.ExecuteWriteAsync(async transaction =>
{
var cursor = await transaction.RunAsync(@"
MATCH (m:Movie {title: $title})
SET m.votes = coalesce(m.votes, 0) + 1;",
new {title}
);

var summary = await cursor.ConsumeAsync();
return summary.Counters.PropertiesSet;
});
var (_, summary) = await _driver
.ExecutableQuery(@"
MATCH (m:Movie {title: $title})
SET m.votes = coalesce(m.votes, 0) + 1")
.WithParameters(new { title })
.WithConfig(_queryConfig)
.ExecuteAsync();

return summary.Counters.PropertiesSet;
}

public async Task<List<Movie>> Search(string search)
{
await using var session = _driver.AsyncSession(WithDatabase);
return await session.ExecuteReadAsync(async transaction =>
{
var cursor = await transaction.RunAsync(@"
MATCH (movie:Movie)
WHERE toLower(movie.title) CONTAINS toLower($title)
RETURN movie.title AS title,
movie.released AS released,
movie.tagline AS tagline,
movie.votes AS votes",
new {title = search}
);

return await cursor.ToListAsync(record => new Movie(
record["title"].As<string>(),
Tagline: record["tagline"].As<string>(),
Released: record["released"].As<long>(),
Votes: record["votes"]?.As<long>()
));
});
var (queryResults, _) = await _driver
.ExecutableQuery(@"
MATCH (movie:Movie)
WHERE toLower(movie.title) CONTAINS toLower($title)
RETURN movie.title AS title,
movie.released AS released,
movie.tagline AS tagline,
movie.votes AS votes")
.WithParameters(new { title = search })
.WithConfig(_queryConfig)
.ExecuteAsync();

return queryResults
.Select(
record => new Movie(
record["title"].As<string>(),
Tagline: record["tagline"].As<string>(),
Released: record["released"].As<long>(),
Votes: record["votes"]?.As<long>()))
.ToList();
}

public async Task<D3Graph> FetchD3Graph(int limit)
{
await using var session = _driver.AsyncSession(WithDatabase);
return await session.ExecuteReadAsync(async transaction =>
var (queryResults, _) = await _driver
.ExecutableQuery(@"
MATCH (m:Movie)<-[:ACTED_IN]-(p:Person)
WITH m, p
ORDER BY m.title, p.name
RETURN m.title AS title, collect(p.name) AS cast
LIMIT $limit")
.WithParameters(new { limit })
.WithConfig(_queryConfig)
.ExecuteAsync();

var nodes = new List<D3Node>();
var links = new List<D3Link>();

foreach (var record in queryResults)
{
var cursor = await transaction.RunAsync(@"
MATCH (m:Movie)<-[:ACTED_IN]-(p:Person)
WITH m, p
ORDER BY m.title, p.name
RETURN m.title AS title, collect(p.name) AS cast
LIMIT $limit",
new {limit}
);

var nodes = new List<D3Node>();
var links = new List<D3Link>();

// IAsyncEnumerable available from Version 5.5 of .NET Driver.
await foreach (var record in cursor)
var movie = new D3Node(record["title"].As<string>(), "movie");
var movieIndex = nodes.Count;
nodes.Add(movie);
foreach (var actorName in record["cast"].As<IList<string>>())
{
var movie = new D3Node(record["title"].As<string>(), "movie");
var movieIndex = nodes.Count;
nodes.Add(movie);
foreach (var actorName in record["cast"].As<IList<string>>())
{
var actor = new D3Node(actorName, "actor");
var actorIndex = nodes.IndexOf(actor);
actorIndex = actorIndex == -1 ? nodes.Count : actorIndex;
nodes.Add(actor);
links.Add(new D3Link(actorIndex, movieIndex));
}
var actor = new D3Node(actorName, "actor");
var actorIndex = nodes.IndexOf(actor);
actorIndex = actorIndex == -1 ? nodes.Count : actorIndex;
nodes.Add(actor);
links.Add(new D3Link(actorIndex, movieIndex));
}
}

return new D3Graph(nodes, links);
});
return new D3Graph(nodes, links);
}

private static IEnumerable<Person> MapCast(IEnumerable<IDictionary<string, object>> persons)
{
return persons
.Select(dictionary =>
new Person(
dictionary["name"].As<string>(),
dictionary["job"].As<string>(),
dictionary["role"].As<string>()
)
).ToList();
.Select(
dictionary =>
new Person(
dictionary["name"].As<string>(),
dictionary["job"].As<string>(),
dictionary["role"].As<string>()))
.ToList();
}

private static void WithDatabase(SessionConfigBuilder sessionConfigBuilder)
{
var neo4jVersion = Environment.GetEnvironmentVariable("NEO4J_VERSION") ?? "";
if (!neo4jVersion.StartsWith("4"))
return;

sessionConfigBuilder.WithDatabase(Database());
}

private static string Database()
{
return Environment.GetEnvironmentVariable("NEO4J_DATABASE") ?? "movies";
}
}
}

0 comments on commit ff79ebc

Please sign in to comment.