diff --git a/src/Core/Context/StaticSearch.cs b/src/Core/Context/StaticSearch.cs index 51d033a..ff8c3c3 100644 --- a/src/Core/Context/StaticSearch.cs +++ b/src/Core/Context/StaticSearch.cs @@ -156,45 +156,52 @@ public SearchResults Search(AuthIdentity identity, RecipeQuery query) case RecipeQuery.SortOrder.Title: q = (query.Direction == RecipeQuery.SortDirection.Ascending) - ? q.OrderBy(p => p.Recipe.Title) - : q.OrderByDescending(p => p.Recipe.Title); + ? q.OrderBy(p => p.Recipe.Title).ThenBy(p => p.Recipe.RecipeId) + : q.OrderByDescending(p => p.Recipe.Title).ThenBy(p => p.Recipe.RecipeId); break; case RecipeQuery.SortOrder.PrepTime: q = (query.Direction == RecipeQuery.SortDirection.Ascending) - ? q.OrderBy(p => p.Recipe.PrepTime) - : q.OrderByDescending(p => p.Recipe.PrepTime); + ? q.OrderBy(p => p.Recipe.PrepTime).ThenBy(p => p.Recipe.RecipeId) + : q.OrderByDescending(p => p.Recipe.PrepTime).ThenBy(p => p.Recipe.RecipeId); break; case RecipeQuery.SortOrder.CookTime: q = (query.Direction == RecipeQuery.SortDirection.Ascending) - ? q.OrderBy(p => p.Recipe.CookTime) - : q.OrderByDescending(p => p.Recipe.CookTime); + ? q.OrderBy(p => p.Recipe.CookTime).ThenBy(p => p.Recipe.RecipeId) + : q.OrderByDescending(p => p.Recipe.CookTime).ThenBy(p => p.Recipe.RecipeId); break; case RecipeQuery.SortOrder.TotalTime: q = (query.Direction == RecipeQuery.SortDirection.Ascending) ? q.OrderBy(p => p.Recipe.PrepTime + p.Recipe.CookTime) - : q.OrderByDescending(p => p.Recipe.PrepTime + p.Recipe.CookTime); + .ThenBy(p => p.Recipe.RecipeId) + : q.OrderByDescending(p => p.Recipe.PrepTime + p.Recipe.CookTime) + .ThenBy(p => p.Recipe.RecipeId); break; case RecipeQuery.SortOrder.Image: q = (query.Direction == RecipeQuery.SortDirection.Ascending) - ? q.OrderBy(p => p.Recipe.ImageUrl) - : q.OrderByDescending(p => p.Recipe.ImageUrl); + ? q.OrderBy(p => p.Recipe.ImageUrl).ThenBy(p => p.Recipe.RecipeId) + : q.OrderByDescending(p => p.Recipe.ImageUrl).ThenBy(p => p.Recipe.RecipeId); break; default: q = (query.Direction == RecipeQuery.SortDirection.Ascending) - ? q.OrderBy(p => p.Recipe.Rating) - : q.OrderByDescending(p => p.Recipe.Rating); + ? q.OrderBy(p => p.Recipe.Rating).ThenBy(p => p.Recipe.RecipeId) + : q.OrderByDescending(p => p.Recipe.Rating).ThenBy(p => p.Recipe.RecipeId); break; } + var totalCount = q.LongCount(); + return new SearchResults { - Briefs = q.Select(r => Provisioning.DTO.Recipes.ToRecipeBrief(r.Recipe)).ToArray(), - TotalCount = q.Count(), + Briefs = q.Skip(query.Offset) + .Take(RecipeQuery.PageSize) + .Select(r => Provisioning.DTO.Recipes.ToRecipeBrief(r.Recipe)) + .ToArray(), + TotalCount = totalCount, }; } diff --git a/src/Core/Properties/AssemblyInfo.cs b/src/Core/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..89dcf4e --- /dev/null +++ b/src/Core/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("UnitTests")] diff --git a/src/Core/Recipes/RecipeQuery.cs b/src/Core/Recipes/RecipeQuery.cs index 71d0496..e55a759 100644 --- a/src/Core/Recipes/RecipeQuery.cs +++ b/src/Core/Recipes/RecipeQuery.cs @@ -4,6 +4,8 @@ namespace KitchenPC.Core.Recipes; public class RecipeQuery { + public const int PageSize = 100; + public enum PhotoFilter { All = 0, diff --git a/src/DB/NHSearch.cs b/src/DB/NHSearch.cs index 781bc56..a82ecfc 100644 --- a/src/DB/NHSearch.cs +++ b/src/DB/NHSearch.cs @@ -224,19 +224,20 @@ public async Task SearchAsync( RecipeQuery.SortOrder.Image => q.OrderBy(p => p.ImageUrl), _ => q.OrderBy(p => p.Rating), }; - var results = ( + var totalCount = await q.ToRowCountInt64Query().RowCountInt64Async(cancellationToken); + var results = await ( query.Direction == RecipeQuery.SortDirection.Descending ? orderBy.Desc() : orderBy.Asc() ) + .ThenBy(p => p.RecipeId) + .Asc() .Skip(query.Offset) - .Take(100) + .Take(RecipeQuery.PageSize) .ListAsync(cancellationToken); - var loadedResults = await results; - return new SearchResults { - Briefs = loadedResults.Select(r => r.AsRecipeBrief()).ToArray(), - TotalCount = loadedResults.Count, // TODO: This needs to be the total matches, not the returned matches + Briefs = results.Select(r => r.AsRecipeBrief()).ToArray(), + TotalCount = totalCount, }; } } diff --git a/src/UnitTests/RecipeSearch.cs b/src/UnitTests/RecipeSearch.cs new file mode 100644 index 0000000..83b22ad --- /dev/null +++ b/src/UnitTests/RecipeSearch.cs @@ -0,0 +1,69 @@ +using System; +using System.Linq; +using KitchenPC.Core; +using KitchenPC.Core.Context; +using KitchenPC.Core.Provisioning; +using KitchenPC.Core.Recipes; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using RecipeDto = KitchenPC.Core.Provisioning.DTO.Recipes; + +namespace KitchenPC.UnitTests; + +[TestClass] +public class RecipeSearchTest +{ + [TestMethod] + public void StaticSearchReturnsStablePagesAndTotalCount() + { + var recipes = Enumerable + .Range(0, 205) + .Select(index => new RecipeDto + { + RecipeId = new Guid(index + 1, 0, 0, new byte[8]), + Title = $"Recipe {index:D3}", + }) + .ToList(); + var store = new DataStore + { + Recipes = recipes, + RecipeIngredients = [], + RecipeMetadata = [], + }; + var search = new StaticSearch(store); + + var firstPage = search.Search(AuthIdentity.Anonymous, CreateQuery(0)); + var secondPage = search.Search(AuthIdentity.Anonymous, CreateQuery(100)); + var finalPage = search.Search(AuthIdentity.Anonymous, CreateQuery(200)); + var beyondEnd = search.Search(AuthIdentity.Anonymous, CreateQuery(300)); + + Assert.AreEqual(205, firstPage.TotalCount); + Assert.AreEqual(RecipeQuery.PageSize, firstPage.Briefs.Length); + Assert.AreEqual(205, secondPage.TotalCount); + Assert.AreEqual(RecipeQuery.PageSize, secondPage.Briefs.Length); + Assert.AreEqual(205, finalPage.TotalCount); + Assert.AreEqual(5, finalPage.Briefs.Length); + Assert.AreEqual(205, beyondEnd.TotalCount); + Assert.AreEqual(0, beyondEnd.Briefs.Length); + + CollectionAssert.AreEqual( + recipes.Take(RecipeQuery.PageSize).Select(recipe => recipe.RecipeId).ToArray(), + firstPage.Briefs.Select(recipe => recipe.Id).ToArray() + ); + CollectionAssert.AreEqual( + recipes + .Skip(RecipeQuery.PageSize) + .Take(RecipeQuery.PageSize) + .Select(recipe => recipe.RecipeId) + .ToArray(), + secondPage.Briefs.Select(recipe => recipe.Id).ToArray() + ); + } + + private static RecipeQuery CreateQuery(int offset) => + new() + { + Offset = offset, + Sort = RecipeQuery.SortOrder.Title, + Direction = RecipeQuery.SortDirection.Ascending, + }; +}