-
Notifications
You must be signed in to change notification settings - Fork 11
/
SearchPostsQuery.cs
176 lines (149 loc) · 7.37 KB
/
SearchPostsQuery.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
using MediatR;
using Microsoft.Extensions.Options;
using Opw.PineBlog.Entities;
using Opw.PineBlog.Files;
using Opw.PineBlog.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
// TODO: improve test coverage
namespace Opw.PineBlog.Posts.Search
{
/// <summary>
/// Query that searches posts.
/// </summary>
public class SearchPostsQuery : IRequest<Result<PostListModel>>
{
/// <summary>
/// The requested page.
/// </summary>
public int Page { get; set; } = 1;
/// <summary>
/// The number of items per page, if not set the BlogOptions.ItemsPerPage will be used.
/// </summary>
public int? ItemsPerPage { get; set; }
/// <summary>
/// Search query.
/// </summary>
public string SearchQuery { get; set; }
/// <summary>
/// Handler for the SearchPostsQuery.
/// </summary>
public class Handler : IRequestHandler<SearchPostsQuery, Result<PostListModel>>
{
private readonly IOptionsSnapshot<PineBlogOptions> _blogOptions;
private readonly IBlogUnitOfWork _uow;
private readonly IPostRanker _postRanker;
private readonly PostUrlHelper _postUrlHelper;
private readonly FileUrlHelper _fileUrlHelper;
/// <summary>
/// Implementation of SearchPostsQuery.Handler.
/// </summary>
/// <param name="uow">The blog unit of work.</param>
/// <param name="postRanker">Post ranker.</param>
/// <param name="blogOptions">The blog options.</param>
/// <param name="postUrlHelper">Post URL helper.</param>
/// <param name="fileUrlHelper">File URL helper.</param>
public Handler(
IBlogUnitOfWork uow,
IPostRanker postRanker,
IOptionsSnapshot<PineBlogOptions> blogOptions,
PostUrlHelper postUrlHelper,
FileUrlHelper fileUrlHelper)
{
_blogOptions = blogOptions;
_uow = uow;
_postRanker = postRanker;
_postUrlHelper = postUrlHelper;
_fileUrlHelper = fileUrlHelper;
}
/// <summary>
/// Handle the SearchPostsQuery request.
/// </summary>
/// <param name="request">The SearchPostsQuery request.</param>
/// <param name="cancellationToken">A cancellation token.</param>
public async Task<Result<PostListModel>> Handle(SearchPostsQuery request, CancellationToken cancellationToken)
{
var itemsPerPage = (request.ItemsPerPage.HasValue) ? request.ItemsPerPage : _blogOptions.Value.ItemsPerPage;
var pager = new Pager(request.Page, itemsPerPage.Value);
var pagingUrlPartFormat = _blogOptions.Value.PagingUrlPartFormat;
var predicates = new List<Expression<Func<Post, bool>>>();
predicates.Add(p => p.Published != null);
IEnumerable<Post> posts;
if (!string.IsNullOrWhiteSpace(request.SearchQuery))
{
predicates.Add(BuildSearchExpression(request.SearchQuery));
pagingUrlPartFormat += "&" + string.Format(_blogOptions.Value.SearchQueryUrlPartFormat, HttpUtility.UrlEncode(request.SearchQuery));
posts = await _uow.Posts.GetAsync(predicates, 0, int.MaxValue, cancellationToken);
posts = _postRanker.Rank(posts, request.SearchQuery);
posts = await GetPagedListAsync(posts, predicates, pager, pagingUrlPartFormat, cancellationToken);
}
else
{
posts = await GetPagedListAsync(predicates, pager, pagingUrlPartFormat, cancellationToken);
}
posts = posts.Select(p => _postUrlHelper.ReplaceUrlFormatWithBaseUrl(p));
var model = new PostListModel
{
Blog = new BlogModel(_blogOptions.Value),
PostListType = PostListType.Blog,
Posts = posts,
Pager = pager
};
model.Blog.CoverUrl = _fileUrlHelper.ReplaceUrlFormatWithBaseUrl(model.Blog.CoverUrl);
if (!string.IsNullOrWhiteSpace(request.SearchQuery))
{
model.PostListType = PostListType.Search;
model.SearchQuery = request.SearchQuery;
}
return Result<PostListModel>.Success(model);
}
private Expression<Func<Post, bool>> BuildSearchExpression(string query)
{
var parameterExp = Expression.Parameter(typeof(Post), "p");
Expression exp = null;
foreach (var term in query.ParseTerms())
{
exp = ConcatOr(exp, GetContainsExpression(nameof(Post.Title), term.Trim(), parameterExp));
exp = ConcatOr(exp, GetContainsExpression(nameof(Post.Description), term.Trim(), parameterExp));
exp = ConcatOr(exp, GetContainsExpression(nameof(Post.Categories), term.Trim(), parameterExp));
exp = ConcatOr(exp, GetContainsExpression(nameof(Post.Content), term.Trim(), parameterExp));
}
return Expression.Lambda<Func<Post, bool>>(exp, parameterExp);
}
private Expression ConcatOr(Expression exp1, Expression exp2)
{
if (exp1 == null)
exp1 = exp2;
else
exp1 = Expression.OrElse(exp1, exp2);
return exp1;
}
private Expression GetContainsExpression(string propertyName, string term, ParameterExpression parameterExp)
{
var propertyExp = Expression.Property(parameterExp, propertyName);
var method = typeof(string).GetMethod("Contains", new[] { typeof(string) });
var someValue = Expression.Constant(term, typeof(string));
return Expression.Call(propertyExp, method, someValue);
}
private async Task<IEnumerable<Post>> GetPagedListAsync(IEnumerable<Expression<Func<Post, bool>>> predicates, Pager pager, string pagingUrlPartFormat, CancellationToken cancellationToken)
{
var skip = (pager.CurrentPage - 1) * pager.ItemsPerPage;
var count = await _uow.Posts.CountAsync(predicates, cancellationToken);
pager.Configure(count, pagingUrlPartFormat);
return await _uow.Posts.GetAsync(predicates, skip, pager.ItemsPerPage, cancellationToken);
}
private async Task<IEnumerable<Post>> GetPagedListAsync(IEnumerable<Post> posts, IEnumerable<Expression<Func<Post, bool>>> predicates, Pager pager, string pagingUrlPartFormat, CancellationToken cancellationToken)
{
var skip = (pager.CurrentPage - 1) * pager.ItemsPerPage;
var count = await _uow.Posts.CountAsync(predicates, cancellationToken);
pager.Configure(count, pagingUrlPartFormat);
return posts.Skip(skip).Take(pager.ItemsPerPage);
}
}
}
}