Skip to content

Commit 6681e3d

Browse files
Adds AlwaysRun feature to filters (#385)
1 parent a47ceb4 commit 6681e3d

File tree

9 files changed

+150
-38
lines changed

9 files changed

+150
-38
lines changed

samples/YesSql.Samples.Web/Controllers/HomeController.cs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,25 @@ public async Task<IActionResult> Index([ModelBinder(BinderType = typeof(QueryFil
4444
var search = new Filter
4545
{
4646
SearchText = currentSearchText,
47-
OriginalSearchText = currentSearchText,
48-
Filters = new List<SelectListItem>()
49-
{
50-
new SelectListItem("Select...", ""),
51-
new SelectListItem("Published", ContentsStatus.Published.ToString()),
52-
new SelectListItem("Draft", ContentsStatus.Draft.ToString())
53-
}
47+
OriginalSearchText = currentSearchText
5448
};
5549

5650
filterResult.MapTo(search);
5751

52+
53+
search.Statuses = new List<SelectListItem>()
54+
{
55+
new SelectListItem("Select...", "", search.SelectedStatus == BlogPostStatus.Default),
56+
new SelectListItem("Published", BlogPostStatus.Published.ToString(), search.SelectedStatus == BlogPostStatus.Published),
57+
new SelectListItem("Draft", BlogPostStatus.Draft.ToString(), search.SelectedStatus == BlogPostStatus.Draft)
58+
};
59+
60+
search.Sorts = new List<SelectListItem>()
61+
{
62+
new SelectListItem("Newest", BlogPostSort.Newest.ToString(), search.SelectedSort == BlogPostSort.Newest),
63+
new SelectListItem("Oldest", BlogPostSort.Oldest.ToString(), search.SelectedSort == BlogPostSort.Oldest)
64+
};
65+
5866
var vm = new BlogPostViewModel
5967
{
6068
BlogPosts = posts,

samples/YesSql.Samples.Web/Startup.cs

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,14 @@ public void ConfigureServices(IServiceCollection services)
3434
.WithNamedTerm("status", builder => builder
3535
.OneCondition((val, query) =>
3636
{
37-
if (Enum.TryParse<ContentsStatus>(val, true, out var e))
37+
if (Enum.TryParse<BlogPostStatus>(val, true, out var e))
3838
{
3939
switch (e)
4040
{
41-
case ContentsStatus.Published:
41+
case BlogPostStatus.Published:
4242
query.With<BlogPostIndex>(x => x.Published);
4343
break;
44-
case ContentsStatus.Draft:
44+
case BlogPostStatus.Draft:
4545
query.With<BlogPostIndex>(x => !x.Published);
4646
break;
4747
default:
@@ -53,22 +53,66 @@ public void ConfigureServices(IServiceCollection services)
5353
})
5454
.MapTo<Filter>((val, model) =>
5555
{
56-
if (Enum.TryParse<ContentsStatus>(val, true, out var e))
56+
if (Enum.TryParse<BlogPostStatus>(val, true, out var e))
5757
{
58-
model.SelectedFilter = e;
58+
model.SelectedStatus = e;
5959
}
6060
})
6161
.MapFrom<Filter>((model) =>
6262
{
63-
if (model.SelectedFilter != ContentsStatus.Default)
63+
if (model.SelectedStatus != BlogPostStatus.Default)
6464
{
65-
return (true, model.SelectedFilter.ToString());
65+
return (true, model.SelectedStatus.ToString());
6666
}
6767

6868
return (false, String.Empty);
6969

7070
})
7171
)
72+
.WithNamedTerm("sort", b => b
73+
.OneCondition((val, query) =>
74+
{
75+
if (Enum.TryParse<BlogPostSort>(val, true, out var e))
76+
{
77+
switch (e)
78+
{
79+
case BlogPostSort.Newest:
80+
query.With<BlogPostIndex>().OrderByDescending(x => x.PublishedUtc);
81+
break;
82+
case BlogPostSort.Oldest:
83+
query.With<BlogPostIndex>().OrderBy(x => x.PublishedUtc);
84+
break;
85+
default:
86+
query.With<BlogPostIndex>().OrderByDescending(x => x.PublishedUtc);
87+
break;
88+
}
89+
}
90+
else
91+
{
92+
query.With<BlogPostIndex>().OrderByDescending(x => x.PublishedUtc);
93+
}
94+
95+
return query;
96+
})
97+
.MapTo<Filter>((val, model) =>
98+
{
99+
if (Enum.TryParse<BlogPostSort>(val, true, out var e))
100+
{
101+
model.SelectedSort = e;
102+
}
103+
})
104+
.MapFrom<Filter>((model) =>
105+
{
106+
if (model.SelectedSort != BlogPostSort.Newest)
107+
{
108+
return (true, model.SelectedSort.ToString());
109+
}
110+
111+
return (false, String.Empty);
112+
113+
})
114+
.AlwaysRun()
115+
)
72116
.WithDefaultTerm("title", b => b
73117
.ManyCondition(
74118
((val, query) => query.With<BlogPostIndex>(x => x.Title.Contains(val))),

samples/YesSql.Samples.Web/ViewModels/BlogPostViewModel.cs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,29 @@ public class Filter
1919
public string Author { get; set; }
2020
public string SearchText { get; set; }
2121
public string OriginalSearchText { get; set; }
22-
public ContentsStatus SelectedFilter { get; set; }
22+
public BlogPostStatus SelectedStatus { get; set; }
23+
public BlogPostSort SelectedSort { get; set; }
2324

24-
[ModelBinder(BinderType = typeof(QueryFilterEngineModelBinder<BlogPost>), Name = "SearchText")]
25+
[ModelBinder(BinderType = typeof(QueryFilterEngineModelBinder<BlogPost>), Name = nameof(SearchText))]
2526
public QueryFilterResult<BlogPost> FilterResult { get; set; }
2627

2728
[BindNever]
28-
public List<SelectListItem> Filters { get; set; } = new();
29+
public List<SelectListItem> Statuses { get; set; } = new();
30+
31+
[BindNever]
32+
public List<SelectListItem> Sorts { get; set; } = new();
2933
}
3034

31-
public enum ContentsStatus
35+
public enum BlogPostStatus
3236
{
3337
Default,
3438
Draft,
3539
Published
3640
}
41+
42+
public enum BlogPostSort
43+
{
44+
Newest,
45+
Oldest,
46+
}
3747
}

samples/YesSql.Samples.Web/Views/Home/Index.cshtml

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,36 @@
88
</head>
99

1010
<body class="m-5">
11+
12+
<form asp-action="IndexPost" method="post">
13+
<input type="search" asp-for="Search.OriginalSearchText" type="hidden"></input>
14+
15+
16+
<div class="form-group row mb-3">
17+
<label asp-for="Search.SearchText" class="col-2 align-self-center">Search</label>
18+
<div class="col-10">
19+
<input class="form-control" type="search" asp-for="Search.SearchText"></input>
20+
</div>
21+
</div>
22+
23+
<div class="form-group row mb-3">
24+
<label asp-for="Search.SelectedStatus" class="col-2 align-self-center">Status</label>
25+
<div class="col-10">
26+
<select class="form-control" asp-for="Search.SelectedStatus" asp-items="Model.Search.Statuses"></select>
27+
</div>
28+
</div>
29+
30+
<div class="form-group row mb-3">
31+
<label asp-for="Search.SelectedSort" class="col-2 align-self-center">Sort</label>
32+
<div class="col-10">
33+
<select class="form-control" asp-for="Search.SelectedSort" asp-items="Model.Search.Sorts"></select>
34+
</div>
35+
36+
</div>
37+
38+
<button class="btn btn-primary d-flex ms-auto mb-3" type="submit">Search</button>
39+
</form>
40+
1141
@foreach (var blogPost in Model.BlogPosts)
1242
{
1343
<div class="card">
@@ -19,18 +49,5 @@
1949
</div>
2050
}
2151

22-
<form asp-action="IndexPost" method="post">
23-
<input type="search" asp-for="Search.OriginalSearchText" type="hidden"></input>
2452

25-
<div class="w-100 form-group my-3">
26-
<input class="form-control" type="search" asp-for="Search.SearchText"></input>
27-
</div>
28-
29-
30-
<div class="w-50 form-group my-3">
31-
<select class="form-control" asp-for="Search.SelectedFilter" asp-items="Model.Search.Filters"></select>
32-
</div>
33-
34-
<button class="btn btn-primary d-block my-3" type="submit">Search</button>
35-
</form>
3653
</body>

src/YesSql.Filters.Abstractions/Builders/UnaryEngineBuilder.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ public UnaryEngineBuilder<T, TTermOption> AllowMultiple()
2626

2727
return this;
2828
}
29+
30+
public UnaryEngineBuilder<T, TTermOption> AlwaysRun()
31+
{
32+
_termOption.AlwaysRun = true;
33+
34+
return this;
35+
}
2936

3037
public override (Parser<OperatorNode> Parser, TTermOption TermOption) Build()
3138
=> (_parser, _termOption);

src/YesSql.Filters.Abstractions/Services/FilterResult.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace YesSql.Filters.Abstractions.Services
99
public abstract class FilterResult<T, TTermOption> : IEnumerable<TermNode> where TTermOption : TermOption
1010
{
1111

12-
protected Dictionary<string, TermNode> _terms = new Dictionary<string, TermNode>();
12+
protected Dictionary<string, TermNode> _terms = new Dictionary<string, TermNode>(StringComparer.OrdinalIgnoreCase);
1313

1414
public FilterResult(IReadOnlyDictionary<string, TTermOption> termOptions)
1515
{

src/YesSql.Filters.Abstractions/Services/TermOption.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ public TermOption(string name)
1717
/// </summary>
1818
public bool Single { get; set; } = true;
1919

20+
/// <summary>
21+
/// Whether this term filter should always run, even when not specified.
22+
/// </summary>
23+
public bool AlwaysRun { get; set; }
24+
2025
public Delegate MapTo { get; set; }
2126
public Delegate MapFrom { get; set; }
2227
public Func<string, string, TermNode> MapFromFactory { get; set; }

src/YesSql.Filters.Query/QueryEngineBuilder.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Collections.Generic;
23
using System.Linq;
34
using YesSql.Filters.Abstractions.Builders;
@@ -24,7 +25,7 @@ public IQueryParser<T> Build()
2425
var builders = _termBuilders.Values.Select(x => x.Build());
2526

2627
var parsers = builders.Select(x => x.Parser).ToArray();
27-
var termOptions = builders.Select(x => x.TermOption).ToDictionary(k => k.Name, v => v);
28+
var termOptions = builders.Select(x => x.TermOption).ToDictionary(k => k.Name, v => v, StringComparer.OrdinalIgnoreCase);
2829

2930
return new QueryParser<T>(parsers, termOptions);
3031
}

src/YesSql.Filters.Query/QueryFilterResult.cs

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,34 @@ public async ValueTask<IQuery<T>> ExecuteAsync(QueryExecutionContext<T> context)
3636
foreach (var term in _terms.Values)
3737
{
3838
// TODO optimize value task.
39-
context.CurrentTermOption = TermOptions[term.TermName];
40-
41-
var termQuery = visitor.Visit(term, context);
42-
context.Item = await termQuery.Invoke(context.Item);
43-
context.CurrentTermOption = null;
39+
await VisitTerm(TermOptions, context, visitor, term);
4440
}
4541

42+
// Execute always run terms. These are not added to the terms list.
43+
foreach (var termOption in TermOptions)
44+
{
45+
if (!termOption.Value.AlwaysRun)
46+
{
47+
continue;
48+
}
49+
50+
if (!_terms.ContainsKey(termOption.Key))
51+
{
52+
var alwaysRunNode = new NamedTermNode(termOption.Key, new UnaryNode(String.Empty));
53+
await VisitTerm(TermOptions, context, visitor, alwaysRunNode);
54+
}
55+
}
56+
4657
return context.Item;
4758
}
59+
60+
private async static Task VisitTerm(IReadOnlyDictionary<string, QueryTermOption<T>> termOptions, QueryExecutionContext<T> context, QueryFilterVisitor<T> visitor, TermNode term)
61+
{
62+
context.CurrentTermOption = termOptions[term.TermName];
63+
64+
var termQuery = visitor.Visit(term, context);
65+
context.Item = await termQuery.Invoke(context.Item);
66+
context.CurrentTermOption = null;
67+
}
4868
}
4969
}

0 commit comments

Comments
 (0)