diff --git a/docker/api/Dockerfile b/docker/api/Dockerfile index 0966fa8..67582d1 100644 --- a/docker/api/Dockerfile +++ b/docker/api/Dockerfile @@ -1,4 +1,4 @@ FROM mcr.microsoft.com/dotnet/core/aspnet:latest -COPY musiccatalogue.api-1.17.0.0 /opt/musiccatalogue.api-1.17.0.0 -WORKDIR /opt/musiccatalogue.api-1.17.0.0/bin +COPY musiccatalogue.api-1.18.0.0 /opt/musiccatalogue.api-1.18.0.0 +WORKDIR /opt/musiccatalogue.api-1.18.0.0/bin ENTRYPOINT [ "./MusicCatalogue.Api" ] diff --git a/docker/ui/Dockerfile b/docker/ui/Dockerfile index aca9622..1fcf5c6 100644 --- a/docker/ui/Dockerfile +++ b/docker/ui/Dockerfile @@ -1,6 +1,6 @@ FROM node:20-alpine -COPY musiccatalogue.ui-1.17.0.0 /opt/musiccatalogue.ui-1.17.0.0 -WORKDIR /opt/musiccatalogue.ui-1.17.0.0 +COPY musiccatalogue.ui-1.18.0.0 /opt/musiccatalogue.ui-1.18.0.0 +WORKDIR /opt/musiccatalogue.ui-1.18.0.0 RUN npm install RUN npm run build ENTRYPOINT [ "npm", "start" ] diff --git a/src/MusicCatalogue.Api/Controllers/GenresController.cs b/src/MusicCatalogue.Api/Controllers/GenresController.cs index 8aac318..a3240af 100644 --- a/src/MusicCatalogue.Api/Controllers/GenresController.cs +++ b/src/MusicCatalogue.Api/Controllers/GenresController.cs @@ -3,6 +3,7 @@ using MusicCatalogue.Entities.Database; using MusicCatalogue.Entities.Exceptions; using MusicCatalogue.Entities.Interfaces; +using MusicCatalogue.Entities.Search; namespace MusicCatalogue.Api.Controllers { @@ -20,18 +21,22 @@ public GenresController(IMusicCatalogueFactory factory) } /// - /// Return a list of all the genres in the catalogue + /// Return a list of genres matching the specified criteria /// /// - [HttpGet] - [Route("")] - public async Task>> GetGenresAsync() + [HttpPost] + [Route("search")] + public async Task>> GetGenresAsync(GenreSearchCriteria criteria) { - // Get a list of all artists in the catalogue - List genres = await _factory.Genres.ListAsync(x => true); + // Ideally, this method would use the GET verb but as more filtering criteria are added that leads + // to an increasing number of query string parameters and a very messy URL. So the filter criteria + // are POSTed in the request body, instead, and bound into a strongly typed criteria object + + // Retrieve a list of matching genres + var genres = await _factory.Search.GenreSearchAsync(criteria); // If there are no genres, return a no content response - if (!genres.Any()) + if (genres == null) { return NoContent(); } diff --git a/src/MusicCatalogue.Api/MusicCatalogue.Api.csproj b/src/MusicCatalogue.Api/MusicCatalogue.Api.csproj index c88a9f5..e63ac14 100644 --- a/src/MusicCatalogue.Api/MusicCatalogue.Api.csproj +++ b/src/MusicCatalogue.Api/MusicCatalogue.Api.csproj @@ -2,9 +2,9 @@ net7.0 - 1.17.0.0 - 1.17.0.0 - 1.17.0 + 1.18.0.0 + 1.18.0.0 + 1.18.0 enable enable diff --git a/src/MusicCatalogue.Data/MusicCatalogue.Data.csproj b/src/MusicCatalogue.Data/MusicCatalogue.Data.csproj index b428021..bc01c2f 100644 --- a/src/MusicCatalogue.Data/MusicCatalogue.Data.csproj +++ b/src/MusicCatalogue.Data/MusicCatalogue.Data.csproj @@ -5,7 +5,7 @@ enable enable MusicCatalogue.Data - 1.16.0.0 + 1.17.0.0 Dave Walker Copyright (c) Dave Walker 2023 Dave Walker @@ -17,7 +17,7 @@ https://github.com/davewalker5/MusicCatalogue MIT false - 1.16.0.0 + 1.17.0.0 diff --git a/src/MusicCatalogue.Entities/Interfaces/ISearchManager.cs b/src/MusicCatalogue.Entities/Interfaces/ISearchManager.cs index cf5a709..8e1d7ea 100644 --- a/src/MusicCatalogue.Entities/Interfaces/ISearchManager.cs +++ b/src/MusicCatalogue.Entities/Interfaces/ISearchManager.cs @@ -7,5 +7,6 @@ public interface ISearchManager { Task?> AlbumSearchAsync(AlbumSearchCriteria criteria); Task?> ArtistSearchAsync(ArtistSearchCriteria criteria); + Task?> GenreSearchAsync(GenreSearchCriteria criteria); } } \ No newline at end of file diff --git a/src/MusicCatalogue.Entities/MusicCatalogue.Entities.csproj b/src/MusicCatalogue.Entities/MusicCatalogue.Entities.csproj index 927891c..2e91ad9 100644 --- a/src/MusicCatalogue.Entities/MusicCatalogue.Entities.csproj +++ b/src/MusicCatalogue.Entities/MusicCatalogue.Entities.csproj @@ -5,7 +5,7 @@ enable enable MusicCatalogue.Entities - 1.16.0.0 + 1.17.0.0 Dave Walker Copyright (c) Dave Walker 2023 Dave Walker @@ -17,7 +17,7 @@ https://github.com/davewalker5/MusicCatalogue MIT false - 1.16.0.0 + 1.17.0.0 diff --git a/src/MusicCatalogue.Entities/Search/GenreSearchCriteria.cs b/src/MusicCatalogue.Entities/Search/GenreSearchCriteria.cs new file mode 100644 index 0000000..d61bd02 --- /dev/null +++ b/src/MusicCatalogue.Entities/Search/GenreSearchCriteria.cs @@ -0,0 +1,10 @@ +using System.Diagnostics.CodeAnalysis; + +namespace MusicCatalogue.Entities.Search +{ + [ExcludeFromCodeCoverage] + public class GenreSearchCriteria + { + public bool? WishList { get; set; } + } +} diff --git a/src/MusicCatalogue.Logic/Database/SearchManager.cs b/src/MusicCatalogue.Logic/Database/SearchManager.cs index 520c501..996d9a9 100644 --- a/src/MusicCatalogue.Logic/Database/SearchManager.cs +++ b/src/MusicCatalogue.Logic/Database/SearchManager.cs @@ -1,7 +1,6 @@ using MusicCatalogue.Entities.Database; using MusicCatalogue.Entities.Interfaces; using MusicCatalogue.Entities.Search; -using System.Runtime.InteropServices; namespace MusicCatalogue.Logic.Database { @@ -30,29 +29,29 @@ internal SearchManager(IMusicCatalogueFactory factory) : base(factory) // as "return artists who have produced an album in the Jazz genre". So, start by retrieving a // list of albums matching the criteria then derive the artists from that var albums = await Factory.Albums - .ListAsync(x => ( + .ListAsync(x => ( (criteria.WishList == null) || ((criteria.WishList == false) && (x.IsWishListItem == null)) || (x.IsWishListItem == criteria.WishList) - ) && - ( + ) && + ( (criteria.GenreId == null) || (x.GenreId == criteria.GenreId) - )); + )); // If there are no albums, there can't be any matching artists if (albums.Any()) { - // Compile a list of artist IDs and load the matching artists - var artistIds = albums.Select(x => x.ArtistId).ToList(); + // Compile a list of unique artist IDs and load the matching artists + var artistIds = albums.Select(x => x.ArtistId).Distinct().ToList(); artists = await Factory.Artists - .ListAsync(x => artistIds.Contains(x.Id) && - ( + .ListAsync(x => artistIds.Contains(x.Id) && + ( (prefix == null) || ((x.SearchableName != null) && x.SearchableName.StartsWith(prefix)) || ((x.SearchableName == null) && x.Name.StartsWith(prefix)) - ), - false); + ), + false); // Now map the albums onto their associated artists foreach (var artist in artists) @@ -90,5 +89,32 @@ internal SearchManager(IMusicCatalogueFactory factory) : base(factory) return albums.Any() ? albums : null; } + /// + /// Return the genres matching the specified criteria + /// + /// + /// + public async Task?> GenreSearchAsync(GenreSearchCriteria criteria) + { + List? genres = null; + + // Retrieve a list of albums, as they're the entity that's tagged with a genre + var albums = await Factory.Albums + .ListAsync(x => ( + (criteria.WishList == null) || + ((criteria.WishList == false) && (x.IsWishListItem == null)) || + (x.IsWishListItem == criteria.WishList) + )); + + // If there are no albums, there can't be any matching artists + if (albums.Any()) + { + // Get a list of unique genre IDs and load the matching genres + var genreIds = albums.Select(x => x.GenreId).Distinct().ToList(); + genres = await Factory.Genres.ListAsync(x => genreIds.Contains(x.Id)); + } + + return genres; + } } } diff --git a/src/MusicCatalogue.Logic/MusicCatalogue.Logic.csproj b/src/MusicCatalogue.Logic/MusicCatalogue.Logic.csproj index bdfa29a..fe415bd 100644 --- a/src/MusicCatalogue.Logic/MusicCatalogue.Logic.csproj +++ b/src/MusicCatalogue.Logic/MusicCatalogue.Logic.csproj @@ -5,7 +5,7 @@ enable enable MusicCatalogue.Logic - 1.16.0.0 + 1.17.0.0 Dave Walker Copyright (c) Dave Walker 2023 Dave Walker @@ -17,7 +17,7 @@ https://github.com/davewalker5/MusicCatalogue MIT false - 1.16.0.0 + 1.17.0.0 diff --git a/src/MusicCatalogue.LookupTool/MusicCatalogue.LookupTool.csproj b/src/MusicCatalogue.LookupTool/MusicCatalogue.LookupTool.csproj index dd4ae0d..aa44384 100644 --- a/src/MusicCatalogue.LookupTool/MusicCatalogue.LookupTool.csproj +++ b/src/MusicCatalogue.LookupTool/MusicCatalogue.LookupTool.csproj @@ -3,9 +3,9 @@ Exe net7.0 - 1.16.0.0 - 1.16.0.0 - 1.16.0 + 1.17.0.0 + 1.17.0.0 + 1.17.0 enable enable false diff --git a/src/MusicCatalogue.Tests/AlbumSearchManagerTest.cs b/src/MusicCatalogue.Tests/AlbumSearchManagerTest.cs index a3ed1fa..119d756 100644 --- a/src/MusicCatalogue.Tests/AlbumSearchManagerTest.cs +++ b/src/MusicCatalogue.Tests/AlbumSearchManagerTest.cs @@ -29,7 +29,7 @@ public void TestInitialize() // Add the artists _jazzArtistId = Task.Run(() => _factory.Artists.AddAsync("Diana Krall")).Result.Id; - _popGenreId = Task.Run(() => _factory.Artists.AddAsync("Katie Melua")).Result.Id; + _popArtistId = Task.Run(() => _factory.Artists.AddAsync("Katie Melua")).Result.Id; // Add the albums, one on the wishlist and one not Task.Run(() => _factory.Albums.AddAsync(_jazzArtistId, _jazzGenreId, JazzAlbumTitle, 2002, null, false, null, null, null)).Wait(); diff --git a/src/MusicCatalogue.Tests/GenreSearchManagerTest.cs b/src/MusicCatalogue.Tests/GenreSearchManagerTest.cs new file mode 100644 index 0000000..1a48d35 --- /dev/null +++ b/src/MusicCatalogue.Tests/GenreSearchManagerTest.cs @@ -0,0 +1,82 @@ +using MusicCatalogue.Data; +using MusicCatalogue.Entities.Interfaces; +using MusicCatalogue.Entities.Search; +using MusicCatalogue.Logic.Factory; + +namespace MusicCatalogue.Tests +{ + [TestClass] + public class GenreSearchManagerTest + { + private const string JazzGenre = "Jazz"; + private const string PopGenre = "Pop"; + + private IMusicCatalogueFactory? _factory; + private int _jazzGenreId; + private int _popGenreId; + private int _jazzArtistId; + private int _popArtistId; + + [TestInitialize] + public void TestInitialize() + { + MusicCatalogueDbContext context = MusicCatalogueDbContextFactory.CreateInMemoryDbContext(); + _factory = new MusicCatalogueFactory(context); + + // Add the genres + _jazzGenreId = Task.Run(() => _factory.Genres.AddAsync("Jazz")).Result.Id; + _popGenreId = Task.Run(() => _factory.Genres.AddAsync("Pop")).Result.Id; + + // Add the artists + _jazzArtistId = Task.Run(() => _factory.Artists.AddAsync("Diana Krall")).Result.Id; + _popArtistId = Task.Run(() => _factory.Artists.AddAsync("Katie Melua")).Result.Id; + } + + [TestMethod] + public async Task SearchForAllGenresTest() + { + // Add the albums, one on the wishlist and one not + Task.Run(() => _factory!.Albums.AddAsync(_jazzArtistId, _jazzGenreId, "Live In Paris", 2002, null, false, null, null, null)).Wait(); + Task.Run(() => _factory!.Albums.AddAsync(_popArtistId, _popGenreId, "Album No. 8", 2020, null, true, null, null, null)).Wait(); + + var genres = await _factory!.Search.GenreSearchAsync(new GenreSearchCriteria()); + Assert.IsNotNull(genres); + Assert.AreEqual(2, genres.Count); + } + + [TestMethod] + public async Task SearchMainCatalogueGenresTest() + { + // Add the albums, one on the wishlist and one not + Task.Run(() => _factory!.Albums.AddAsync(_jazzArtistId, _jazzGenreId, "Live In Paris", 2002, null, false, null, null, null)).Wait(); + Task.Run(() => _factory!.Albums.AddAsync(_popArtistId, _popGenreId, "Album No. 8", 2020, null, true, null, null, null)).Wait(); + + var criteria = new GenreSearchCriteria { WishList = false }; + var genres = await _factory!.Search.GenreSearchAsync(criteria); + Assert.IsNotNull(genres); + Assert.AreEqual(1, genres.Count); + Assert.AreEqual(JazzGenre, genres[0].Name); + } + + [TestMethod] + public async Task SearchWishlistGenresTest() + { + // Add the albums, one on the wishlist and one not + Task.Run(() => _factory!.Albums.AddAsync(_jazzArtistId, _jazzGenreId, "Live In Paris", 2002, null, false, null, null, null)).Wait(); + Task.Run(() => _factory!.Albums.AddAsync(_popArtistId, _popGenreId, "Album No. 8", 2020, null, true, null, null, null)).Wait(); + + var criteria = new GenreSearchCriteria { WishList = true }; + var genres = await _factory!.Search.GenreSearchAsync(criteria); + Assert.IsNotNull(genres); + Assert.AreEqual(1, genres.Count); + Assert.AreEqual(PopGenre, genres[0].Name); + } + + [TestMethod] + public async Task NoMatchesTest() + { + var genres = await _factory!.Search.GenreSearchAsync(new GenreSearchCriteria()); + Assert.IsNull(genres); + } + } +}