From 66d03513ebf0b687f72cd16a46cfb14a3f71bc6d Mon Sep 17 00:00:00 2001 From: Leon Hoppe Date: Sat, 5 Jul 2025 15:11:02 +0200 Subject: [PATCH] Finished advanced search functionality --- .../.idea/projectSettingsUpdater.xml | 1 + .idea/.idea.HopFrame/.idea/workspace.xml | 116 +++++++++----- .../ServiceCollectionExtensions.cs | 1 + .../Services/ISearchExpressionBuilder.cs | 9 ++ .../Implementations/ContextExplorer.cs | 8 +- .../Implementations/RepositoryTableManager.cs | 4 +- .../SearchExpressionBuilder.cs | 151 ++++++++++++++++++ .../Services/Implementations/TableManager.cs | 70 +------- .../Components/Pages/HopFrameTablePage.razor | 7 +- .../SearchSuggestionProvider.cs | 12 +- .../Services/ContextExplorerTests.cs | 3 + .../Services/DisplayPropertyTests.cs | 2 +- .../Services/TableManagerTests.cs | 39 +---- 13 files changed, 272 insertions(+), 151 deletions(-) create mode 100644 src/HopFrame.Core/Services/ISearchExpressionBuilder.cs create mode 100644 src/HopFrame.Core/Services/Implementations/SearchExpressionBuilder.cs diff --git a/.idea/.idea.HopFrame/.idea/projectSettingsUpdater.xml b/.idea/.idea.HopFrame/.idea/projectSettingsUpdater.xml index 64af657..ef20cb0 100644 --- a/.idea/.idea.HopFrame/.idea/projectSettingsUpdater.xml +++ b/.idea/.idea.HopFrame/.idea/projectSettingsUpdater.xml @@ -2,6 +2,7 @@ \ No newline at end of file diff --git a/.idea/.idea.HopFrame/.idea/workspace.xml b/.idea/.idea.HopFrame/.idea/workspace.xml index 51a4601..7332fc3 100644 --- a/.idea/.idea.HopFrame/.idea/workspace.xml +++ b/.idea/.idea.HopFrame/.idea/workspace.xml @@ -12,13 +12,19 @@ - - + + + + + + + - - - + + + + - + @@ -125,28 +131,28 @@ - { - "keyToString": { - ".NET Launch Settings Profile.HopFrame.Testing.Api: https.executor": "Run", - ".NET Launch Settings Profile.HopFrame.Testing.executor": "Run", - ".NET Launch Settings Profile.HopFrame.Testing: https.executor": "Run", - ".NET Project.HopFrame.Testing.executor": "Run", - "72b118b0-a6fc-4561-acdf-74f0b454dbb8.executor": "Debug", - "RunOnceActivity.ShowReadmeOnStart": "true", - "RunOnceActivity.git.unshallow": "true", - "b5f11219-dfc4-47a1-b02c-90ab603034fb.executor": "Debug", - "dcdf1689-dc07-47e4-8824-2e60a4fbf301.executor": "Debug", - "git-widget-placeholder": "!35 on feature/advanced-search", - "list.type.of.created.stylesheet": "CSS", - "node.js.detected.package.eslint": "true", - "node.js.detected.package.tslint": "true", - "node.js.selected.package.eslint": "(autodetect)", - "node.js.selected.package.tslint": "(autodetect)", - "nodejs_package_manager_path": "npm", - "settings.editor.selected.configurable": "preferences.pluginManager", - "vue.rearranger.settings.migration": "true" + +}]]> @@ -696,7 +703,6 @@ + + + + + + + + + + + + + + file://$PROJECT_DIR$/src/HopFrame.Core/Services/Implementations/SearchExpressionBuilder.cs + 102 + + + + + + + + + \ No newline at end of file diff --git a/src/HopFrame.Core/ServiceCollectionExtensions.cs b/src/HopFrame.Core/ServiceCollectionExtensions.cs index daf73dd..0d9ead1 100644 --- a/src/HopFrame.Core/ServiceCollectionExtensions.cs +++ b/src/HopFrame.Core/ServiceCollectionExtensions.cs @@ -17,6 +17,7 @@ public static class ServiceCollectionExtensions { services.AddScoped(); services.TryAddScoped(); services.TryAddScoped(); + services.AddScoped(); return services; } diff --git a/src/HopFrame.Core/Services/ISearchExpressionBuilder.cs b/src/HopFrame.Core/Services/ISearchExpressionBuilder.cs new file mode 100644 index 0000000..5765e4c --- /dev/null +++ b/src/HopFrame.Core/Services/ISearchExpressionBuilder.cs @@ -0,0 +1,9 @@ +using System.Linq.Expressions; +using System.Reflection; +using HopFrame.Core.Config; + +namespace HopFrame.Core.Services; + +public interface ISearchExpressionBuilder { + Expression? BuildSearchExpression(TableConfig table, string searchTerm, ParameterExpression parameter); +} \ No newline at end of file diff --git a/src/HopFrame.Core/Services/Implementations/ContextExplorer.cs b/src/HopFrame.Core/Services/Implementations/ContextExplorer.cs index e9b8dfe..b839bf3 100644 --- a/src/HopFrame.Core/Services/Implementations/ContextExplorer.cs +++ b/src/HopFrame.Core/Services/Implementations/ContextExplorer.cs @@ -50,12 +50,12 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr if (context is DbContextConfig) { var type = typeof(TableManager<>).MakeGenericType(table.TableType); - return Activator.CreateInstance(type, (DbContext)repo, table, this, provider) as ITableManager; + return Activator.CreateInstance(type, (DbContext)repo, table, this, provider, provider.GetRequiredService()) as ITableManager; } if (context is RepositoryGroupConfig repoConfig) { var type = typeof(RepositoryTableManager<,>).MakeGenericType(table.TableType, repoConfig.KeyProperty.PropertyType); - return Activator.CreateInstance(type, repo, this, provider) as ITableManager; + return Activator.CreateInstance(type, repo, this, provider, provider.GetRequiredService()) as ITableManager; } } @@ -72,12 +72,12 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr if (context is DbContextConfig) { var type = typeof(TableManager<>).MakeGenericType(table.TableType); - return Activator.CreateInstance(type, (DbContext)repo, table, this, provider) as ITableManager; + return Activator.CreateInstance(type, (DbContext)repo, table, this, provider, provider.GetRequiredService()) as ITableManager; } if (context is RepositoryGroupConfig repoConfig) { var type = typeof(RepositoryTableManager<,>).MakeGenericType(table.TableType, repoConfig.KeyProperty.PropertyType); - return Activator.CreateInstance(type, repo, this, provider) as ITableManager; + return Activator.CreateInstance(type, repo, this, provider, provider.GetRequiredService()) as ITableManager; } } diff --git a/src/HopFrame.Core/Services/Implementations/RepositoryTableManager.cs b/src/HopFrame.Core/Services/Implementations/RepositoryTableManager.cs index f86ab10..541c203 100644 --- a/src/HopFrame.Core/Services/Implementations/RepositoryTableManager.cs +++ b/src/HopFrame.Core/Services/Implementations/RepositoryTableManager.cs @@ -3,7 +3,7 @@ using HopFrame.Core.Repositories; namespace HopFrame.Core.Services.Implementations; -public class RepositoryTableManager(IHopFrameRepository repo, IContextExplorer explorer, IServiceProvider provider) : ITableManager where TModel : class { +public class RepositoryTableManager(IHopFrameRepository repo, IContextExplorer explorer, IServiceProvider provider, ISearchExpressionBuilder searchExpressionBuilder) : ITableManager where TModel : class { public async Task> LoadPage(int page, int perPage = 20) { return await repo.LoadPage(page, perPage); } @@ -34,7 +34,7 @@ public class RepositoryTableManager(IHopFrameRepository DisplayProperty(object? item, PropertyConfig prop, object? value = null, object? enumerableValue = null) { - var manager = new TableManager(null!, null!, explorer, provider); + var manager = new TableManager(null!, null!, explorer, provider, searchExpressionBuilder); return await manager.DisplayProperty(item, prop, value, enumerableValue); } } \ No newline at end of file diff --git a/src/HopFrame.Core/Services/Implementations/SearchExpressionBuilder.cs b/src/HopFrame.Core/Services/Implementations/SearchExpressionBuilder.cs new file mode 100644 index 0000000..51978a6 --- /dev/null +++ b/src/HopFrame.Core/Services/Implementations/SearchExpressionBuilder.cs @@ -0,0 +1,151 @@ +using System.Linq.Expressions; +using System.Reflection; +using HopFrame.Core.Config; + +namespace HopFrame.Core.Services.Implementations; + +internal sealed class SearchExpressionBuilder(IContextExplorer explorer) : ISearchExpressionBuilder { + private readonly struct SearchPart { + public string? Property { get; init; } + public string Term { get; init; } + public bool Negated { get; init; } + } + + private Expression AddPropertySearchExpression(PropertyInfo property, ParameterExpression parameter, string searchTerm, PropertyConfig config) { + Expression propertyAccess = Expression.Property(parameter, property); + + if (config.IsEnumerable) { //Call Count() extension method before checking the search term + propertyAccess = Expression.Property(propertyAccess, config.Info.PropertyType.GetProperty(nameof(List.Count))!); + } + + var toStringCall = Expression.Call(propertyAccess, nameof(ToString), Type.EmptyTypes); + var searchExpression = Expression.Call( + toStringCall, + typeof(string).GetMethod(config.IsEnumerable ? nameof(string.Equals) : nameof(string.Contains), [typeof(string)])!, + Expression.Constant(searchTerm)); + + return searchExpression; + } + + private Expression AddForeignPropertySearchExpression(PropertyInfo navigationProperty, PropertyInfo displayedProperty, ParameterExpression parameter, string searchTerm) { + var navigationAccess = Expression.Property(parameter, navigationProperty); + var nullCheck = Expression.NotEqual(navigationAccess, Expression.Constant(null)); + var displayedPropertyAccess = Expression.Property(navigationAccess, displayedProperty); + + var toStringCall = Expression.Call(displayedPropertyAccess, nameof(ToString), Type.EmptyTypes); + var searchExpression = Expression.Call( + toStringCall, + typeof(string).GetMethod(nameof(string.Contains), new[] { typeof(string) })!, + Expression.Constant(searchTerm)); + + return Expression.AndAlso(nullCheck, searchExpression); + } + + private IEnumerable GetSuitableProperties(TableConfig table) { + Type[] validTypes = [typeof(string), typeof(Guid), typeof(DateTime), typeof(DateOnly), typeof(TimeOnly)]; + + return table.Properties + .Where(prop => !prop.IsVirtualProperty) + .Where(prop => prop.List) + .Where(prop => prop.Searchable) + .Where(prop => prop.Info.PropertyType.IsEnum || validTypes.Contains(prop.Info.PropertyType) || prop.IsEnumerable) + .Select(prop => prop.Info); + } + + private IEnumerable<(PropertyInfo navigation, PropertyInfo display)> GetSuitableForeignProperties(TableConfig table) { + return table.Properties + .Where(prop => prop.List) + .Where(prop => prop.IsRelation) + .Where(prop => prop.Searchable) + .Where(prop => prop.DisplayedProperty != null) + .Select(prop => (prop.Info, explorer + .GetTable(prop.Info.PropertyType)!.Properties + .Find(p => p.Info.Name == prop.DisplayedProperty!.Name)! + .Info)); + } + + private IEnumerable ExtractSearchParts(string searchTerm) { + var rawParts = searchTerm.Split(' '); + var parts = new List(); + + foreach (var part in rawParts) { + if (string.IsNullOrWhiteSpace(part)) + continue; + + if (!part.Contains('=')) { + var negated = part.StartsWith('!'); + + parts.Add(new() { + Term = negated ? part[1..] : part, + Negated = negated, + }); + continue; + } + + var split = part.Split('='); + var term = string.Join('=', split[1..]); + var termNegated = term.StartsWith('!'); + + parts.Add(new() { + Property = split[0], + Term = termNegated ? term[1..] : term, + Negated = termNegated + }); + } + + return parts; + } + + public Expression? BuildSearchExpression(TableConfig table, string searchTerm, ParameterExpression parameter) { + var properties = GetSuitableProperties(table).ToArray(); + var foreignProperties = GetSuitableForeignProperties(table).ToArray(); + + var parts = ExtractSearchParts(searchTerm); + + Expression? expression = null; + foreach (var part in parts) { + Expression? subExp = null; + + if (part.Property is null) { + foreach (var property in properties) { + var exp = AddPropertySearchExpression(property, parameter, part.Term, table.Properties.First(p => p.Info == property)); + subExp = subExp is null + ? exp + : Expression.OrElse(subExp, exp); + } + + foreach (var property in foreignProperties) { + var exp = AddForeignPropertySearchExpression(property.navigation, property.display, parameter, part.Term); + subExp = subExp is null + ? exp + : Expression.OrElse(subExp, exp); + } + + if (subExp is null) + continue; + } + + var prop = properties.FirstOrDefault(p => p.Name == part.Property); + if (prop is not null) { + subExp = AddPropertySearchExpression(prop, parameter, part.Term, table.Properties.First(p => p.Info == prop)); + } + + var forProp = foreignProperties.FirstOrDefault(p => p.navigation.Name == part.Property); + if (forProp.navigation is not null) { + subExp = AddForeignPropertySearchExpression(forProp.navigation, forProp.display, parameter, part.Term); + } + + if (subExp is null) + continue; + + if (part.Negated) + subExp = Expression.Not(subExp); + + expression = expression is null + ? subExp + : Expression.AndAlso(expression, subExp); + } + + return expression; + } +} \ No newline at end of file diff --git a/src/HopFrame.Core/Services/Implementations/TableManager.cs b/src/HopFrame.Core/Services/Implementations/TableManager.cs index e5172fc..5a64ce4 100644 --- a/src/HopFrame.Core/Services/Implementations/TableManager.cs +++ b/src/HopFrame.Core/Services/Implementations/TableManager.cs @@ -1,6 +1,7 @@ using System.Collections; using System.ComponentModel.DataAnnotations; using System.Linq.Expressions; +using System.Reflection; using System.Reflection.Metadata; using HopFrame.Core.Config; using Microsoft.EntityFrameworkCore; @@ -8,7 +9,7 @@ using Microsoft.EntityFrameworkCore.ChangeTracking; namespace HopFrame.Core.Services.Implementations; -internal sealed class TableManager(DbContext context, TableConfig config, IContextExplorer explorer, IServiceProvider provider) : ITableManager where TModel : class { +internal sealed class TableManager(DbContext context, TableConfig config, IContextExplorer explorer, IServiceProvider provider, ISearchExpressionBuilder searchExpressionBuilder) : ITableManager where TModel : class { public async Task> LoadPage(int page, int perPage = 20) { var table = context.Set(); @@ -22,70 +23,13 @@ internal sealed class TableManager(DbContext context, TableConfig config public async Task<(IEnumerable, int)> Search(string searchTerm, int page = 0, int perPage = 20) { var table = context.Set(); - var isNegative = searchTerm.StartsWith('!'); - if (isNegative) - searchTerm = searchTerm[1..]; - - Type[] validTypes = [typeof(string), typeof(Guid)]; - var parameter = Expression.Parameter(typeof(TModel), "x"); - var properties = config.Properties - .Where(prop => !prop.IsVirtualProperty) - .Where(prop => prop.List) - .Where(prop => prop.Info.PropertyType.IsEnum || validTypes.Contains(prop.Info.PropertyType)) - .Select(prop => prop.Info); - - Expression? combinedExpression = null; + var exp = searchExpressionBuilder.BuildSearchExpression(config, searchTerm, parameter); - foreach (var property in properties) { - var propertyAccess = Expression.Property(parameter, property); - var toStringCall = Expression.Call(propertyAccess, nameof(ToString), Type.EmptyTypes); - var searchExpression = Expression.Call( - toStringCall, - typeof(string).GetMethod(nameof(string.Contains), [typeof(string)])!, - Expression.Constant(searchTerm)); - - combinedExpression = combinedExpression == null - ? searchExpression - : Expression.OrElse(combinedExpression, searchExpression); - } - - var foreignProperties = config.Properties - .Where(prop => prop.List) - .Where(prop => prop.IsRelation) - .Where(prop => prop.DisplayedProperty != null) - .Select(prop => (prop.Info, explorer - .GetTable(prop.Info.PropertyType)!.Properties - .Find(p => p.Info.Name == prop.DisplayedProperty!.Name)! - .Info)); + if (exp is null) + return ([], 0); - foreach (var (navigationProperty, displayedProperty) in foreignProperties) { - var navigationAccess = Expression.Property(parameter, navigationProperty); - var nullCheck = Expression.NotEqual(navigationAccess, Expression.Constant(null)); - var displayedPropertyAccess = Expression.Property(navigationAccess, displayedProperty); - - var toStringCall = Expression.Call(displayedPropertyAccess, nameof(ToString), Type.EmptyTypes); - var searchExpression = Expression.Call( - toStringCall, - typeof(string).GetMethod(nameof(string.Contains), new[] { typeof(string) })!, - Expression.Constant(searchTerm)); - - var safeSearchExpression = Expression.AndAlso(nullCheck, searchExpression); - - combinedExpression = combinedExpression == null - ? safeSearchExpression - : Expression.OrElse(combinedExpression, safeSearchExpression); - } - - if (combinedExpression == null) - return ( - await LoadPage(page, perPage), - await TotalPages(perPage)); - - if (isNegative) - combinedExpression = Expression.Not(combinedExpression); - - var lambda = Expression.Lambda>(combinedExpression, parameter); + var lambda = Expression.Lambda>(exp, parameter); var result = await IncludeForeignKeys(table) .Where(lambda) .Skip(page * perPage) @@ -98,7 +42,7 @@ internal sealed class TableManager(DbContext context, TableConfig config return (result, (int)Math.Ceiling(totalEntries / (double)perPage)); } - + public async Task TotalPages(int perPage = 20) { var table = context.Set(); return (int)Math.Ceiling(await table.CountAsync() / (double)perPage); diff --git a/src/HopFrame.Web/Components/Pages/HopFrameTablePage.razor b/src/HopFrame.Web/Components/Pages/HopFrameTablePage.razor index c4eb1ac..84f19c3 100644 --- a/src/HopFrame.Web/Components/Pages/HopFrameTablePage.razor +++ b/src/HopFrame.Web/Components/Pages/HopFrameTablePage.razor @@ -300,12 +300,17 @@ await Reload(); } - private void SearchSuggestionSelected(string? suggestion) { + private async Task SearchSuggestionSelected(string? suggestion) { if (string.IsNullOrWhiteSpace(suggestion)) return; _searchTerm = SearchSuggestions.CompleteSearchSuggestion(_config!, _searchTerm ?? string.Empty, suggestion); _searchBox!.Value = _searchTerm; _searchBox.FocusAsync(); UpdateSearchSuggestions(); + + if (!suggestion.EndsWith('=')) + await OnSearch(new() { + Value = _searchTerm + }); } private void UpdateSearchSuggestions() { diff --git a/src/HopFrame.Web/Services/Implementation/SearchSuggestionProvider.cs b/src/HopFrame.Web/Services/Implementation/SearchSuggestionProvider.cs index 5bc622d..e65f347 100644 --- a/src/HopFrame.Web/Services/Implementation/SearchSuggestionProvider.cs +++ b/src/HopFrame.Web/Services/Implementation/SearchSuggestionProvider.cs @@ -4,7 +4,7 @@ using HopFrame.Web.Helpers; namespace HopFrame.Web.Services.Implementation; -public sealed class SearchSuggestionProvider(IContextExplorer explorer, IServiceProvider provider) : ISearchSuggestionProvider { +public sealed class SearchSuggestionProvider : ISearchSuggestionProvider { public IEnumerable GenerateSearchSuggestions(TableConfig table, string searchText) { if (table.ContextConfig is not DbContextConfig) return []; @@ -27,15 +27,6 @@ public sealed class SearchSuggestionProvider(IContextExplorer explorer, IService if (property.Info.PropertyType == typeof(TimeOnly)) return [TimeOnly.FromDateTime(DateTime.Now).ToString()]; - - if (property.IsRelation) { - var manager = explorer.GetTableManager(table.TableType); - var entries = manager!.LoadPage(0, 100).Result; - return entries - .Select(e => manager.DisplayProperty(e, property).Result) - .Distinct() - .Take(10); - } } if (searchText.Length != 0 && !searchText.EndsWith(' ')) @@ -45,6 +36,7 @@ public sealed class SearchSuggestionProvider(IContextExplorer explorer, IService var searchableProperties = table.Properties .Where(p => !p.IsVirtualProperty) .Where(p => p.List) + .Where(p => p.Searchable) .Where(p => p.Info.PropertyType.IsEnum || p.Info.PropertyType.IsNumeric() || diff --git a/tests/HopFrame.Tests.Core/Services/ContextExplorerTests.cs b/tests/HopFrame.Tests.Core/Services/ContextExplorerTests.cs index d092f88..e26974f 100644 --- a/tests/HopFrame.Tests.Core/Services/ContextExplorerTests.cs +++ b/tests/HopFrame.Tests.Core/Services/ContextExplorerTests.cs @@ -1,6 +1,8 @@ using HopFrame.Core.Config; +using HopFrame.Core.Services; using HopFrame.Core.Services.Implementations; using HopFrame.Tests.Core.Models; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Moq; @@ -121,6 +123,7 @@ public class ContextExplorerTests { var dbContext = new MockDbContext(); var provider = new Mock(); provider.Setup(p => p.GetService(typeof(MockDbContext))).Returns(dbContext); + provider.Setup(p => p.GetService(typeof(ISearchExpressionBuilder))).Returns(new Mock().Object); var contextExplorer = new ContextExplorer(config, provider.Object, new Logger(new LoggerFactory())); // Act diff --git a/tests/HopFrame.Tests.Core/Services/DisplayPropertyTests.cs b/tests/HopFrame.Tests.Core/Services/DisplayPropertyTests.cs index 79d6ecb..b68d225 100644 --- a/tests/HopFrame.Tests.Core/Services/DisplayPropertyTests.cs +++ b/tests/HopFrame.Tests.Core/Services/DisplayPropertyTests.cs @@ -19,7 +19,7 @@ public class DisplayPropertyTests { _explorerMock = new Mock(); _config = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "Models", 0); _tableManager = - new TableManager(contextMock.Object, _config, _explorerMock.Object, _providerMock.Object); + new TableManager(contextMock.Object, _config, _explorerMock.Object, _providerMock.Object, new SearchExpressionBuilder(_explorerMock.Object)); } [Fact] diff --git a/tests/HopFrame.Tests.Core/Services/TableManagerTests.cs b/tests/HopFrame.Tests.Core/Services/TableManagerTests.cs index 3ab4bcd..8e50a64 100644 --- a/tests/HopFrame.Tests.Core/Services/TableManagerTests.cs +++ b/tests/HopFrame.Tests.Core/Services/TableManagerTests.cs @@ -10,6 +10,9 @@ using Moq; namespace HopFrame.Tests.Core.Services; public class TableManagerTests { + + private Mock _searchBuilderMock = new(); + private Mock CreateMockDbContext(List data) where TModel : class { var dbContext = new Mock(); var dbSet = CreateMockDbSet(data); @@ -51,7 +54,7 @@ public class TableManagerTests { var config = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "Models", 0); var explorer = new Mock(); var provider = new Mock(); - var manager = new TableManager(dbContext.Object, config, explorer.Object, provider.Object); + var manager = new TableManager(dbContext.Object, config, explorer.Object, provider.Object, _searchBuilderMock.Object); // Act var result = (await manager.LoadPage(1, 2)).ToArray(); @@ -61,32 +64,6 @@ public class TableManagerTests { Assert.Equal("Item3", ((MockModel)result[0]).Name); } - [Fact] - public async Task Search_ReturnsMatchingData() { - // Arrange - var data = new List { - new MockModel { Id = 1, Name = "Item1" }, - new MockModel { Id = 2, Name = "Item2" }, - new MockModel { Id = 3, Name = "TestItem" } - }; - var dbContext = CreateMockDbContext(data); - var config = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "Models", 0); - config.Properties.Add(new PropertyConfig(typeof(MockModel).GetProperty("Name")!, config, 0) - { Searchable = true }); - var explorer = new Mock(); - var provider = new Mock(); - var manager = new TableManager(dbContext.Object, config, explorer.Object, provider.Object); - - // Act - var (result, totalPages) = await manager.Search("Test", 0, 2); - - // Assert - var collection = result as object[] ?? result.ToArray(); - Assert.Single(collection); - Assert.Equal("TestItem", ((MockModel)collection.First()).Name); - Assert.Equal(1, totalPages); - } - [Fact] public async Task TotalPages_ReturnsCorrectPageCount() { // Arrange @@ -99,7 +76,7 @@ public class TableManagerTests { var config = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "Models", 0); var explorer = new Mock(); var provider = new Mock(); - var manager = new TableManager(dbContext, config, explorer.Object, provider.Object); + var manager = new TableManager(dbContext, config, explorer.Object, provider.Object, _searchBuilderMock.Object); await dbContext.Models.AddRangeAsync(data); await dbContext.SaveChangesAsync(); @@ -121,7 +98,7 @@ public class TableManagerTests { var config = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "Models", 0); var explorer = new Mock(); var provider = new Mock(); - var manager = new TableManager(dbContext.Object, config, explorer.Object, provider.Object); + var manager = new TableManager(dbContext.Object, config, explorer.Object, provider.Object, _searchBuilderMock.Object); var item = data.First(); // Act @@ -142,7 +119,7 @@ public class TableManagerTests { var config = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "Models", 0); var explorer = new Mock(); var provider = new Mock(); - var manager = new TableManager(dbContext.Object, config, explorer.Object, provider.Object); + var manager = new TableManager(dbContext.Object, config, explorer.Object, provider.Object, _searchBuilderMock.Object); // Act await manager.EditItem(data.First()); @@ -159,7 +136,7 @@ public class TableManagerTests { var config = new TableConfig(new DbContextConfig(typeof(MockDbContext), null!), typeof(MockModel), "Models", 0); var explorer = new Mock(); var provider = new Mock(); - var manager = new TableManager(dbContext.Object, config, explorer.Object, provider.Object); + var manager = new TableManager(dbContext.Object, config, explorer.Object, provider.Object, _searchBuilderMock.Object); var newItem = new MockModel { Id = 3, Name = "NewItem" }; // Act