diff --git a/samples/YesSql.Samples.Web/Controllers/HomeController.cs b/samples/YesSql.Samples.Web/Controllers/HomeController.cs index 9af45606..cc4b0a4d 100644 --- a/samples/YesSql.Samples.Web/Controllers/HomeController.cs +++ b/samples/YesSql.Samples.Web/Controllers/HomeController.cs @@ -44,17 +44,25 @@ public async Task Index([ModelBinder(BinderType = typeof(QueryFil var search = new Filter { SearchText = currentSearchText, - OriginalSearchText = currentSearchText, - Filters = new List() - { - new SelectListItem("Select...", ""), - new SelectListItem("Published", ContentsStatus.Published.ToString()), - new SelectListItem("Draft", ContentsStatus.Draft.ToString()) - } + OriginalSearchText = currentSearchText }; filterResult.MapTo(search); + + search.Statuses = new List() + { + new SelectListItem("Select...", "", search.SelectedStatus == BlogPostStatus.Default), + new SelectListItem("Published", BlogPostStatus.Published.ToString(), search.SelectedStatus == BlogPostStatus.Published), + new SelectListItem("Draft", BlogPostStatus.Draft.ToString(), search.SelectedStatus == BlogPostStatus.Draft) + }; + + search.Sorts = new List() + { + new SelectListItem("Newest", BlogPostSort.Newest.ToString(), search.SelectedSort == BlogPostSort.Newest), + new SelectListItem("Oldest", BlogPostSort.Oldest.ToString(), search.SelectedSort == BlogPostSort.Oldest) + }; + var vm = new BlogPostViewModel { BlogPosts = posts, diff --git a/samples/YesSql.Samples.Web/Startup.cs b/samples/YesSql.Samples.Web/Startup.cs index 48c164d8..871a2f37 100644 --- a/samples/YesSql.Samples.Web/Startup.cs +++ b/samples/YesSql.Samples.Web/Startup.cs @@ -34,14 +34,14 @@ public void ConfigureServices(IServiceCollection services) .WithNamedTerm("status", builder => builder .OneCondition((val, query) => { - if (Enum.TryParse(val, true, out var e)) + if (Enum.TryParse(val, true, out var e)) { switch (e) { - case ContentsStatus.Published: + case BlogPostStatus.Published: query.With(x => x.Published); break; - case ContentsStatus.Draft: + case BlogPostStatus.Draft: query.With(x => !x.Published); break; default: @@ -53,22 +53,66 @@ public void ConfigureServices(IServiceCollection services) }) .MapTo((val, model) => { - if (Enum.TryParse(val, true, out var e)) + if (Enum.TryParse(val, true, out var e)) { - model.SelectedFilter = e; + model.SelectedStatus = e; } }) .MapFrom((model) => { - if (model.SelectedFilter != ContentsStatus.Default) + if (model.SelectedStatus != BlogPostStatus.Default) { - return (true, model.SelectedFilter.ToString()); + return (true, model.SelectedStatus.ToString()); } return (false, String.Empty); }) ) + .WithNamedTerm("sort", b => b + .OneCondition((val, query) => + { + if (Enum.TryParse(val, true, out var e)) + { + switch (e) + { + case BlogPostSort.Newest: + query.With().OrderByDescending(x => x.PublishedUtc); + break; + case BlogPostSort.Oldest: + query.With().OrderBy(x => x.PublishedUtc); + break; + default: + query.With().OrderByDescending(x => x.PublishedUtc); + break; + } + } + else + { + query.With().OrderByDescending(x => x.PublishedUtc); + } + + return query; + }) + .MapTo((val, model) => + { + if (Enum.TryParse(val, true, out var e)) + { + model.SelectedSort = e; + } + }) + .MapFrom((model) => + { + if (model.SelectedSort != BlogPostSort.Newest) + { + return (true, model.SelectedSort.ToString()); + } + + return (false, String.Empty); + + }) + .AlwaysRun() + ) .WithDefaultTerm("title", b => b .ManyCondition( ((val, query) => query.With(x => x.Title.Contains(val))), diff --git a/samples/YesSql.Samples.Web/ViewModels/BlogPostViewModel.cs b/samples/YesSql.Samples.Web/ViewModels/BlogPostViewModel.cs index 06bd6356..8608c9b3 100644 --- a/samples/YesSql.Samples.Web/ViewModels/BlogPostViewModel.cs +++ b/samples/YesSql.Samples.Web/ViewModels/BlogPostViewModel.cs @@ -19,19 +19,29 @@ public class Filter public string Author { get; set; } public string SearchText { get; set; } public string OriginalSearchText { get; set; } - public ContentsStatus SelectedFilter { get; set; } + public BlogPostStatus SelectedStatus { get; set; } + public BlogPostSort SelectedSort { get; set; } - [ModelBinder(BinderType = typeof(QueryFilterEngineModelBinder), Name = "SearchText")] + [ModelBinder(BinderType = typeof(QueryFilterEngineModelBinder), Name = nameof(SearchText))] public QueryFilterResult FilterResult { get; set; } [BindNever] - public List Filters { get; set; } = new(); + public List Statuses { get; set; } = new(); + + [BindNever] + public List Sorts { get; set; } = new(); } - public enum ContentsStatus + public enum BlogPostStatus { Default, Draft, Published } + + public enum BlogPostSort + { + Newest, + Oldest, + } } diff --git a/samples/YesSql.Samples.Web/Views/Home/Index.cshtml b/samples/YesSql.Samples.Web/Views/Home/Index.cshtml index f6fc4458..fd053b1f 100644 --- a/samples/YesSql.Samples.Web/Views/Home/Index.cshtml +++ b/samples/YesSql.Samples.Web/Views/Home/Index.cshtml @@ -8,6 +8,36 @@ + +
+ + + +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+ +
+ + +
+ @foreach (var blogPost in Model.BlogPosts) {
@@ -19,18 +49,5 @@
} -
- -
- -
- - -
- -
- - -
diff --git a/src/YesSql.Filters.Abstractions/Builders/UnaryEngineBuilder.cs b/src/YesSql.Filters.Abstractions/Builders/UnaryEngineBuilder.cs index 91380631..e6af3a7a 100644 --- a/src/YesSql.Filters.Abstractions/Builders/UnaryEngineBuilder.cs +++ b/src/YesSql.Filters.Abstractions/Builders/UnaryEngineBuilder.cs @@ -26,6 +26,13 @@ public UnaryEngineBuilder AllowMultiple() return this; } + + public UnaryEngineBuilder AlwaysRun() + { + _termOption.AlwaysRun = true; + + return this; + } public override (Parser Parser, TTermOption TermOption) Build() => (_parser, _termOption); diff --git a/src/YesSql.Filters.Abstractions/Services/FilterResult.cs b/src/YesSql.Filters.Abstractions/Services/FilterResult.cs index e9f8a374..7a9f285c 100644 --- a/src/YesSql.Filters.Abstractions/Services/FilterResult.cs +++ b/src/YesSql.Filters.Abstractions/Services/FilterResult.cs @@ -9,7 +9,7 @@ namespace YesSql.Filters.Abstractions.Services public abstract class FilterResult : IEnumerable where TTermOption : TermOption { - protected Dictionary _terms = new Dictionary(); + protected Dictionary _terms = new Dictionary(StringComparer.OrdinalIgnoreCase); public FilterResult(IReadOnlyDictionary termOptions) { diff --git a/src/YesSql.Filters.Abstractions/Services/TermOption.cs b/src/YesSql.Filters.Abstractions/Services/TermOption.cs index 5fc12013..85684128 100644 --- a/src/YesSql.Filters.Abstractions/Services/TermOption.cs +++ b/src/YesSql.Filters.Abstractions/Services/TermOption.cs @@ -17,6 +17,11 @@ public TermOption(string name) /// public bool Single { get; set; } = true; + /// + /// Whether this term filter should always run, even when not specified. + /// + public bool AlwaysRun { get; set; } + public Delegate MapTo { get; set; } public Delegate MapFrom { get; set; } public Func MapFromFactory { get; set; } diff --git a/src/YesSql.Filters.Query/QueryEngineBuilder.cs b/src/YesSql.Filters.Query/QueryEngineBuilder.cs index 6dfd3e0f..96aee8c1 100644 --- a/src/YesSql.Filters.Query/QueryEngineBuilder.cs +++ b/src/YesSql.Filters.Query/QueryEngineBuilder.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using YesSql.Filters.Abstractions.Builders; @@ -24,7 +25,7 @@ public IQueryParser Build() var builders = _termBuilders.Values.Select(x => x.Build()); var parsers = builders.Select(x => x.Parser).ToArray(); - var termOptions = builders.Select(x => x.TermOption).ToDictionary(k => k.Name, v => v); + var termOptions = builders.Select(x => x.TermOption).ToDictionary(k => k.Name, v => v, StringComparer.OrdinalIgnoreCase); return new QueryParser(parsers, termOptions); } diff --git a/src/YesSql.Filters.Query/QueryFilterResult.cs b/src/YesSql.Filters.Query/QueryFilterResult.cs index cf1c84c9..e324fe8b 100644 --- a/src/YesSql.Filters.Query/QueryFilterResult.cs +++ b/src/YesSql.Filters.Query/QueryFilterResult.cs @@ -36,14 +36,34 @@ public async ValueTask> ExecuteAsync(QueryExecutionContext context) foreach (var term in _terms.Values) { // TODO optimize value task. - context.CurrentTermOption = TermOptions[term.TermName]; - - var termQuery = visitor.Visit(term, context); - context.Item = await termQuery.Invoke(context.Item); - context.CurrentTermOption = null; + await VisitTerm(TermOptions, context, visitor, term); } + // Execute always run terms. These are not added to the terms list. + foreach (var termOption in TermOptions) + { + if (!termOption.Value.AlwaysRun) + { + continue; + } + + if (!_terms.ContainsKey(termOption.Key)) + { + var alwaysRunNode = new NamedTermNode(termOption.Key, new UnaryNode(String.Empty)); + await VisitTerm(TermOptions, context, visitor, alwaysRunNode); + } + } + return context.Item; } + + private async static Task VisitTerm(IReadOnlyDictionary> termOptions, QueryExecutionContext context, QueryFilterVisitor visitor, TermNode term) + { + context.CurrentTermOption = termOptions[term.TermName]; + + var termQuery = visitor.Visit(term, context); + context.Item = await termQuery.Invoke(context.Item); + context.CurrentTermOption = null; + } } }