diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 9665adb..818c4af 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -75,4 +75,5 @@ jobs: with: working-directory: e2e browser: chrome - start: dotnet run --no-launch-profile --project .. \ No newline at end of file + start: dotnet run --no-launch-profile --project .. + wait-on: 'http://localhost:8080' diff --git a/Controllers/MoviesController.cs b/Controllers/MoviesController.cs index 46b8e89..97126c9 100644 --- a/Controllers/MoviesController.cs +++ b/Controllers/MoviesController.cs @@ -20,6 +20,10 @@ public MoviesController(IMovieRepository movieRepository) [HttpGet] public Task GetMovieDetails([FromRoute] string title) { + if (title == "favicon.ico") + return null; + + title = System.Net.WebUtility.UrlDecode(title); return _movieRepository.FindByTitle(title); } @@ -27,6 +31,7 @@ public Task GetMovieDetails([FromRoute] string title) [HttpPost] public Task VoteInMovie([FromRoute] string title) { + title = System.Net.WebUtility.UrlDecode(title); return _movieRepository.VoteByTitle(title); } -} \ No newline at end of file +} diff --git a/Model/Movie.cs b/Model/Movie.cs index a0f175d..a2b1879 100644 --- a/Model/Movie.cs +++ b/Model/Movie.cs @@ -2,5 +2,9 @@ namespace MoviesDotNetCore.Model; -public record Movie(string Title, IEnumerable Cast = null, long? Released = null, string Tagline = null, - long? Votes = null); \ No newline at end of file +public record Movie( + string Title, + IEnumerable Cast = null, + long? Released = null, + string Tagline = null, + long? Votes = null); diff --git a/MoviesDotNetCore.csproj b/MoviesDotNetCore.csproj index d9be188..4cf8649 100644 --- a/MoviesDotNetCore.csproj +++ b/MoviesDotNetCore.csproj @@ -5,7 +5,7 @@ - + diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json index d639256..8a5551d 100644 --- a/Properties/launchSettings.json +++ b/Properties/launchSettings.json @@ -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", diff --git a/Repositories/MovieRepository.cs b/Repositories/MovieRepository.cs index 41da181..3f26bdb 100644 --- a/Repositories/MovieRepository.cs +++ b/Repositories/MovieRepository.cs @@ -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; @@ -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 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(), - MapCast(record["cast"].As>>()) - )); - }); + 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(), + MapCast(record["cast"].As>>()))) + .Single(); } public async Task 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> 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(), - Tagline: record["tagline"].As(), - Released: record["released"].As(), - Votes: record["votes"]?.As() - )); - }); + 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(), + Tagline: record["tagline"].As(), + Released: record["released"].As(), + Votes: record["votes"]?.As())) + .ToList(); } public async Task 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(); + var links = new List(); + + 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(); - var links = new List(); - - // IAsyncEnumerable available from Version 5.5 of .NET Driver. - await foreach (var record in cursor) + var movie = new D3Node(record["title"].As(), "movie"); + var movieIndex = nodes.Count; + nodes.Add(movie); + foreach (var actorName in record["cast"].As>()) { - var movie = new D3Node(record["title"].As(), "movie"); - var movieIndex = nodes.Count; - nodes.Add(movie); - foreach (var actorName in record["cast"].As>()) - { - 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 MapCast(IEnumerable> persons) { return persons - .Select(dictionary => - new Person( - dictionary["name"].As(), - dictionary["job"].As(), - dictionary["role"].As() - ) - ).ToList(); + .Select( + dictionary => + new Person( + dictionary["name"].As(), + dictionary["job"].As(), + dictionary["role"].As())) + .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"; - } -} \ No newline at end of file +}