mirror of
https://github.com/Biarity/Sieve.git
synced 2024-11-22 13:32:33 +01:00
Merge branch 'master' into master
This commit is contained in:
commit
b992778f0d
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Feature Request
|
||||
url: https://github.com/biarity/sieve/discussions/new
|
||||
about: Share your ideas on how to make Sieve better.
|
32
.github/workflows/ci.yml
vendored
Normal file
32
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
# ------------------------------------------------------------------------------
|
||||
# <auto-generated>
|
||||
#
|
||||
# This code was generated.
|
||||
#
|
||||
# - To turn off auto-generation set:
|
||||
#
|
||||
# [GitHubActions (AutoGenerate = false)]
|
||||
#
|
||||
# - To trigger manual generation invoke:
|
||||
#
|
||||
# nuke --generate-configuration GitHubActions_ci --host GitHubActions
|
||||
#
|
||||
# </auto-generated>
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
name: ci
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- 'releases/*'
|
||||
|
||||
jobs:
|
||||
ubuntu-latest:
|
||||
name: ubuntu-latest
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Run './build.cmd Ci'
|
||||
run: ./build.cmd Ci
|
35
.github/workflows/ci_publish.yml
vendored
Normal file
35
.github/workflows/ci_publish.yml
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
# ------------------------------------------------------------------------------
|
||||
# <auto-generated>
|
||||
#
|
||||
# This code was generated.
|
||||
#
|
||||
# - To turn off auto-generation set:
|
||||
#
|
||||
# [GitHubActions (AutoGenerate = false)]
|
||||
#
|
||||
# - To trigger manual generation invoke:
|
||||
#
|
||||
# nuke --generate-configuration GitHubActions_ci_publish --host GitHubActions
|
||||
#
|
||||
# </auto-generated>
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
name: ci_publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'releases/*'
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
ubuntu-latest:
|
||||
name: ubuntu-latest
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Run './build.cmd CiPublish'
|
||||
run: ./build.cmd CiPublish
|
||||
env:
|
||||
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
|
8
.gitignore
vendored
8
.gitignore
vendored
@ -260,4 +260,10 @@ paket-files/
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyc
|
||||
|
||||
# Nuke output
|
||||
/output
|
||||
|
||||
# Sample database
|
||||
Sieve.Sample/Sieve.db
|
||||
|
115
.nuke/build.schema.json
Normal file
115
.nuke/build.schema.json
Normal file
@ -0,0 +1,115 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "Build Schema",
|
||||
"$ref": "#/definitions/build",
|
||||
"definitions": {
|
||||
"build": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Configuration": {
|
||||
"type": "string",
|
||||
"description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)",
|
||||
"enum": [
|
||||
"Debug",
|
||||
"Release"
|
||||
]
|
||||
},
|
||||
"Continue": {
|
||||
"type": "boolean",
|
||||
"description": "Indicates to continue a previously failed build attempt"
|
||||
},
|
||||
"Help": {
|
||||
"type": "boolean",
|
||||
"description": "Shows the help text for this build assembly"
|
||||
},
|
||||
"Host": {
|
||||
"type": "string",
|
||||
"description": "Host for execution. Default is 'automatic'",
|
||||
"enum": [
|
||||
"AppVeyor",
|
||||
"AzurePipelines",
|
||||
"Bamboo",
|
||||
"Bitrise",
|
||||
"GitHubActions",
|
||||
"GitLab",
|
||||
"Jenkins",
|
||||
"SpaceAutomation",
|
||||
"TeamCity",
|
||||
"Terminal",
|
||||
"TravisCI"
|
||||
]
|
||||
},
|
||||
"NoLogo": {
|
||||
"type": "boolean",
|
||||
"description": "Disables displaying the NUKE logo"
|
||||
},
|
||||
"NUGET_API_KEY": {
|
||||
"type": "string"
|
||||
},
|
||||
"Plan": {
|
||||
"type": "boolean",
|
||||
"description": "Shows the execution plan (HTML)"
|
||||
},
|
||||
"Profile": {
|
||||
"type": "array",
|
||||
"description": "Defines the profiles to load",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"Root": {
|
||||
"type": "string",
|
||||
"description": "Root directory during build execution"
|
||||
},
|
||||
"Skip": {
|
||||
"type": "array",
|
||||
"description": "List of targets to be skipped. Empty list skips all dependencies",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Ci",
|
||||
"CiPublish",
|
||||
"Clean",
|
||||
"Compile",
|
||||
"Package",
|
||||
"Publish",
|
||||
"Restore",
|
||||
"Test"
|
||||
]
|
||||
}
|
||||
},
|
||||
"Solution": {
|
||||
"type": "string",
|
||||
"description": "Path to a solution file that is automatically loaded"
|
||||
},
|
||||
"Target": {
|
||||
"type": "array",
|
||||
"description": "List of targets to be invoked. Default is '{default_target}'",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Ci",
|
||||
"CiPublish",
|
||||
"Clean",
|
||||
"Compile",
|
||||
"Package",
|
||||
"Publish",
|
||||
"Restore",
|
||||
"Test"
|
||||
]
|
||||
}
|
||||
},
|
||||
"Verbosity": {
|
||||
"type": "string",
|
||||
"description": "Logging verbosity during build execution. Default is 'Normal'",
|
||||
"enum": [
|
||||
"Minimal",
|
||||
"Normal",
|
||||
"Quiet",
|
||||
"Verbose"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
4
.nuke/parameters.json
Normal file
4
.nuke/parameters.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "./build.schema.json",
|
||||
"Solution": "Sieve.sln"
|
||||
}
|
3
GitVersion.yml
Normal file
3
GitVersion.yml
Normal file
@ -0,0 +1,3 @@
|
||||
branches:
|
||||
release:
|
||||
mode: ContinuousDeployment
|
190
LICENSE
190
LICENSE
@ -1,192 +1,4 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
Copyright 2018 Biarity, 2021 Ashish Patel and Kevin Sommer
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
80
README.md
80
README.md
@ -2,7 +2,8 @@
|
||||
⚗️ Sieve is a simple, clean, and extensible framework for .NET Core that **adds sorting, filtering, and pagination functionality out of the box**.
|
||||
Most common use case would be for serving ASP.NET Core GET queries.
|
||||
|
||||
[![NuGet Release](https://img.shields.io/nuget/v/Sieve.svg?style=flat-square)](https://www.nuget.org/packages/Sieve)
|
||||
[![NuGet Release](https://img.shields.io/nuget/v/Sieve?style=for-the-badge)](https://www.nuget.org/packages/Sieve)
|
||||
[![NuGet Pre-Release](https://img.shields.io/nuget/vpre/Sieve?style=for-the-badge)](https://www.nuget.org/packages/Sieve)
|
||||
|
||||
[Get Sieve on nuget](https://www.nuget.org/packages/Sieve/)
|
||||
|
||||
@ -74,7 +75,7 @@ Where `SieveCustomSortMethodsOfPosts` for example is:
|
||||
```C#
|
||||
public class SieveCustomSortMethods : ISieveCustomSortMethods
|
||||
{
|
||||
public IQueryable<Post> Popularity(IQueryable<Post> source, bool useThenBy, bool desc) // The method is given an indicator of weather to use ThenBy(), and if the query is descending
|
||||
public IQueryable<Post> Popularity(IQueryable<Post> source, bool useThenBy, bool desc) // The method is given an indicator of whether to use ThenBy(), and if the query is descending
|
||||
{
|
||||
var result = useThenBy ?
|
||||
((IOrderedQueryable<Post>)source).ThenBy(p => p.LikeCount) : // ThenBy only works on IOrderedQueryable<TEntity>
|
||||
@ -127,7 +128,8 @@ Then you can add the configuration:
|
||||
"CaseSensitive": "boolean: should property names be case-sensitive? Defaults to false",
|
||||
"DefaultPageSize": "int number: optional number to fallback to when no page argument is given. Set <=0 to disable paging if no pageSize is specified (default).",
|
||||
"MaxPageSize": "int number: maximum allowed page size. Set <=0 to make infinite (default)",
|
||||
"ThrowExceptions": "boolean: should Sieve throw exceptions instead of silently failing? Defaults to false"
|
||||
"ThrowExceptions": "boolean: should Sieve throw exceptions instead of silently failing? Defaults to false",
|
||||
"IgnoreNullsOnNotEqual": "boolean: ignore null values when filtering using is not equal operator? Default to true"
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -157,7 +159,10 @@ More formally:
|
||||
* `pageSize` is the number of items returned per page
|
||||
|
||||
Notes:
|
||||
* You can use backslashes to escape commas and pipes within value fields
|
||||
* You can use backslashes to escape special characters and sequences:
|
||||
* commas: `Title@=some\,title` makes a match with "some,title"
|
||||
* pipes: `Title@=some\|title` makes a match with "some|title"
|
||||
* null values: `Title@=\null` will search for items with title equal to "null" (not a missing value, but "null"-string literally)
|
||||
* You can have spaces anywhere except *within* `{Name}` or `{Operator}` fields
|
||||
* If you need to look at the data before applying pagination (eg. get total count), use the optional paramters on `Apply` to defer pagination (an [example](https://github.com/Biarity/Sieve/issues/34))
|
||||
* Here's a [good example on how to work with enumerables](https://github.com/Biarity/Sieve/issues/2)
|
||||
@ -258,13 +263,78 @@ public class ApplicationSieveProcessor : SieveProcessor
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
Now you should inject the new class instead:
|
||||
```C#
|
||||
services.AddScoped<ISieveProcessor, ApplicationSieveProcessor>();
|
||||
```
|
||||
|
||||
Find More on Sieve's Fluent API [here](https://github.com/Biarity/Sieve/issues/4#issuecomment-364629048).
|
||||
|
||||
### Modular Fluent API configuration
|
||||
Adding all fluent mappings directly in the processor can become unwieldy on larger projects.
|
||||
It can also clash with vertical architectures.
|
||||
To enable functional grouping of mappings the `ISieveConfiguration` interface was created together with extensions to the default mapper.
|
||||
```C#
|
||||
public class SieveConfigurationForPost : ISieveConfiguration
|
||||
{
|
||||
protected override SievePropertyMapper Configure(SievePropertyMapper mapper)
|
||||
{
|
||||
mapper.Property<Post>(p => p.Title)
|
||||
.CanFilter()
|
||||
.HasName("a_different_query_name_here");
|
||||
|
||||
mapper.Property<Post>(p => p.CommentCount)
|
||||
.CanSort();
|
||||
|
||||
mapper.Property<Post>(p => p.DateCreated)
|
||||
.CanSort()
|
||||
.CanFilter()
|
||||
.HasName("created_on");
|
||||
|
||||
return mapper;
|
||||
}
|
||||
}
|
||||
```
|
||||
With the processor simplified to:
|
||||
```C#
|
||||
public class ApplicationSieveProcessor : SieveProcessor
|
||||
{
|
||||
public ApplicationSieveProcessor(
|
||||
IOptions<SieveOptions> options,
|
||||
ISieveCustomSortMethods customSortMethods,
|
||||
ISieveCustomFilterMethods customFilterMethods)
|
||||
: base(options, customSortMethods, customFilterMethods)
|
||||
{
|
||||
}
|
||||
|
||||
protected override SievePropertyMapper MapProperties(SievePropertyMapper mapper)
|
||||
{
|
||||
return mapper
|
||||
.ApplyConfiguration<SieveConfigurationForPost>()
|
||||
.ApplyConfiguration<SieveConfigurationForComment>();
|
||||
}
|
||||
}
|
||||
```
|
||||
There is also the option to scan and add all configurations for a given assembly
|
||||
```C#
|
||||
public class ApplicationSieveProcessor : SieveProcessor
|
||||
{
|
||||
public ApplicationSieveProcessor(
|
||||
IOptions<SieveOptions> options,
|
||||
ISieveCustomSortMethods customSortMethods,
|
||||
ISieveCustomFilterMethods customFilterMethods)
|
||||
: base(options, customSortMethods, customFilterMethods)
|
||||
{
|
||||
}
|
||||
|
||||
protected override SievePropertyMapper MapProperties(SievePropertyMapper mapper)
|
||||
{
|
||||
return mapper.ApplyConfigurationForAssembly(typeof(ApplicationSieveProcessor).Assembly);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Upgrading to v2.2.0
|
||||
|
||||
2.2.0 introduced OR logic for filter values. This means your custom filters will need to accept multiple values rather than just the one.
|
||||
|
@ -2,10 +2,10 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Sieve.Models;
|
||||
using Sieve.Sample.Entities;
|
||||
using Sieve.Services;
|
||||
using SieveTests.Entities;
|
||||
|
||||
namespace SieveTests.Controllers
|
||||
namespace Sieve.Sample.Controllers
|
||||
{
|
||||
[Route("api/[controller]/[action]")]
|
||||
public class PostsController : Controller
|
@ -1,6 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace SieveTests.Entities
|
||||
namespace Sieve.Sample.Entities
|
||||
{
|
||||
public class ApplicationDbContext : DbContext
|
||||
{
|
@ -2,14 +2,14 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Sieve.Attributes;
|
||||
|
||||
namespace SieveTests.Entities
|
||||
namespace Sieve.Sample.Entities
|
||||
{
|
||||
public class Post
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
[Sieve(CanFilter = true, CanSort = true)]
|
||||
public string Title { get; set; } = Guid.NewGuid().ToString().Replace("-", string.Empty).Substring(0, 8);
|
||||
public string Title { get; set; } = Guid.NewGuid().ToString().Replace("-", string.Empty)[..8];
|
||||
|
||||
[Sieve(CanFilter = true, CanSort = true)]
|
||||
public int LikeCount { get; set; } = new Random().Next(0, 1000);
|
15
Sieve.Sample/Entities/SieveConfigurationForPost.cs
Normal file
15
Sieve.Sample/Entities/SieveConfigurationForPost.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using Sieve.Services;
|
||||
|
||||
namespace Sieve.Sample.Entities
|
||||
{
|
||||
public class SieveConfigurationForPost : ISieveConfiguration
|
||||
{
|
||||
public void Configure(SievePropertyMapper mapper)
|
||||
{
|
||||
mapper.Property<Post>(p => p.Title)
|
||||
.CanSort()
|
||||
.CanFilter()
|
||||
.HasName("CustomTitleName");
|
||||
}
|
||||
}
|
||||
}
|
52
Sieve.Sample/Migrations/20210513114647_Initial.Designer.cs
generated
Normal file
52
Sieve.Sample/Migrations/20210513114647_Initial.Designer.cs
generated
Normal file
@ -0,0 +1,52 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Sieve.Sample.Entities;
|
||||
|
||||
namespace Sieve.Sample.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20210513114647_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "3.1.14");
|
||||
|
||||
modelBuilder.Entity("Sieve.Sample.Entities.Post", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("CategoryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("CommentCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset>("DateCreated")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateLastViewed")
|
||||
.HasColumnType("datetime");
|
||||
|
||||
b.Property<int>("LikeCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Posts");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,9 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace SieveTests.Migrations
|
||||
namespace Sieve.Sample.Migrations
|
||||
{
|
||||
public partial class Init : Migration
|
||||
public partial class Initial : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
@ -13,14 +12,18 @@ namespace SieveTests.Migrations
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(nullable: false)
|
||||
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Title = table.Column<string>(nullable: true),
|
||||
LikeCount = table.Column<int>(nullable: false),
|
||||
CommentCount = table.Column<int>(nullable: false),
|
||||
DateCreated = table.Column<DateTimeOffset>(nullable: false),
|
||||
LikeCount = table.Column<int>(nullable: false),
|
||||
Title = table.Column<string>(nullable: true),
|
||||
DateLastViewed = table.Column<DateTime>(type: "datetime", nullable: false),
|
||||
CategoryId = table.Column<int>(nullable: true)
|
||||
},
|
||||
constraints: table => table.PrimaryKey("PK_Posts", x => x.Id));
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Posts", x => x.Id);
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
50
Sieve.Sample/Migrations/ApplicationDbContextModelSnapshot.cs
Normal file
50
Sieve.Sample/Migrations/ApplicationDbContextModelSnapshot.cs
Normal file
@ -0,0 +1,50 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Sieve.Sample.Entities;
|
||||
|
||||
namespace Sieve.Sample.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "3.1.14");
|
||||
|
||||
modelBuilder.Entity("Sieve.Sample.Entities.Post", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("CategoryId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("CommentCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset>("DateCreated")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("DateLastViewed")
|
||||
.HasColumnType("datetime");
|
||||
|
||||
b.Property<int>("LikeCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Posts");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
using Microsoft.AspNetCore;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
|
||||
namespace SieveTests
|
||||
namespace Sieve.Sample
|
||||
{
|
||||
public static class Program
|
||||
{
|
@ -1,9 +1,9 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using Sieve.Models;
|
||||
using Sieve.Sample.Entities;
|
||||
using Sieve.Services;
|
||||
using SieveTests.Entities;
|
||||
|
||||
namespace SieveTests.Services
|
||||
namespace Sieve.Sample.Services
|
||||
{
|
||||
public class ApplicationSieveProcessor : SieveProcessor
|
||||
{
|
||||
@ -13,11 +13,18 @@ namespace SieveTests.Services
|
||||
|
||||
protected override SievePropertyMapper MapProperties(SievePropertyMapper mapper)
|
||||
{
|
||||
// Option 1: Map all properties centrally
|
||||
mapper.Property<Post>(p => p.Title)
|
||||
.CanSort()
|
||||
.CanFilter()
|
||||
.HasName("CustomTitleName");
|
||||
|
||||
// Option 2: Manually apply functionally grouped mapping configurations
|
||||
//mapper.ApplyConfiguration<SieveConfigurationForPost>();
|
||||
|
||||
// Option 3: Scan and apply all configurations
|
||||
//mapper.ApplyConfigurationsFromAssembly(typeof(ApplicationSieveProcessor).Assembly);
|
||||
|
||||
return mapper;
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
using System.Linq;
|
||||
using Sieve.Sample.Entities;
|
||||
using Sieve.Services;
|
||||
using SieveTests.Entities;
|
||||
|
||||
namespace SieveTests.Services
|
||||
namespace Sieve.Sample.Services
|
||||
{
|
||||
public class SieveCustomFilterMethods : ISieveCustomFilterMethods
|
||||
{
|
@ -1,8 +1,8 @@
|
||||
using System.Linq;
|
||||
using Sieve.Sample.Entities;
|
||||
using Sieve.Services;
|
||||
using SieveTests.Entities;
|
||||
|
||||
namespace SieveTests.Services
|
||||
namespace Sieve.Sample.Services
|
||||
{
|
||||
public class SieveCustomSortMethods : ISieveCustomSortMethods
|
||||
{
|
20
Sieve.Sample/Sieve.Sample.csproj
Normal file
20
Sieve.Sample/Sieve.Sample.csproj
Normal file
@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.14">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.14" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.14" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SieveUnitTests\SieveUnitTests.csproj" />
|
||||
<ProjectReference Include="..\Sieve\Sieve.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
60
Sieve.Sample/Startup.cs
Normal file
60
Sieve.Sample/Startup.cs
Normal file
@ -0,0 +1,60 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Sieve.Models;
|
||||
using Sieve.Sample.Entities;
|
||||
using Sieve.Sample.Services;
|
||||
using Sieve.Services;
|
||||
|
||||
namespace Sieve.Sample
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
public Startup(IConfiguration configuration)
|
||||
{
|
||||
Configuration = configuration;
|
||||
}
|
||||
|
||||
public IConfiguration Configuration { get; }
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddMvc(opts =>
|
||||
{
|
||||
opts.EnableEndpointRouting = false;
|
||||
});
|
||||
|
||||
services.AddDbContext<ApplicationDbContext>(options =>
|
||||
options.UseSqlite("Data Source=.\\sieve.db"));
|
||||
|
||||
services.Configure<SieveOptions>(Configuration.GetSection("Sieve"));
|
||||
|
||||
services.AddScoped<ISieveCustomSortMethods, SieveCustomSortMethods>();
|
||||
services.AddScoped<ISieveCustomFilterMethods, SieveCustomFilterMethods>();
|
||||
services.AddScoped<ISieveProcessor, ApplicationSieveProcessor>();
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
{
|
||||
PrepareDatabase(app);
|
||||
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
}
|
||||
|
||||
app.UseMvc();
|
||||
}
|
||||
|
||||
private static void PrepareDatabase(IApplicationBuilder app)
|
||||
{
|
||||
using var scope = app.ApplicationServices.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||
dbContext.Database.EnsureDeleted();
|
||||
dbContext.Database.Migrate();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
{
|
||||
"Logging": {
|
||||
"IncludeScopes": false,
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"System": "Information",
|
@ -4,10 +4,10 @@
|
||||
},
|
||||
"Sieve": {
|
||||
"CaseSensitive": false,
|
||||
"DefaultPageSize": 10
|
||||
"DefaultPageSize": 10,
|
||||
"IgnoreNullsOnNotEqual": true
|
||||
},
|
||||
"Logging": {
|
||||
"IncludeScopes": false,
|
||||
"Debug": {
|
||||
"LogLevel": {
|
||||
"Default": "Warning"
|
@ -5,7 +5,7 @@ VisualStudioVersion = 15.0.27130.2027
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sieve", "Sieve\Sieve.csproj", "{B32B8B33-94B0-40E3-8FE5-D54602222717}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SieveTests", "SieveTests\SieveTests.csproj", "{8043D264-42A0-4275-97A1-46400C02E37E}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sieve.Sample", "Sieve.Sample\Sieve.Sample.csproj", "{8043D264-42A0-4275-97A1-46400C02E37E}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SieveUnitTests", "SieveUnitTests\SieveUnitTests.csproj", "{21C3082D-F40E-457F-BE2E-AA099E19E199}"
|
||||
EndProject
|
||||
@ -15,12 +15,16 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
||||
README.md = README.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "build\_build.csproj", "{C8829BE9-BC1E-4C67-ACEA-EC5DD3633EE2}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{C8829BE9-BC1E-4C67-ACEA-EC5DD3633EE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C8829BE9-BC1E-4C67-ACEA-EC5DD3633EE2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B32B8B33-94B0-40E3-8FE5-D54602222717}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B32B8B33-94B0-40E3-8FE5-D54602222717}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B32B8B33-94B0-40E3-8FE5-D54602222717}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
@ -6,45 +7,49 @@ namespace Sieve.Models
|
||||
{
|
||||
public class FilterTerm : IFilterTerm, IEquatable<FilterTerm>
|
||||
{
|
||||
public FilterTerm() { }
|
||||
private const string EscapedPipePattern = @"(?<!($|[^\\]|^)(\\\\)*?\\)\|";
|
||||
private const string OperatorsRegEx = @"(!@=\*|!_=\*|!=\*|!@=|!_=|==\*|@=\*|_=\*|==|!=|>=|<=|>|<|@=|_=)";
|
||||
private const string EscapeNegPatternForOper = @"(?<!\\)" + OperatorsRegEx;
|
||||
private const string EscapePosPatternForOper = @"(?<=\\)" + OperatorsRegEx;
|
||||
|
||||
private const string EscapedPipePattern = @"(?<!($|[^\\])(\\\\)*?\\)\|";
|
||||
|
||||
private static readonly string[] Operators = new string[] {
|
||||
"!@=*",
|
||||
"!_=*",
|
||||
"!=*",
|
||||
"!@=",
|
||||
"!_=",
|
||||
"==*",
|
||||
"@=*",
|
||||
"_=*",
|
||||
"==",
|
||||
"!=",
|
||||
">=",
|
||||
"<=",
|
||||
">",
|
||||
"<",
|
||||
"@=",
|
||||
"_="
|
||||
private static readonly HashSet<string> _escapedSequences = new HashSet<string>
|
||||
{
|
||||
@"\|",
|
||||
@"\\"
|
||||
};
|
||||
|
||||
public string Filter
|
||||
{
|
||||
set
|
||||
{
|
||||
var filterSplits = value.Split(Operators, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(t => t.Trim()).ToArray();
|
||||
var filterSplits = Regex.Split(value,EscapeNegPatternForOper).Select(t => t.Trim()).ToArray();
|
||||
|
||||
Names = Regex.Split(filterSplits[0], EscapedPipePattern).Select(t => t.Trim()).ToArray();
|
||||
Values = filterSplits.Length > 1 ? Regex.Split(filterSplits[1], EscapedPipePattern).Select(t => t.Trim()).ToArray() : null;
|
||||
Operator = Array.Find(Operators, o => value.Contains(o)) ?? "==";
|
||||
|
||||
if (filterSplits.Length > 2)
|
||||
{
|
||||
foreach (var match in Regex.Matches(filterSplits[2],EscapePosPatternForOper))
|
||||
{
|
||||
var matchStr = match.ToString();
|
||||
filterSplits[2] = filterSplits[2].Replace('\\' + matchStr, matchStr);
|
||||
}
|
||||
|
||||
Values = Regex.Split(filterSplits[2], EscapedPipePattern)
|
||||
.Select(UnEscape)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
Operator = Regex.Match(value,EscapeNegPatternForOper).Value;
|
||||
OperatorParsed = GetOperatorParsed(Operator);
|
||||
OperatorIsCaseInsensitive = Operator.EndsWith("*");
|
||||
OperatorIsNegated = OperatorParsed != FilterOperator.NotEquals && Operator.StartsWith("!");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private string UnEscape(string escapedTerm)
|
||||
=> _escapedSequences.Aggregate(escapedTerm,
|
||||
(current, sequence) => Regex.Replace(current, $@"(\\)({sequence})", "$2"));
|
||||
|
||||
public string[] Names { get; private set; }
|
||||
|
||||
public FilterOperator OperatorParsed { get; private set; }
|
||||
@ -90,6 +95,5 @@ namespace Sieve.Models
|
||||
&& Values.SequenceEqual(other.Values)
|
||||
&& Operator == other.Operator;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
@ -13,7 +14,15 @@ namespace Sieve.Models
|
||||
where TFilterTerm : IFilterTerm, new()
|
||||
where TSortTerm : ISortTerm, new()
|
||||
{
|
||||
/// <summary>
|
||||
/// Pattern used to split filters and sorts by comma.
|
||||
/// </summary>
|
||||
private const string EscapedCommaPattern = @"(?<!($|[^\\])(\\\\)*?\\),\s*";
|
||||
|
||||
/// <summary>
|
||||
/// Escaped comma e.g. used in filter filter string.
|
||||
/// </summary>
|
||||
private const string EscapedComma = @"\,";
|
||||
|
||||
[DataMember]
|
||||
public string Filters { get; set; }
|
||||
@ -34,15 +43,20 @@ namespace Sieve.Models
|
||||
var value = new List<TFilterTerm>();
|
||||
foreach (var filter in Regex.Split(Filters, EscapedCommaPattern))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(filter)) continue;
|
||||
if (string.IsNullOrWhiteSpace(filter))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var filterValue = filter.Replace(EscapedComma, ",");
|
||||
|
||||
if (filter.StartsWith("("))
|
||||
{
|
||||
var filterOpAndVal = filter.Substring(filter.LastIndexOf(")") + 1);
|
||||
var subfilters = filter.Replace(filterOpAndVal, "").Replace("(", "").Replace(")", "");
|
||||
var filterOpAndVal = filterValue[(filterValue.LastIndexOf(")", StringComparison.Ordinal) + 1)..];
|
||||
var subFilters = filterValue.Replace(filterOpAndVal, "").Replace("(", "").Replace(")", "");
|
||||
var filterTerm = new TFilterTerm
|
||||
{
|
||||
Filter = subfilters + filterOpAndVal
|
||||
Filter = subFilters + filterOpAndVal
|
||||
};
|
||||
value.Add(filterTerm);
|
||||
}
|
||||
@ -50,7 +64,7 @@ namespace Sieve.Models
|
||||
{
|
||||
var filterTerm = new TFilterTerm
|
||||
{
|
||||
Filter = filter
|
||||
Filter = filterValue
|
||||
};
|
||||
value.Add(filterTerm);
|
||||
}
|
||||
@ -65,29 +79,28 @@ namespace Sieve.Models
|
||||
|
||||
public List<TSortTerm> GetSortsParsed()
|
||||
{
|
||||
if (Sorts != null)
|
||||
{
|
||||
var value = new List<TSortTerm>();
|
||||
foreach (var sort in Regex.Split(Sorts, EscapedCommaPattern))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sort)) continue;
|
||||
|
||||
var sortTerm = new TSortTerm()
|
||||
{
|
||||
Sort = sort
|
||||
};
|
||||
if (!value.Any(s => s.Name == sortTerm.Name))
|
||||
{
|
||||
value.Add(sortTerm);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
else
|
||||
if (Sorts == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var value = new List<TSortTerm>();
|
||||
foreach (var sort in Regex.Split(Sorts, EscapedCommaPattern))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sort)) continue;
|
||||
|
||||
var sortTerm = new TSortTerm
|
||||
{
|
||||
Sort = sort
|
||||
};
|
||||
|
||||
if (value.All(s => s.Name != sortTerm.Name))
|
||||
{
|
||||
value.Add(sortTerm);
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
namespace Sieve.Models
|
||||
namespace Sieve.Models
|
||||
{
|
||||
public class SieveOptions
|
||||
{
|
||||
@ -10,6 +10,8 @@
|
||||
|
||||
public bool ThrowExceptions { get; set; } = false;
|
||||
|
||||
public bool IgnoreNullsOnNotEqual { get; set; } = true;
|
||||
|
||||
public string CultureNameOfTypeConversion { get; set; } = "en";
|
||||
}
|
||||
}
|
||||
|
70
Sieve/Services/ISieveConfiguration.cs
Normal file
70
Sieve/Services/ISieveConfiguration.cs
Normal file
@ -0,0 +1,70 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Sieve.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Use this interface to create SieveConfiguration (just like EntityTypeConfigurations are defined for EF)
|
||||
/// </summary>
|
||||
public interface ISieveConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures sieve property mappings.
|
||||
/// </summary>
|
||||
/// <param name="mapper"> The mapper used to configure the sieve properties on. </param>
|
||||
void Configure(SievePropertyMapper mapper);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration extensions to the <see cref="SievePropertyMapper" />
|
||||
/// </summary>
|
||||
public static class SieveConfigurationExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies configuration that is defined in an <see cref="ISieveConfiguration" /> instance.
|
||||
/// </summary>
|
||||
/// <param name="mapper"> The mapper to apply the configuration on. </param>
|
||||
/// <typeparam name="T">The configuration to be applied. </typeparam>
|
||||
/// <returns>
|
||||
/// The same <see cref="SievePropertyMapper" /> instance so that additional configuration calls can be chained.
|
||||
/// </returns>
|
||||
public static SievePropertyMapper ApplyConfiguration<T>(this SievePropertyMapper mapper) where T : ISieveConfiguration, new()
|
||||
{
|
||||
var configuration = new T();
|
||||
configuration.Configure(mapper);
|
||||
return mapper;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies configuration from all <see cref="ISieveConfiguration" />
|
||||
/// instances that are defined in provided assembly.
|
||||
/// </summary>
|
||||
/// <param name="mapper"> The mapper to apply the configuration on. </param>
|
||||
/// <param name="assembly"> The assembly to scan. </param>
|
||||
/// <returns>
|
||||
/// The same <see cref="SievePropertyMapper" /> instance so that additional configuration calls can be chained.
|
||||
/// </returns>
|
||||
public static SievePropertyMapper ApplyConfigurationsFromAssembly(this SievePropertyMapper mapper, Assembly assembly)
|
||||
{
|
||||
foreach (var type in assembly.GetTypes().Where(t => !t.IsAbstract && !t.IsGenericTypeDefinition))
|
||||
{
|
||||
// Only accept types that contain a parameterless constructor, are not abstract.
|
||||
var noArgConstructor = type.GetConstructor(Type.EmptyTypes);
|
||||
if (noArgConstructor is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (type.GetInterfaces().Any(t => t == typeof(ISieveConfiguration)))
|
||||
{
|
||||
var configuration = (ISieveConfiguration)noArgConstructor.Invoke(new object?[] { });
|
||||
configuration.Configure(mapper);
|
||||
}
|
||||
}
|
||||
|
||||
return mapper;
|
||||
}
|
||||
}
|
||||
}
|
@ -15,40 +15,50 @@ namespace Sieve.Services
|
||||
{
|
||||
public class SieveProcessor : SieveProcessor<SieveModel, FilterTerm, SortTerm>, ISieveProcessor
|
||||
{
|
||||
public SieveProcessor(IOptions<SieveOptions> options) : base(options)
|
||||
public SieveProcessor(IOptions<SieveOptions> options)
|
||||
: base(options)
|
||||
{
|
||||
}
|
||||
|
||||
public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomSortMethods customSortMethods) : base(options, customSortMethods)
|
||||
public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomSortMethods customSortMethods)
|
||||
: base(options, customSortMethods)
|
||||
{
|
||||
}
|
||||
|
||||
public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomFilterMethods customFilterMethods) : base(options, customFilterMethods)
|
||||
public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomFilterMethods customFilterMethods)
|
||||
: base(options, customFilterMethods)
|
||||
{
|
||||
}
|
||||
|
||||
public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomSortMethods customSortMethods, ISieveCustomFilterMethods customFilterMethods) : base(options, customSortMethods, customFilterMethods)
|
||||
public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomSortMethods customSortMethods,
|
||||
ISieveCustomFilterMethods customFilterMethods) : base(options, customSortMethods, customFilterMethods)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class SieveProcessor<TFilterTerm, TSortTerm> : SieveProcessor<SieveModel<TFilterTerm, TSortTerm>, TFilterTerm, TSortTerm>, ISieveProcessor<TFilterTerm, TSortTerm>
|
||||
public class SieveProcessor<TFilterTerm, TSortTerm> :
|
||||
SieveProcessor<SieveModel<TFilterTerm, TSortTerm>, TFilterTerm, TSortTerm>, ISieveProcessor<TFilterTerm, TSortTerm>
|
||||
where TFilterTerm : IFilterTerm, new()
|
||||
where TSortTerm : ISortTerm, new()
|
||||
{
|
||||
public SieveProcessor(IOptions<SieveOptions> options) : base(options)
|
||||
public SieveProcessor(IOptions<SieveOptions> options)
|
||||
: base(options)
|
||||
{
|
||||
}
|
||||
|
||||
public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomSortMethods customSortMethods) : base(options, customSortMethods)
|
||||
public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomSortMethods customSortMethods)
|
||||
: base(options, customSortMethods)
|
||||
{
|
||||
}
|
||||
|
||||
public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomFilterMethods customFilterMethods) : base(options, customFilterMethods)
|
||||
public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomFilterMethods customFilterMethods)
|
||||
: base(options, customFilterMethods)
|
||||
{
|
||||
}
|
||||
|
||||
public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomSortMethods customSortMethods, ISieveCustomFilterMethods customFilterMethods) : base(options, customSortMethods, customFilterMethods)
|
||||
public SieveProcessor(IOptions<SieveOptions> options, ISieveCustomSortMethods customSortMethods,
|
||||
ISieveCustomFilterMethods customFilterMethods)
|
||||
: base(options, customSortMethods, customFilterMethods)
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -58,18 +68,18 @@ namespace Sieve.Services
|
||||
where TFilterTerm : IFilterTerm, new()
|
||||
where TSortTerm : ISortTerm, new()
|
||||
{
|
||||
private const string nullFilterValue = "null";
|
||||
private readonly IOptions<SieveOptions> _options;
|
||||
private const string NullFilterValue = "null";
|
||||
private const char EscapeChar = '\\';
|
||||
private readonly ISieveCustomSortMethods _customSortMethods;
|
||||
private readonly ISieveCustomFilterMethods _customFilterMethods;
|
||||
private readonly SievePropertyMapper mapper = new SievePropertyMapper();
|
||||
private readonly SievePropertyMapper _mapper = new SievePropertyMapper();
|
||||
|
||||
public SieveProcessor(IOptions<SieveOptions> options,
|
||||
ISieveCustomSortMethods customSortMethods,
|
||||
ISieveCustomFilterMethods customFilterMethods)
|
||||
{
|
||||
mapper = MapProperties(mapper);
|
||||
_options = options;
|
||||
_mapper = MapProperties(_mapper);
|
||||
Options = options;
|
||||
_customSortMethods = customSortMethods;
|
||||
_customFilterMethods = customFilterMethods;
|
||||
}
|
||||
@ -77,25 +87,27 @@ namespace Sieve.Services
|
||||
public SieveProcessor(IOptions<SieveOptions> options,
|
||||
ISieveCustomSortMethods customSortMethods)
|
||||
{
|
||||
mapper = MapProperties(mapper);
|
||||
_options = options;
|
||||
_mapper = MapProperties(_mapper);
|
||||
Options = options;
|
||||
_customSortMethods = customSortMethods;
|
||||
}
|
||||
|
||||
public SieveProcessor(IOptions<SieveOptions> options,
|
||||
ISieveCustomFilterMethods customFilterMethods)
|
||||
{
|
||||
mapper = MapProperties(mapper);
|
||||
_options = options;
|
||||
_mapper = MapProperties(_mapper);
|
||||
Options = options;
|
||||
_customFilterMethods = customFilterMethods;
|
||||
}
|
||||
|
||||
public SieveProcessor(IOptions<SieveOptions> options)
|
||||
{
|
||||
mapper = MapProperties(mapper);
|
||||
_options = options;
|
||||
_mapper = MapProperties(_mapper);
|
||||
Options = options;
|
||||
}
|
||||
|
||||
protected IOptions<SieveOptions> Options { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Apply filtering, sorting, and pagination parameters found in `model` to `source`
|
||||
/// </summary>
|
||||
@ -107,12 +119,8 @@ namespace Sieve.Services
|
||||
/// <param name="applySorting">Should the data be sorted? Defaults to true.</param>
|
||||
/// <param name="applyPagination">Should the data be paginated? Defaults to true.</param>
|
||||
/// <returns>Returns a transformed version of `source`</returns>
|
||||
public IQueryable<TEntity> Apply<TEntity>(
|
||||
TSieveModel model,
|
||||
IQueryable<TEntity> source,
|
||||
object[] dataForCustomMethods = null,
|
||||
bool applyFiltering = true,
|
||||
bool applySorting = true,
|
||||
public IQueryable<TEntity> Apply<TEntity>(TSieveModel model, IQueryable<TEntity> source,
|
||||
object[] dataForCustomMethods = null, bool applyFiltering = true, bool applySorting = true,
|
||||
bool applyPagination = true)
|
||||
{
|
||||
var result = source;
|
||||
@ -124,19 +132,16 @@ namespace Sieve.Services
|
||||
|
||||
try
|
||||
{
|
||||
// Filter
|
||||
if (applyFiltering)
|
||||
{
|
||||
result = ApplyFiltering(model, result, dataForCustomMethods);
|
||||
}
|
||||
|
||||
// Sort
|
||||
if (applySorting)
|
||||
{
|
||||
result = ApplySorting(model, result, dataForCustomMethods);
|
||||
}
|
||||
|
||||
// Paginate
|
||||
if (applyPagination)
|
||||
{
|
||||
result = ApplyPagination(model, result);
|
||||
@ -146,25 +151,21 @@ namespace Sieve.Services
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (_options.Value.ThrowExceptions)
|
||||
{
|
||||
if (ex is SieveException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
throw new SieveException(ex.Message, ex);
|
||||
}
|
||||
else
|
||||
if (!Options.Value.ThrowExceptions)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
if (ex is SieveException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
throw new SieveException(ex.Message, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private IQueryable<TEntity> ApplyFiltering<TEntity>(
|
||||
TSieveModel model,
|
||||
IQueryable<TEntity> result,
|
||||
protected virtual IQueryable<TEntity> ApplyFiltering<TEntity>(TSieveModel model, IQueryable<TEntity> result,
|
||||
object[] dataForCustomMethods = null)
|
||||
{
|
||||
if (model?.GetFiltersParsed() == null)
|
||||
@ -184,38 +185,33 @@ namespace Sieve.Services
|
||||
var (fullPropertyName, property) = GetSieveProperty<TEntity>(false, true, filterTermName);
|
||||
if (property != null)
|
||||
{
|
||||
Expression propertyValue = parameter;
|
||||
Expression nullCheck = null;
|
||||
var names = fullPropertyName.Split('.');
|
||||
for (var i = 0; i < names.Length; i++)
|
||||
if (filterTerm.Values == null)
|
||||
{
|
||||
propertyValue = Expression.PropertyOrField(propertyValue, names[i]);
|
||||
|
||||
if (i != names.Length - 1 && propertyValue.Type.IsNullable())
|
||||
{
|
||||
nullCheck = GenerateFilterNullCheckExpression(propertyValue, nullCheck);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (filterTerm.Values == null) continue;
|
||||
|
||||
var converter = TypeDescriptor.GetConverter(property.PropertyType);
|
||||
foreach (var filterTermValue in filterTerm.Values)
|
||||
{
|
||||
var isFilterTermValueNull = filterTermValue.ToLower() == nullFilterValue;
|
||||
var (propertyValue, nullCheck) =
|
||||
GetPropertyValueAndNullCheckExpression(parameter, fullPropertyName);
|
||||
|
||||
var isFilterTermValueNull =
|
||||
IsFilterTermValueNull(propertyValue, filterTerm, filterTermValue);
|
||||
|
||||
var filterValue = isFilterTermValueNull
|
||||
? Expression.Constant(null, property.PropertyType)
|
||||
: ConvertStringValueToConstantExpression(filterTermValue, property, converter, cultureInfoToUseForTypeConversion);
|
||||
|
||||
if (filterTerm.OperatorIsCaseInsensitive)
|
||||
if (filterTerm.OperatorIsCaseInsensitive && !isFilterTermValueNull)
|
||||
{
|
||||
propertyValue = Expression.Call(propertyValue,
|
||||
typeof(string).GetMethods()
|
||||
.First(m => m.Name == "ToUpper" && m.GetParameters().Length == 0));
|
||||
.First(m => m.Name == "ToUpper" && m.GetParameters().Length == 0));
|
||||
|
||||
filterValue = Expression.Call(filterValue,
|
||||
typeof(string).GetMethods()
|
||||
.First(m => m.Name == "ToUpper" && m.GetParameters().Length == 0));
|
||||
.First(m => m.Name == "ToUpper" && m.GetParameters().Length == 0));
|
||||
}
|
||||
|
||||
var expression = GetExpression(filterTerm, filterValue, propertyValue);
|
||||
@ -225,61 +221,103 @@ namespace Sieve.Services
|
||||
expression = Expression.Not(expression);
|
||||
}
|
||||
|
||||
var filterValueNullCheck = !isFilterTermValueNull && propertyValue.Type.IsNullable()
|
||||
? GenerateFilterNullCheckExpression(propertyValue, nullCheck)
|
||||
: nullCheck;
|
||||
|
||||
if (filterValueNullCheck != null)
|
||||
if (expression.NodeType != ExpressionType.NotEqual || Options.Value.IgnoreNullsOnNotEqual)
|
||||
{
|
||||
expression = Expression.AndAlso(filterValueNullCheck, expression);
|
||||
var filterValueNullCheck = GetFilterValueNullCheck(parameter, fullPropertyName, isFilterTermValueNull);
|
||||
if (filterValueNullCheck != null)
|
||||
{
|
||||
expression = Expression.AndAlso(filterValueNullCheck, expression);
|
||||
}
|
||||
}
|
||||
|
||||
if (innerExpression == null)
|
||||
{
|
||||
innerExpression = expression;
|
||||
}
|
||||
else
|
||||
{
|
||||
innerExpression = Expression.OrElse(innerExpression, expression);
|
||||
}
|
||||
innerExpression = innerExpression == null
|
||||
? expression
|
||||
: Expression.OrElse(innerExpression, expression);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result = ApplyCustomMethod(result, filterTermName, _customFilterMethods,
|
||||
new object[] {
|
||||
result,
|
||||
filterTerm.Operator,
|
||||
filterTerm.Values
|
||||
}, dataForCustomMethods);
|
||||
|
||||
new object[] {result, filterTerm.Operator, filterTerm.Values}, dataForCustomMethods);
|
||||
}
|
||||
}
|
||||
|
||||
if (outerExpression == null)
|
||||
{
|
||||
outerExpression = innerExpression;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (innerExpression == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
outerExpression = Expression.AndAlso(outerExpression, innerExpression);
|
||||
}
|
||||
|
||||
return outerExpression == null
|
||||
? result
|
||||
: result.Where(Expression.Lambda<Func<TEntity, bool>>(outerExpression, parameter));
|
||||
}
|
||||
|
||||
private static Expression GenerateFilterNullCheckExpression(Expression propertyValue, Expression nullCheckExpression)
|
||||
private static Expression GetFilterValueNullCheck(Expression parameter, string fullPropertyName, bool isFilterTermValueNull)
|
||||
{
|
||||
var (propertyValue, nullCheck) = GetPropertyValueAndNullCheckExpression(parameter, fullPropertyName);
|
||||
|
||||
if (!isFilterTermValueNull && propertyValue.Type.IsNullable())
|
||||
{
|
||||
return GenerateFilterNullCheckExpression(propertyValue, nullCheck);
|
||||
}
|
||||
|
||||
return nullCheck;
|
||||
}
|
||||
|
||||
private static bool IsFilterTermValueNull(Expression propertyValue, TFilterTerm filterTerm,
|
||||
string filterTermValue)
|
||||
{
|
||||
var isNotString = propertyValue.Type != typeof(string);
|
||||
|
||||
var isValidStringNullOperation = filterTerm.OperatorParsed == FilterOperator.Equals ||
|
||||
filterTerm.OperatorParsed == FilterOperator.NotEquals;
|
||||
|
||||
return filterTermValue.ToLower() == NullFilterValue && (isNotString || isValidStringNullOperation);
|
||||
}
|
||||
|
||||
private static (Expression propertyValue, Expression nullCheck) GetPropertyValueAndNullCheckExpression(
|
||||
Expression parameter, string fullPropertyName)
|
||||
{
|
||||
var propertyValue = parameter;
|
||||
Expression nullCheck = null;
|
||||
var names = fullPropertyName.Split('.');
|
||||
for (var i = 0; i < names.Length; i++)
|
||||
{
|
||||
propertyValue = Expression.PropertyOrField(propertyValue, names[i]);
|
||||
|
||||
if (i != names.Length - 1 && propertyValue.Type.IsNullable())
|
||||
{
|
||||
nullCheck = GenerateFilterNullCheckExpression(propertyValue, nullCheck);
|
||||
}
|
||||
}
|
||||
|
||||
return (propertyValue, nullCheck);
|
||||
}
|
||||
|
||||
private static Expression GenerateFilterNullCheckExpression(Expression propertyValue,
|
||||
Expression nullCheckExpression)
|
||||
{
|
||||
return nullCheckExpression == null
|
||||
? Expression.NotEqual(propertyValue, Expression.Default(propertyValue.Type))
|
||||
: Expression.AndAlso(nullCheckExpression, Expression.NotEqual(propertyValue, Expression.Default(propertyValue.Type)));
|
||||
: Expression.AndAlso(nullCheckExpression,
|
||||
Expression.NotEqual(propertyValue, Expression.Default(propertyValue.Type)));
|
||||
}
|
||||
|
||||
private Expression ConvertStringValueToConstantExpression(string value, PropertyInfo property, TypeConverter converter, CultureInfo cultureInfo)
|
||||
private static Expression ConvertStringValueToConstantExpression(string value, PropertyInfo property, TypeConverter converter, CultureInfo cultureInfo)
|
||||
{
|
||||
// to allow user to distinguish between prop==null (as null) and prop==\null (as "null"-string)
|
||||
value = value.Equals(EscapeChar + NullFilterValue, StringComparison.InvariantCultureIgnoreCase)
|
||||
? value.TrimStart(EscapeChar)
|
||||
: value;
|
||||
dynamic constantVal = converter.CanConvertFrom(typeof(string))
|
||||
? converter.ConvertFrom(null, cultureInfo, value)
|
||||
: Convert.ChangeType(value, property.PropertyType);
|
||||
@ -289,47 +327,32 @@ namespace Sieve.Services
|
||||
|
||||
private static Expression GetExpression(TFilterTerm filterTerm, dynamic filterValue, dynamic propertyValue)
|
||||
{
|
||||
switch (filterTerm.OperatorParsed)
|
||||
return filterTerm.OperatorParsed switch
|
||||
{
|
||||
case FilterOperator.Equals:
|
||||
return Expression.Equal(propertyValue, filterValue);
|
||||
case FilterOperator.NotEquals:
|
||||
return Expression.NotEqual(propertyValue, filterValue);
|
||||
case FilterOperator.GreaterThan:
|
||||
return Expression.GreaterThan(propertyValue, filterValue);
|
||||
case FilterOperator.LessThan:
|
||||
return Expression.LessThan(propertyValue, filterValue);
|
||||
case FilterOperator.GreaterThanOrEqualTo:
|
||||
return Expression.GreaterThanOrEqual(propertyValue, filterValue);
|
||||
case FilterOperator.LessThanOrEqualTo:
|
||||
return Expression.LessThanOrEqual(propertyValue, filterValue);
|
||||
case FilterOperator.Contains:
|
||||
return Expression.Call(propertyValue,
|
||||
typeof(string).GetMethods()
|
||||
.First(m => m.Name == "Contains" && m.GetParameters().Length == 1),
|
||||
filterValue);
|
||||
case FilterOperator.StartsWith:
|
||||
return Expression.Call(propertyValue,
|
||||
typeof(string).GetMethods()
|
||||
.First(m => m.Name == "StartsWith" && m.GetParameters().Length == 1),
|
||||
filterValue);
|
||||
default:
|
||||
return Expression.Equal(propertyValue, filterValue);
|
||||
}
|
||||
FilterOperator.Equals => Expression.Equal(propertyValue, filterValue),
|
||||
FilterOperator.NotEquals => Expression.NotEqual(propertyValue, filterValue),
|
||||
FilterOperator.GreaterThan => Expression.GreaterThan(propertyValue, filterValue),
|
||||
FilterOperator.LessThan => Expression.LessThan(propertyValue, filterValue),
|
||||
FilterOperator.GreaterThanOrEqualTo => Expression.GreaterThanOrEqual(propertyValue, filterValue),
|
||||
FilterOperator.LessThanOrEqualTo => Expression.LessThanOrEqual(propertyValue, filterValue),
|
||||
FilterOperator.Contains => Expression.Call(propertyValue,
|
||||
typeof(string).GetMethods().First(m => m.Name == "Contains" && m.GetParameters().Length == 1),
|
||||
filterValue),
|
||||
FilterOperator.StartsWith => Expression.Call(propertyValue,
|
||||
typeof(string).GetMethods().First(m => m.Name == "StartsWith" && m.GetParameters().Length == 1),
|
||||
filterValue),
|
||||
_ => Expression.Equal(propertyValue, filterValue)
|
||||
};
|
||||
}
|
||||
|
||||
// Workaround to ensure that the filter value gets passed as a parameter in generated SQL from EF Core
|
||||
// See https://github.com/aspnet/EntityFrameworkCore/issues/3361
|
||||
// Expression.Constant passed the target type to allow Nullable comparison
|
||||
// See http://bradwilson.typepad.com/blog/2008/07/creating-nullab.html
|
||||
private Expression GetClosureOverConstant<T>(T constant, Type targetType)
|
||||
private static Expression GetClosureOverConstant<T>(T constant, Type targetType)
|
||||
{
|
||||
return Expression.Constant(constant, targetType);
|
||||
Expression<Func<T>> hoistedConstant = () => constant;
|
||||
return Expression.Convert(hoistedConstant.Body, targetType);
|
||||
}
|
||||
|
||||
private IQueryable<TEntity> ApplySorting<TEntity>(
|
||||
TSieveModel model,
|
||||
IQueryable<TEntity> result,
|
||||
protected virtual IQueryable<TEntity> ApplySorting<TEntity>(TSieveModel model, IQueryable<TEntity> result,
|
||||
object[] dataForCustomMethods = null)
|
||||
{
|
||||
if (model?.GetSortsParsed() == null)
|
||||
@ -349,33 +372,29 @@ namespace Sieve.Services
|
||||
else
|
||||
{
|
||||
result = ApplyCustomMethod(result, sortTerm.Name, _customSortMethods,
|
||||
new object[]
|
||||
{
|
||||
result,
|
||||
useThenBy,
|
||||
sortTerm.Descending
|
||||
}, dataForCustomMethods);
|
||||
new object[] {result, useThenBy, sortTerm.Descending}, dataForCustomMethods);
|
||||
}
|
||||
|
||||
useThenBy = true;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private IQueryable<TEntity> ApplyPagination<TEntity>(
|
||||
TSieveModel model,
|
||||
IQueryable<TEntity> result)
|
||||
protected virtual IQueryable<TEntity> ApplyPagination<TEntity>(TSieveModel model, IQueryable<TEntity> result)
|
||||
{
|
||||
var page = model?.Page ?? 1;
|
||||
var pageSize = model?.PageSize ?? _options.Value.DefaultPageSize;
|
||||
var maxPageSize = _options.Value.MaxPageSize > 0 ? _options.Value.MaxPageSize : pageSize;
|
||||
var pageSize = model?.PageSize ?? Options.Value.DefaultPageSize;
|
||||
var maxPageSize = Options.Value.MaxPageSize > 0 ? Options.Value.MaxPageSize : pageSize;
|
||||
|
||||
if (pageSize > 0)
|
||||
if (pageSize <= 0)
|
||||
{
|
||||
result = result.Skip((page - 1) * pageSize);
|
||||
result = result.Take(Math.Min(pageSize, maxPageSize));
|
||||
return result;
|
||||
}
|
||||
|
||||
result = result.Skip((page - 1) * pageSize);
|
||||
result = result.Take(Math.Min(pageSize, maxPageSize));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -384,51 +403,52 @@ namespace Sieve.Services
|
||||
return mapper;
|
||||
}
|
||||
|
||||
private (string, PropertyInfo) GetSieveProperty<TEntity>(
|
||||
bool canSortRequired,
|
||||
bool canFilterRequired,
|
||||
private (string, PropertyInfo) GetSieveProperty<TEntity>(bool canSortRequired, bool canFilterRequired,
|
||||
string name)
|
||||
{
|
||||
var property = mapper.FindProperty<TEntity>(canSortRequired, canFilterRequired, name, _options.Value.CaseSensitive);
|
||||
if (property.Item1 == null)
|
||||
var property = _mapper.FindProperty<TEntity>(canSortRequired, canFilterRequired, name,
|
||||
Options.Value.CaseSensitive);
|
||||
if (property.Item1 != null)
|
||||
{
|
||||
var prop = FindPropertyBySieveAttribute<TEntity>(canSortRequired, canFilterRequired, name, _options.Value.CaseSensitive);
|
||||
return (prop?.Name, prop);
|
||||
return property;
|
||||
}
|
||||
return property;
|
||||
|
||||
var prop = FindPropertyBySieveAttribute<TEntity>(canSortRequired, canFilterRequired, name,
|
||||
Options.Value.CaseSensitive);
|
||||
return (prop?.Name, prop);
|
||||
}
|
||||
|
||||
private PropertyInfo FindPropertyBySieveAttribute<TEntity>(
|
||||
bool canSortRequired,
|
||||
bool canFilterRequired,
|
||||
string name,
|
||||
bool isCaseSensitive)
|
||||
private static PropertyInfo FindPropertyBySieveAttribute<TEntity>(bool canSortRequired, bool canFilterRequired,
|
||||
string name, bool isCaseSensitive)
|
||||
{
|
||||
return Array.Find(typeof(TEntity).GetProperties(), p =>
|
||||
{
|
||||
return p.GetCustomAttribute(typeof(SieveAttribute)) is SieveAttribute sieveAttribute
|
||||
&& (!canSortRequired || sieveAttribute.CanSort)
|
||||
&& (!canFilterRequired || sieveAttribute.CanFilter)
|
||||
&& (sieveAttribute.Name ?? p.Name).Equals(name, isCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase);
|
||||
});
|
||||
return Array.Find(typeof(TEntity).GetProperties(),
|
||||
p => p.GetCustomAttribute(typeof(SieveAttribute)) is SieveAttribute SieveAttribute
|
||||
&& (!canSortRequired || SieveAttribute.CanSort)
|
||||
&& (!canFilterRequired || SieveAttribute.CanFilter)
|
||||
&& (SieveAttribute.Name ?? p.Name).Equals(name,
|
||||
isCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private IQueryable<TEntity> ApplyCustomMethod<TEntity>(IQueryable<TEntity> result, string name, object parent, object[] parameters, object[] optionalParameters = null)
|
||||
private IQueryable<TEntity> ApplyCustomMethod<TEntity>(IQueryable<TEntity> result, string name, object parent,
|
||||
object[] parameters, object[] optionalParameters = null)
|
||||
{
|
||||
var customMethod = parent?.GetType()
|
||||
.GetMethodExt(name,
|
||||
_options.Value.CaseSensitive ? BindingFlags.Default : BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance,
|
||||
typeof(IQueryable<TEntity>));
|
||||
Options.Value.CaseSensitive
|
||||
? BindingFlags.Default
|
||||
: BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance,
|
||||
typeof(IQueryable<TEntity>));
|
||||
|
||||
|
||||
if (customMethod == null)
|
||||
{
|
||||
// Find generic methods `public IQueryable<T> Filter<T>(IQueryable<T> source, ...)`
|
||||
var genericCustomMethod = parent?.GetType()
|
||||
.GetMethodExt(name,
|
||||
_options.Value.CaseSensitive ? BindingFlags.Default : BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance,
|
||||
typeof(IQueryable<>));
|
||||
.GetMethodExt(name,
|
||||
Options.Value.CaseSensitive
|
||||
? BindingFlags.Default
|
||||
: BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance,
|
||||
typeof(IQueryable<>));
|
||||
|
||||
if (genericCustomMethod != null &&
|
||||
genericCustomMethod.ReturnType.IsGenericType &&
|
||||
@ -436,7 +456,8 @@ namespace Sieve.Services
|
||||
{
|
||||
var genericBaseType = genericCustomMethod.ReturnType.GenericTypeArguments[0];
|
||||
var constraints = genericBaseType.GetGenericParameterConstraints();
|
||||
if (constraints == null || constraints.Length == 0 || constraints.All((t) => t.IsAssignableFrom(typeof(TEntity))))
|
||||
if (constraints == null || constraints.Length == 0 ||
|
||||
constraints.All((t) => t.IsAssignableFrom(typeof(TEntity))))
|
||||
{
|
||||
customMethod = genericCustomMethod.MakeGenericMethod(typeof(TEntity));
|
||||
}
|
||||
@ -465,40 +486,34 @@ namespace Sieve.Services
|
||||
}
|
||||
else
|
||||
{
|
||||
var incompatibleCustomMethods = parent?
|
||||
.GetType()
|
||||
.GetMethods
|
||||
(
|
||||
_options.Value.CaseSensitive
|
||||
? BindingFlags.Default
|
||||
: BindingFlags.IgnoreCase | BindingFlags.Public |
|
||||
BindingFlags.Instance
|
||||
)
|
||||
.Where(method => string.Equals(method.Name, name,
|
||||
_options.Value.CaseSensitive
|
||||
? StringComparison.InvariantCulture
|
||||
: StringComparison.InvariantCultureIgnoreCase))
|
||||
.ToList()
|
||||
??
|
||||
new List<MethodInfo>();
|
||||
var incompatibleCustomMethods =
|
||||
parent?
|
||||
.GetType()
|
||||
.GetMethods(Options.Value.CaseSensitive
|
||||
? BindingFlags.Default
|
||||
: BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(method => string.Equals(method.Name, name,
|
||||
Options.Value.CaseSensitive
|
||||
? StringComparison.InvariantCulture
|
||||
: StringComparison.InvariantCultureIgnoreCase))
|
||||
.ToList()
|
||||
?? new List<MethodInfo>();
|
||||
|
||||
if (incompatibleCustomMethods.Any())
|
||||
{
|
||||
var incompatibles =
|
||||
from incompatibleCustomMethod in incompatibleCustomMethods
|
||||
let expected = typeof(IQueryable<TEntity>)
|
||||
let actual = incompatibleCustomMethod.ReturnType
|
||||
select new SieveIncompatibleMethodException(name, expected, actual,
|
||||
$"{name} failed. Expected a custom method for type {expected} but only found for type {actual}");
|
||||
|
||||
var aggregate = new AggregateException(incompatibles);
|
||||
|
||||
throw new SieveIncompatibleMethodException(aggregate.Message, aggregate);
|
||||
}
|
||||
else
|
||||
if (!incompatibleCustomMethods.Any())
|
||||
{
|
||||
throw new SieveMethodNotFoundException(name, $"{name} not found.");
|
||||
}
|
||||
|
||||
var incompatibles =
|
||||
from incompatibleCustomMethod in incompatibleCustomMethods
|
||||
let expected = typeof(IQueryable<TEntity>)
|
||||
let actual = incompatibleCustomMethod.ReturnType
|
||||
select new SieveIncompatibleMethodException(name, expected, actual,
|
||||
$"{name} failed. Expected a custom method for type {expected} but only found for type {actual}");
|
||||
|
||||
var aggregate = new AggregateException(incompatibles);
|
||||
|
||||
throw new SieveIncompatibleMethodException(aggregate.Message, aggregate);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
@ -1,22 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<Version>2.3.3</Version>
|
||||
<Description>Sieve is a simple, clean, and extensible framework for .NET Core that adds sorting, filtering, and pagination functionality out of the box. Most common use case would be for serving ASP.NET Core GET queries. Documentation available on GitHub: https://github.com/Biarity/Sieve/
|
||||
</Description>
|
||||
<Copyright>Copyright 2018</Copyright>
|
||||
<PackageLicenseUrl>https://github.com/Biarity/Sieve/blob/master/LICENSE</PackageLicenseUrl>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<Description>Sieve is a simple, clean, and extensible framework for .NET Core that adds sorting, filtering, and pagination functionality out of the box. Most common use case would be for serving ASP.NET Core GET queries. Documentation available on GitHub: https://github.com/Biarity/Sieve/</Description>
|
||||
<Authors>Biarity;Ashish Patel;Kevin Sommer</Authors>
|
||||
|
||||
<PackageTags>Filter;Sort;Page;Paging;</PackageTags>
|
||||
<PackageProjectUrl>https://github.com/Biarity/Sieve</PackageProjectUrl>
|
||||
<PackageIconUrl>https://emojipedia-us.s3.amazonaws.com/thumbs/240/twitter/120/alembic_2697.png</PackageIconUrl>
|
||||
<RepositoryUrl></RepositoryUrl>
|
||||
<PackageReleaseNotes>Only Skip when pageSize > 0 (#63)
|
||||
Added support for generic filter and sort methods (#60)
|
||||
Don't process when filterTerm.Values is null (#59)
|
||||
</PackageReleaseNotes>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
|
||||
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
|
||||
<Authors>Biarity</Authors>
|
||||
|
||||
<RepositoryUrl>https://github.com/Biarity/Sieve</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
|
||||
<!-- Declare that the Repository URL can be published to NuSpec -->
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<!-- Embed source files that are not tracked by the source control manager to the PDB -->
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<!-- Include PDB in the built .nupkg -->
|
||||
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
BIN
Sieve/nuget.exe
BIN
Sieve/nuget.exe
Binary file not shown.
@ -1,44 +0,0 @@
|
||||
// <auto-generated />
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using Microsoft.EntityFrameworkCore.Storage.Internal;
|
||||
using SieveTests.Entities;
|
||||
using System;
|
||||
|
||||
namespace SieveTests.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20180127005347_Init")]
|
||||
partial class Init
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "2.0.1-rtm-125")
|
||||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||
|
||||
modelBuilder.Entity("SieveTests.Entities.Post", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<int>("CommentCount");
|
||||
|
||||
b.Property<DateTimeOffset>("DateCreated");
|
||||
|
||||
b.Property<int>("LikeCount");
|
||||
|
||||
b.Property<string>("Title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Posts");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
// <auto-generated />
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using Microsoft.EntityFrameworkCore.Storage.Internal;
|
||||
using SieveTests.Entities;
|
||||
using System;
|
||||
|
||||
namespace SieveTests.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20180522013323_AddDateLastViewedColumn")]
|
||||
partial class AddDateLastViewedColumn
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "2.0.1-rtm-125")
|
||||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||
|
||||
modelBuilder.Entity("SieveTests.Entities.Post", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<int>("CommentCount");
|
||||
|
||||
b.Property<DateTimeOffset>("DateCreated");
|
||||
|
||||
b.Property<DateTime>("DateLastViewed")
|
||||
.HasColumnType("datetime");
|
||||
|
||||
b.Property<int>("LikeCount");
|
||||
|
||||
b.Property<string>("Title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Posts");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace SieveTests.Migrations
|
||||
{
|
||||
public partial class AddDateLastViewedColumn : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "DateLastViewed",
|
||||
table: "Posts",
|
||||
type: "datetime",
|
||||
nullable: false,
|
||||
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DateLastViewed",
|
||||
table: "Posts");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
// <auto-generated />
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using Microsoft.EntityFrameworkCore.Storage.Internal;
|
||||
using SieveTests.Entities;
|
||||
using System;
|
||||
|
||||
namespace SieveTests.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "2.0.1-rtm-125")
|
||||
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
|
||||
|
||||
modelBuilder.Entity("SieveTests.Entities.Post", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<int>("CommentCount");
|
||||
|
||||
b.Property<DateTimeOffset>("DateCreated");
|
||||
|
||||
b.Property<DateTime>("DateLastViewed")
|
||||
.HasColumnType("datetime");
|
||||
|
||||
b.Property<int>("LikeCount");
|
||||
|
||||
b.Property<int?>("CategoryId");
|
||||
|
||||
b.Property<string>("Title");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Posts");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Migrations\20180127005211_Itit.cs" />
|
||||
<Compile Remove="Migrations\20180127005211_Itit.Designer.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="wwwroot\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.9" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SieveUnitTests\SieveUnitTests.csproj" />
|
||||
<ProjectReference Include="..\Sieve\Sieve.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
@ -1,67 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Sieve.Models;
|
||||
using Sieve.Services;
|
||||
using SieveTests.Entities;
|
||||
using SieveTests.Services;
|
||||
|
||||
namespace SieveTests
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
public Startup(IConfiguration configuration)
|
||||
{
|
||||
Configuration = configuration;
|
||||
}
|
||||
|
||||
public IConfiguration Configuration { get; }
|
||||
|
||||
// This method gets called by the runtime. Use this method to add services to the container.
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddMvc();
|
||||
|
||||
services.AddDbContext<ApplicationDbContext>(options =>
|
||||
options.UseSqlServer(Configuration.GetConnectionString("TestSqlServer")));
|
||||
|
||||
services.Configure<SieveOptions>(Configuration.GetSection("Sieve"));
|
||||
|
||||
services.AddScoped<ISieveCustomSortMethods, SieveCustomSortMethods>();
|
||||
services.AddScoped<ISieveCustomFilterMethods, SieveCustomFilterMethods>();
|
||||
services.AddScoped<ISieveProcessor, ApplicationSieveProcessor>();
|
||||
}
|
||||
|
||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
|
||||
{
|
||||
// TIME MEASUREMENT
|
||||
var times = new List<long>();
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
var sw = new Stopwatch();
|
||||
sw.Start();
|
||||
await next.Invoke();
|
||||
sw.Stop();
|
||||
times.Add(sw.ElapsedMilliseconds);
|
||||
var text = $"AVG: {(int)times.Average()}ms; AT {sw.ElapsedMilliseconds}; COUNT: {times.Count()}";
|
||||
Console.WriteLine(text);
|
||||
await context.Response.WriteAsync($"<!-- {text} -->");
|
||||
});
|
||||
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
}
|
||||
|
||||
app.UseMvc();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
using Sieve.Services;
|
||||
|
||||
namespace SieveUnitTests.Abstractions.Entity
|
||||
{
|
||||
public class SieveConfigurationForIPost : ISieveConfiguration
|
||||
{
|
||||
public void Configure(SievePropertyMapper mapper)
|
||||
{
|
||||
mapper.Property<IPost>(p => p.ThisHasNoAttributeButIsAccessible)
|
||||
.CanSort()
|
||||
.CanFilter()
|
||||
.HasName("shortname");
|
||||
|
||||
mapper.Property<IPost>(p => p.TopComment.Text)
|
||||
.CanFilter();
|
||||
|
||||
mapper.Property<IPost>(p => p.TopComment.Id)
|
||||
.CanSort();
|
||||
|
||||
mapper.Property<IPost>(p => p.OnlySortableViaFluentApi)
|
||||
.CanSort();
|
||||
|
||||
mapper.Property<IPost>(p => p.TopComment.Text)
|
||||
.CanFilter()
|
||||
.HasName("topc");
|
||||
|
||||
mapper.Property<IPost>(p => p.FeaturedComment.Text)
|
||||
.CanFilter()
|
||||
.HasName("featc");
|
||||
|
||||
mapper
|
||||
.Property<IPost>(p => p.DateCreated)
|
||||
.CanSort()
|
||||
.HasName("CreateDate");
|
||||
}
|
||||
}
|
||||
}
|
@ -7,5 +7,8 @@ namespace SieveUnitTests.Entities
|
||||
{
|
||||
[Sieve(CanFilter = true)]
|
||||
public string Text { get; set; }
|
||||
|
||||
[Sieve(CanFilter = true)]
|
||||
public string Author { get; set; }
|
||||
}
|
||||
}
|
||||
|
37
SieveUnitTests/Entities/SieveConfigurationForPost.cs
Normal file
37
SieveUnitTests/Entities/SieveConfigurationForPost.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using Sieve.Services;
|
||||
|
||||
namespace SieveUnitTests.Entities
|
||||
{
|
||||
public class SieveConfigurationForPost : ISieveConfiguration
|
||||
{
|
||||
public void Configure(SievePropertyMapper mapper)
|
||||
{
|
||||
mapper.Property<Post>(p => p.ThisHasNoAttributeButIsAccessible)
|
||||
.CanSort()
|
||||
.CanFilter()
|
||||
.HasName("shortname");
|
||||
|
||||
mapper.Property<Post>(p => p.TopComment.Text)
|
||||
.CanFilter();
|
||||
|
||||
mapper.Property<Post>(p => p.TopComment.Id)
|
||||
.CanSort();
|
||||
|
||||
mapper.Property<Post>(p => p.OnlySortableViaFluentApi)
|
||||
.CanSort();
|
||||
|
||||
mapper.Property<Post>(p => p.TopComment.Text)
|
||||
.CanFilter()
|
||||
.HasName("topc");
|
||||
|
||||
mapper.Property<Post>(p => p.FeaturedComment.Text)
|
||||
.CanFilter()
|
||||
.HasName("featc");
|
||||
|
||||
mapper
|
||||
.Property<Post>(p => p.DateCreated)
|
||||
.CanSort()
|
||||
.HasName("CreateDate");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,32 +1,42 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Sieve.Exceptions;
|
||||
using Sieve.Models;
|
||||
using Sieve.Services;
|
||||
using SieveUnitTests.Abstractions.Entity;
|
||||
using SieveUnitTests.Entities;
|
||||
using SieveUnitTests.Services;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace SieveUnitTests
|
||||
{
|
||||
[TestClass]
|
||||
public class General
|
||||
{
|
||||
private readonly ITestOutputHelper _testOutputHelper;
|
||||
private readonly SieveProcessor _processor;
|
||||
private readonly SieveProcessor _nullableProcessor;
|
||||
private readonly IQueryable<Post> _posts;
|
||||
private readonly IQueryable<Comment> _comments;
|
||||
|
||||
public General()
|
||||
public General(ITestOutputHelper testOutputHelper)
|
||||
{
|
||||
var nullableAccessor = new SieveOptionsAccessor();
|
||||
nullableAccessor.Value.IgnoreNullsOnNotEqual = false;
|
||||
|
||||
_testOutputHelper = testOutputHelper;
|
||||
_processor = new ApplicationSieveProcessor(new SieveOptionsAccessor(),
|
||||
new SieveCustomSortMethods(),
|
||||
new SieveCustomFilterMethods());
|
||||
|
||||
_nullableProcessor = new ApplicationSieveProcessor(nullableAccessor,
|
||||
new SieveCustomSortMethods(),
|
||||
new SieveCustomFilterMethods());
|
||||
|
||||
_posts = new List<Post>
|
||||
{
|
||||
new Post() {
|
||||
new Post
|
||||
{
|
||||
Id = 0,
|
||||
Title = "A",
|
||||
LikeCount = 100,
|
||||
@ -35,7 +45,8 @@ namespace SieveUnitTests
|
||||
TopComment = new Comment { Id = 0, Text = "A1" },
|
||||
FeaturedComment = new Comment { Id = 4, Text = "A2" }
|
||||
},
|
||||
new Post() {
|
||||
new Post
|
||||
{
|
||||
Id = 1,
|
||||
Title = "B",
|
||||
LikeCount = 50,
|
||||
@ -44,7 +55,8 @@ namespace SieveUnitTests
|
||||
TopComment = new Comment { Id = 3, Text = "B1" },
|
||||
FeaturedComment = new Comment { Id = 5, Text = "B2" }
|
||||
},
|
||||
new Post() {
|
||||
new Post
|
||||
{
|
||||
Id = 2,
|
||||
Title = "C",
|
||||
LikeCount = 0,
|
||||
@ -52,7 +64,8 @@ namespace SieveUnitTests
|
||||
TopComment = new Comment { Id = 2, Text = "C1" },
|
||||
FeaturedComment = new Comment { Id = 6, Text = "C2" }
|
||||
},
|
||||
new Post() {
|
||||
new Post
|
||||
{
|
||||
Id = 3,
|
||||
Title = "D",
|
||||
LikeCount = 3,
|
||||
@ -60,286 +73,323 @@ namespace SieveUnitTests
|
||||
CategoryId = 2,
|
||||
TopComment = new Comment { Id = 1, Text = "D1" },
|
||||
FeaturedComment = new Comment { Id = 7, Text = "D2" }
|
||||
},
|
||||
}
|
||||
}.AsQueryable();
|
||||
|
||||
_comments = new List<Comment>
|
||||
{
|
||||
new Comment() {
|
||||
new Comment
|
||||
{
|
||||
Id = 0,
|
||||
DateCreated = DateTimeOffset.UtcNow.AddDays(-20),
|
||||
Text = "This is an old comment."
|
||||
},
|
||||
new Comment() {
|
||||
new Comment
|
||||
{
|
||||
Id = 1,
|
||||
DateCreated = DateTimeOffset.UtcNow.AddDays(-1),
|
||||
Text = "This is a fairly new comment."
|
||||
},
|
||||
new Comment() {
|
||||
new Comment
|
||||
{
|
||||
Id = 2,
|
||||
DateCreated = DateTimeOffset.UtcNow,
|
||||
Text = "This is a brand new comment. (Text in braces)"
|
||||
Text = "This is a brand new comment. (Text in braces, comma separated)"
|
||||
},
|
||||
}.AsQueryable();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void ContainsCanBeCaseInsensitive()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "Title@=*a"
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
|
||||
Assert.AreEqual(result.First().Id, 0);
|
||||
Assert.IsTrue(result.Count() == 1);
|
||||
Assert.Equal(0, result.First().Id);
|
||||
Assert.True(result.Count() == 1);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void NotEqualsCanBeCaseInsensitive()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "Title!=*a"
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
|
||||
Assert.AreEqual(result.First().Id, 1);
|
||||
Assert.IsTrue(result.Count() == 3);
|
||||
Assert.Equal(1, result.First().Id);
|
||||
Assert.True(result.Count() == 3);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void ContainsIsCaseSensitive()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "Title@=a",
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
|
||||
Assert.IsTrue(result.Count() == 0);
|
||||
Assert.True(!result.Any());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void NotContainsWorks()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "Title!@=D",
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
|
||||
Assert.IsTrue(result.Count() == 3);
|
||||
Assert.True(result.Count() == 3);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void CanFilterBools()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "IsDraft==false"
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
|
||||
Assert.IsTrue(result.Count() == 2);
|
||||
Assert.True(result.Count() == 2);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void CanSortBools()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Sorts = "-IsDraft"
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
|
||||
Assert.AreEqual(result.First().Id, 0);
|
||||
Assert.Equal(0, result.First().Id);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void CanFilterNullableInts()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "CategoryId==1"
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
var nullableResult = _nullableProcessor.Apply(model, _posts);
|
||||
|
||||
Assert.IsTrue(result.Count() == 2);
|
||||
Assert.True(result.Count() == 2);
|
||||
Assert.True(nullableResult.Count() == 2);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void EqualsDoesntFailWithNonStringTypes()
|
||||
[Fact]
|
||||
public void CanFilterNullableIntsWithNotEqual()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
{
|
||||
Filters = "CategoryId!=1"
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
var nullableResult = _nullableProcessor.Apply(model, _posts);
|
||||
|
||||
Assert.True(result.Count() == 1);
|
||||
Assert.True(nullableResult.Count() == 2);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"Text@=*\,")]
|
||||
[InlineData(@"Text@=*\, ")]
|
||||
[InlineData(@"Text@=*braces\,")]
|
||||
[InlineData(@"Text@=*braces\, comma")]
|
||||
public void CanFilterWithEscapedComma(string filter)
|
||||
{
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = filter
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _comments);
|
||||
|
||||
Assert.True(result.Count() == 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EqualsDoesntFailWithNonStringTypes()
|
||||
{
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "LikeCount==50",
|
||||
};
|
||||
|
||||
Console.WriteLine(model.GetFiltersParsed()[0].Values);
|
||||
Console.WriteLine(model.GetFiltersParsed()[0].Operator);
|
||||
Console.WriteLine(model.GetFiltersParsed()[0].OperatorParsed);
|
||||
_testOutputHelper.WriteLine(model.GetFiltersParsed()[0].Values.ToString());
|
||||
_testOutputHelper.WriteLine(model.GetFiltersParsed()[0].Operator);
|
||||
_testOutputHelper.WriteLine(model.GetFiltersParsed()[0].OperatorParsed.ToString());
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
|
||||
Assert.AreEqual(result.First().Id, 1);
|
||||
Assert.IsTrue(result.Count() == 1);
|
||||
Assert.Equal(1, result.First().Id);
|
||||
Assert.True(result.Count() == 1);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void CustomFiltersWork()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "Isnew",
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
|
||||
Assert.IsFalse(result.Any(p => p.Id == 0));
|
||||
Assert.IsTrue(result.Count() == 3);
|
||||
Assert.False(result.Any(p => p.Id == 0));
|
||||
Assert.True(result.Count() == 3);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void CustomGenericFiltersWork()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "Latest",
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _comments);
|
||||
|
||||
Assert.IsFalse(result.Any(p => p.Id == 0));
|
||||
Assert.IsTrue(result.Count() == 2);
|
||||
Assert.False(result.Any(p => p.Id == 0));
|
||||
Assert.True(result.Count() == 2);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void CustomFiltersWithOperatorsWork()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "HasInTitle==A",
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
|
||||
Assert.IsTrue(result.Any(p => p.Id == 0));
|
||||
Assert.IsTrue(result.Count() == 1);
|
||||
Assert.True(result.Any(p => p.Id == 0));
|
||||
Assert.True(result.Count() == 1);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void CustomFiltersMixedWithUsualWork1()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "Isnew,CategoryId==2",
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
|
||||
Assert.IsTrue(result.Any(p => p.Id == 3));
|
||||
Assert.IsTrue(result.Count() == 1);
|
||||
Assert.True(result.Any(p => p.Id == 3));
|
||||
Assert.True(result.Count() == 1);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void CustomFiltersMixedWithUsualWork2()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "CategoryId==2,Isnew",
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
|
||||
Assert.IsTrue(result.Any(p => p.Id == 3));
|
||||
Assert.IsTrue(result.Count() == 1);
|
||||
Assert.True(result.Any(p => p.Id == 3));
|
||||
Assert.True(result.Count() == 1);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void CustomFiltersOnDifferentSourcesCanShareName()
|
||||
{
|
||||
var postModel = new SieveModel()
|
||||
var postModel = new SieveModel
|
||||
{
|
||||
Filters = "CategoryId==2,Isnew",
|
||||
};
|
||||
|
||||
var postResult = _processor.Apply(postModel, _posts);
|
||||
|
||||
Assert.IsTrue(postResult.Any(p => p.Id == 3));
|
||||
Assert.AreEqual(1, postResult.Count());
|
||||
Assert.True(postResult.Any(p => p.Id == 3));
|
||||
Assert.Equal(1, postResult.Count());
|
||||
|
||||
var commentModel = new SieveModel()
|
||||
var commentModel = new SieveModel
|
||||
{
|
||||
Filters = "Isnew",
|
||||
};
|
||||
|
||||
var commentResult = _processor.Apply(commentModel, _comments);
|
||||
|
||||
Assert.IsTrue(commentResult.Any(c => c.Id == 2));
|
||||
Assert.AreEqual(2, commentResult.Count());
|
||||
Assert.True(commentResult.Any(c => c.Id == 2));
|
||||
Assert.Equal(2, commentResult.Count());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void CustomSortsWork()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Sorts = "Popularity",
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
|
||||
Assert.IsFalse(result.First().Id == 0);
|
||||
Assert.False(result.First().Id == 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void CustomGenericSortsWork()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Sorts = "Oldest",
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
|
||||
Assert.IsTrue(result.Last().Id == 0);
|
||||
Assert.True(result.Last().Id == 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void MethodNotFoundExceptionWork()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "does not exist",
|
||||
};
|
||||
|
||||
Assert.ThrowsException<SieveMethodNotFoundException>(() => _processor.Apply(model, _posts));
|
||||
Assert.Throws<SieveMethodNotFoundException>(() => _processor.Apply(model, _posts));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void IncompatibleMethodExceptionsWork()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "TestComment",
|
||||
};
|
||||
|
||||
Assert.ThrowsException<SieveIncompatibleMethodException>(() => _processor.Apply(model, _posts));
|
||||
Assert.Throws<SieveIncompatibleMethodException>(() => _processor.Apply(model, _posts));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void OrNameFilteringWorks()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "(Title|LikeCount)==3",
|
||||
};
|
||||
@ -348,17 +398,17 @@ namespace SieveUnitTests
|
||||
var entry = result.FirstOrDefault();
|
||||
var resultCount = result.Count();
|
||||
|
||||
Assert.IsNotNull(entry);
|
||||
Assert.AreEqual(1, resultCount);
|
||||
Assert.AreEqual(3, entry.Id);
|
||||
Assert.NotNull(entry);
|
||||
Assert.Equal(1, resultCount);
|
||||
Assert.Equal(3, entry.Id);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("CategoryId==1,(CategoryId|LikeCount)==50")]
|
||||
[DataRow("(CategoryId|LikeCount)==50,CategoryId==1")]
|
||||
[Theory]
|
||||
[InlineData("CategoryId==1,(CategoryId|LikeCount)==50")]
|
||||
[InlineData("(CategoryId|LikeCount)==50,CategoryId==1")]
|
||||
public void CombinedAndOrFilterIndependentOfOrder(string filter)
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = filter,
|
||||
};
|
||||
@ -367,14 +417,14 @@ namespace SieveUnitTests
|
||||
var entry = result.FirstOrDefault();
|
||||
var resultCount = result.Count();
|
||||
|
||||
Assert.IsNotNull(entry);
|
||||
Assert.AreEqual(1, resultCount);
|
||||
Assert.NotNull(entry);
|
||||
Assert.Equal(1, resultCount);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void CombinedAndOrWithSpaceFilteringWorks()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "Title==D, (Title|LikeCount)==3",
|
||||
};
|
||||
@ -383,97 +433,98 @@ namespace SieveUnitTests
|
||||
var entry = result.FirstOrDefault();
|
||||
var resultCount = result.Count();
|
||||
|
||||
Assert.IsNotNull(entry);
|
||||
Assert.AreEqual(1, resultCount);
|
||||
Assert.AreEqual(3, entry.Id);
|
||||
Assert.NotNull(entry);
|
||||
Assert.Equal(1, resultCount);
|
||||
Assert.Equal(3, entry.Id);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void OrValueFilteringWorks()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "Title==C|D",
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
Assert.AreEqual(2, result.Count());
|
||||
Assert.IsTrue(result.Any(p => p.Id == 2));
|
||||
Assert.IsTrue(result.Any(p => p.Id == 3));
|
||||
Assert.Equal(2, result.Count());
|
||||
Assert.True(result.Any(p => p.Id == 2));
|
||||
Assert.True(result.Any(p => p.Id == 3));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void OrValueFilteringWorks2()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "Text@=(|)",
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _comments);
|
||||
Assert.AreEqual(1, result.Count());
|
||||
Assert.AreEqual(2, result.FirstOrDefault().Id);
|
||||
Assert.Equal(1, result.Count());
|
||||
Assert.Equal(2, result.FirstOrDefault()?.Id);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void NestedFilteringWorks()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "TopComment.Text!@=A",
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
Assert.AreEqual(3, result.Count());
|
||||
Assert.Equal(3, result.Count());
|
||||
var posts = result.ToList();
|
||||
Assert.IsTrue(posts[0].TopComment.Text.Contains("B"));
|
||||
Assert.IsTrue(posts[1].TopComment.Text.Contains("C"));
|
||||
Assert.IsTrue(posts[2].TopComment.Text.Contains("D"));
|
||||
Assert.Contains("B", posts[0].TopComment.Text);
|
||||
Assert.Contains("C", posts[1].TopComment.Text);
|
||||
Assert.Contains("D", posts[2].TopComment.Text);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void NestedSortingWorks()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Sorts = "TopComment.Id",
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
Assert.AreEqual(4, result.Count());
|
||||
Assert.Equal(4, result.Count());
|
||||
var posts = result.ToList();
|
||||
Assert.AreEqual(posts[0].Id, 0);
|
||||
Assert.AreEqual(posts[1].Id, 3);
|
||||
Assert.AreEqual(posts[2].Id, 2);
|
||||
Assert.AreEqual(posts[3].Id, 1);
|
||||
Assert.Equal(0, posts[0].Id);
|
||||
Assert.Equal(3, posts[1].Id);
|
||||
Assert.Equal(2, posts[2].Id);
|
||||
Assert.Equal(1, posts[3].Id);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void NestedFilteringWithIdenticTypesWorks()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "(topc|featc)@=*2",
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
Assert.AreEqual(4, result.Count());
|
||||
Assert.Equal(4, result.Count());
|
||||
|
||||
model = new SieveModel()
|
||||
model = new SieveModel
|
||||
{
|
||||
Filters = "(topc|featc)@=*B",
|
||||
};
|
||||
|
||||
result = _processor.Apply(model, _posts);
|
||||
Assert.AreEqual(1, result.Count());
|
||||
Assert.Equal(1, result.Count());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void FilteringNullsWorks()
|
||||
{
|
||||
var posts = new List<Post>
|
||||
{
|
||||
new Post() {
|
||||
new Post
|
||||
{
|
||||
Id = 1,
|
||||
Title = null,
|
||||
LikeCount = 0,
|
||||
@ -484,21 +535,22 @@ namespace SieveUnitTests
|
||||
},
|
||||
}.AsQueryable();
|
||||
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "FeaturedComment.Text!@=Some value",
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, posts);
|
||||
Assert.AreEqual(0, result.Count());
|
||||
Assert.Equal(0, result.Count());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void SortingNullsWorks()
|
||||
{
|
||||
var posts = new List<Post>
|
||||
{
|
||||
new Post() {
|
||||
new Post
|
||||
{
|
||||
Id = 1,
|
||||
Title = null,
|
||||
LikeCount = 0,
|
||||
@ -507,7 +559,8 @@ namespace SieveUnitTests
|
||||
TopComment = new Comment { Id = 1 },
|
||||
FeaturedComment = null
|
||||
},
|
||||
new Post() {
|
||||
new Post
|
||||
{
|
||||
Id = 2,
|
||||
Title = null,
|
||||
LikeCount = 0,
|
||||
@ -518,24 +571,25 @@ namespace SieveUnitTests
|
||||
},
|
||||
}.AsQueryable();
|
||||
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Sorts = "TopComment.Id",
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, posts);
|
||||
Assert.AreEqual(2, result.Count());
|
||||
Assert.Equal(2, result.Count());
|
||||
var sortedPosts = result.ToList();
|
||||
Assert.AreEqual(sortedPosts[0].Id, 2);
|
||||
Assert.AreEqual(sortedPosts[1].Id, 1);
|
||||
Assert.Equal(2, sortedPosts[0].Id);
|
||||
Assert.Equal(1, sortedPosts[1].Id);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void FilteringOnNullWorks()
|
||||
{
|
||||
var posts = new List<Post>
|
||||
{
|
||||
new Post() {
|
||||
new Post
|
||||
{
|
||||
Id = 1,
|
||||
Title = null,
|
||||
LikeCount = 0,
|
||||
@ -544,7 +598,8 @@ namespace SieveUnitTests
|
||||
TopComment = null,
|
||||
FeaturedComment = null
|
||||
},
|
||||
new Post() {
|
||||
new Post
|
||||
{
|
||||
Id = 2,
|
||||
Title = null,
|
||||
LikeCount = 0,
|
||||
@ -555,33 +610,190 @@ namespace SieveUnitTests
|
||||
},
|
||||
}.AsQueryable();
|
||||
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "FeaturedComment.Text==null",
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, posts);
|
||||
Assert.AreEqual(1, result.Count());
|
||||
Assert.Equal(1, result.Count());
|
||||
var filteredPosts = result.ToList();
|
||||
Assert.AreEqual(filteredPosts[0].Id, 2);
|
||||
Assert.Equal(2, filteredPosts[0].Id);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void BaseDefinedPropertyMappingSortingWorks_WithCustomName()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Sorts = "-CreateDate"
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
Assert.AreEqual(4, result.Count());
|
||||
Assert.Equal(4, result.Count());
|
||||
|
||||
var posts = result.ToList();
|
||||
Assert.AreEqual(posts[0].Id, 3);
|
||||
Assert.AreEqual(posts[1].Id, 2);
|
||||
Assert.AreEqual(posts[2].Id, 1);
|
||||
Assert.AreEqual(posts[3].Id, 0);
|
||||
Assert.Equal(3,posts[0].Id);
|
||||
Assert.Equal(2,posts[1].Id);
|
||||
Assert.Equal(1,posts[2].Id);
|
||||
Assert.Equal(0,posts[3].Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanFilter_WithEscapeCharacter()
|
||||
{
|
||||
var comments = new List<Comment>
|
||||
{
|
||||
new Comment
|
||||
{
|
||||
Id = 0,
|
||||
DateCreated = DateTimeOffset.UtcNow,
|
||||
Text = "Here is, a comment"
|
||||
},
|
||||
new Comment
|
||||
{
|
||||
Id = 1,
|
||||
DateCreated = DateTimeOffset.UtcNow.AddDays(-1),
|
||||
Text = "Here is, another comment"
|
||||
},
|
||||
}.AsQueryable();
|
||||
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "Text==Here is\\, another comment"
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, comments);
|
||||
Assert.Equal(1, result.Count());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OrEscapedPipeValueFilteringWorks()
|
||||
{
|
||||
var comments = new List<Comment>
|
||||
{
|
||||
new Comment
|
||||
{
|
||||
Id = 0,
|
||||
DateCreated = DateTimeOffset.UtcNow,
|
||||
Text = "Here is | a comment"
|
||||
},
|
||||
new Comment
|
||||
{
|
||||
Id = 1,
|
||||
DateCreated = DateTimeOffset.UtcNow.AddDays(-1),
|
||||
Text = "Here is | another comment"
|
||||
},
|
||||
new Comment
|
||||
{
|
||||
Id = 2,
|
||||
DateCreated = DateTimeOffset.UtcNow.AddDays(-1),
|
||||
Text = @"Here is \| another comment(1)"
|
||||
}
|
||||
}.AsQueryable();
|
||||
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = @"Text==Here is \| a comment|Here is \| another comment|Here is \\\| another comment(1)",
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, comments);
|
||||
Assert.Equal(3, result.Count());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("CategoryId==1,(CategoryId|LikeCount)==50")]
|
||||
[InlineData("(CategoryId|LikeCount)==50,CategoryId==1")]
|
||||
public void CanFilterWithEscape(string filter)
|
||||
{
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = filter
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
var entry = result.FirstOrDefault();
|
||||
var resultCount = result.Count();
|
||||
|
||||
Assert.NotNull(entry);
|
||||
Assert.Equal(1, resultCount);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"Title@=\\")]
|
||||
public void CanFilterWithEscapedBackSlash(string filter)
|
||||
{
|
||||
var posts = new List<Post>
|
||||
{
|
||||
new Post
|
||||
{
|
||||
Id = 1,
|
||||
Title = "E\\",
|
||||
LikeCount = 4,
|
||||
IsDraft = true,
|
||||
CategoryId = 1,
|
||||
TopComment = new Comment { Id = 1, Text = "E1" },
|
||||
FeaturedComment = new Comment { Id = 7, Text = "E2" }
|
||||
}
|
||||
}.AsQueryable();
|
||||
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = filter
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, posts);
|
||||
var entry = result.FirstOrDefault();
|
||||
var resultCount = result.Count();
|
||||
|
||||
Assert.NotNull(entry);
|
||||
Assert.Equal(1, resultCount);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"Title@=\== ")]
|
||||
[InlineData(@"Title@=\!= ")]
|
||||
[InlineData(@"Title@=\> ")]
|
||||
[InlineData(@"Title@=\< ")]
|
||||
[InlineData(@"Title@=\<= ")]
|
||||
[InlineData(@"Title@=\>= ")]
|
||||
[InlineData(@"Title@=\@= ")]
|
||||
[InlineData(@"Title@=\_= ")]
|
||||
[InlineData(@"Title@=!\@= ")]
|
||||
[InlineData(@"Title@=!\_= ")]
|
||||
[InlineData(@"Title@=\@=* ")]
|
||||
[InlineData(@"Title@=\_=* ")]
|
||||
[InlineData(@"Title@=\==* ")]
|
||||
[InlineData(@"Title@=\!=* ")]
|
||||
[InlineData(@"Title@=!\@=* ")]
|
||||
public void CanFilterWithEscapedOperators(string filter)
|
||||
{
|
||||
var posts = new List<Post>
|
||||
{
|
||||
new Post
|
||||
{
|
||||
Id = 1,
|
||||
Title = @"Operators: == != > < >= <= @= _= !@= !_= @=* _=* ==* !=* !@=* !_=* ",
|
||||
LikeCount = 1,
|
||||
IsDraft = true,
|
||||
CategoryId = 1,
|
||||
TopComment = new Comment { Id = 1, Text = "F1" },
|
||||
FeaturedComment = new Comment { Id = 7, Text = "F2" }
|
||||
}
|
||||
}.AsQueryable();
|
||||
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = filter,
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, posts);
|
||||
var entry = result.FirstOrDefault();
|
||||
var resultCount = result.Count();
|
||||
|
||||
Assert.NotNull(entry);
|
||||
Assert.Equal(1, resultCount);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,33 +1,43 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Sieve.Exceptions;
|
||||
using Sieve.Models;
|
||||
using Sieve.Services;
|
||||
using SieveUnitTests.Abstractions.Entity;
|
||||
using SieveUnitTests.Entities;
|
||||
using SieveUnitTests.Services;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace SieveUnitTests
|
||||
{
|
||||
[TestClass]
|
||||
public class GeneralWithInterfaces
|
||||
{
|
||||
private readonly ITestOutputHelper _testOutputHelper;
|
||||
private readonly SieveProcessor _processor;
|
||||
private readonly SieveProcessor _nullableProcessor;
|
||||
private readonly IQueryable<IPost> _posts;
|
||||
private readonly IQueryable<Comment> _comments;
|
||||
|
||||
public GeneralWithInterfaces()
|
||||
public GeneralWithInterfaces(ITestOutputHelper testOutputHelper)
|
||||
{
|
||||
var nullableAccessor = new SieveOptionsAccessor();
|
||||
nullableAccessor.Value.IgnoreNullsOnNotEqual = false;
|
||||
|
||||
_testOutputHelper = testOutputHelper;
|
||||
_processor = new ApplicationSieveProcessor(new SieveOptionsAccessor(),
|
||||
new SieveCustomSortMethods(),
|
||||
new SieveCustomFilterMethods());
|
||||
|
||||
_nullableProcessor = new ApplicationSieveProcessor(nullableAccessor,
|
||||
new SieveCustomSortMethods(),
|
||||
new SieveCustomFilterMethods());
|
||||
|
||||
_posts = new List<IPost>
|
||||
{
|
||||
new Post() {
|
||||
new Post
|
||||
{
|
||||
Id = 0,
|
||||
Title = "A",
|
||||
LikeCount = 100,
|
||||
@ -36,7 +46,8 @@ namespace SieveUnitTests
|
||||
TopComment = new Comment { Id = 0, Text = "A1" },
|
||||
FeaturedComment = new Comment { Id = 4, Text = "A2" }
|
||||
},
|
||||
new Post() {
|
||||
new Post
|
||||
{
|
||||
Id = 1,
|
||||
Title = "B",
|
||||
LikeCount = 50,
|
||||
@ -45,7 +56,8 @@ namespace SieveUnitTests
|
||||
TopComment = new Comment { Id = 3, Text = "B1" },
|
||||
FeaturedComment = new Comment { Id = 5, Text = "B2" }
|
||||
},
|
||||
new Post() {
|
||||
new Post
|
||||
{
|
||||
Id = 2,
|
||||
Title = "C",
|
||||
LikeCount = 0,
|
||||
@ -53,7 +65,8 @@ namespace SieveUnitTests
|
||||
TopComment = new Comment { Id = 2, Text = "C1" },
|
||||
FeaturedComment = new Comment { Id = 6, Text = "C2" }
|
||||
},
|
||||
new Post() {
|
||||
new Post
|
||||
{
|
||||
Id = 3,
|
||||
Title = "D",
|
||||
LikeCount = 3,
|
||||
@ -66,17 +79,20 @@ namespace SieveUnitTests
|
||||
|
||||
_comments = new List<Comment>
|
||||
{
|
||||
new Comment() {
|
||||
new Comment
|
||||
{
|
||||
Id = 0,
|
||||
DateCreated = DateTimeOffset.UtcNow.AddDays(-20),
|
||||
Text = "This is an old comment."
|
||||
},
|
||||
new Comment() {
|
||||
new Comment
|
||||
{
|
||||
Id = 1,
|
||||
DateCreated = DateTimeOffset.UtcNow.AddDays(-1),
|
||||
Text = "This is a fairly new comment."
|
||||
},
|
||||
new Comment() {
|
||||
new Comment
|
||||
{
|
||||
Id = 2,
|
||||
DateCreated = DateTimeOffset.UtcNow,
|
||||
Text = "This is a brand new comment. (Text in braces)"
|
||||
@ -84,263 +100,280 @@ namespace SieveUnitTests
|
||||
}.AsQueryable();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void ContainsCanBeCaseInsensitive()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "Title@=*a"
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
|
||||
Assert.AreEqual(result.First().Id, 0);
|
||||
Assert.IsTrue(result.Count() == 1);
|
||||
Assert.Equal(0, result.First().Id);
|
||||
Assert.True(result.Count() == 1);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void NotEqualsCanBeCaseInsensitive()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "Title!=*a"
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
|
||||
Assert.AreEqual(result.First().Id, 1);
|
||||
Assert.IsTrue(result.Count() == 3);
|
||||
Assert.Equal(1, result.First().Id);
|
||||
Assert.True(result.Count() == 3);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void ContainsIsCaseSensitive()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "Title@=a",
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
|
||||
Assert.IsTrue(result.Count() == 0);
|
||||
Assert.True(!result.Any());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void NotContainsWorks()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "Title!@=D",
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
|
||||
Assert.IsTrue(result.Count() == 3);
|
||||
Assert.True(result.Count() == 3);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void CanFilterBools()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "IsDraft==false"
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
|
||||
Assert.IsTrue(result.Count() == 2);
|
||||
Assert.True(result.Count() == 2);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void CanSortBools()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Sorts = "-IsDraft"
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
|
||||
Assert.AreEqual(result.First().Id, 0);
|
||||
Assert.Equal(0, result.First().Id);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void CanFilterNullableInts()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "CategoryId==1"
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
var nullableResult = _nullableProcessor.Apply(model, _posts);
|
||||
|
||||
Assert.IsTrue(result.Count() == 2);
|
||||
Assert.True(result.Count() == 2);
|
||||
Assert.True(nullableResult.Count() == 2);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void EqualsDoesntFailWithNonStringTypes()
|
||||
[Fact]
|
||||
public void CanFilterNullableIntsWithNotEqual()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
{
|
||||
Filters = "CategoryId!=1"
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
var nullableResult = _nullableProcessor.Apply(model, _posts);
|
||||
|
||||
Assert.True(result.Count() == 1);
|
||||
Assert.True(nullableResult.Count() == 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EqualsDoesntFailWithNonStringTypes()
|
||||
{
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "LikeCount==50",
|
||||
};
|
||||
|
||||
Console.WriteLine(model.GetFiltersParsed()[0].Values);
|
||||
Console.WriteLine(model.GetFiltersParsed()[0].Operator);
|
||||
Console.WriteLine(model.GetFiltersParsed()[0].OperatorParsed);
|
||||
_testOutputHelper.WriteLine(model.GetFiltersParsed()[0].Values.ToString());
|
||||
_testOutputHelper.WriteLine(model.GetFiltersParsed()[0].Operator);
|
||||
_testOutputHelper.WriteLine(model.GetFiltersParsed()[0].OperatorParsed.ToString());
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
|
||||
Assert.AreEqual(result.First().Id, 1);
|
||||
Assert.IsTrue(result.Count() == 1);
|
||||
Assert.Equal(1, result.First().Id);
|
||||
Assert.True(result.Count() == 1);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void CustomFiltersWork()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "Isnew",
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
|
||||
Assert.IsFalse(result.Any(p => p.Id == 0));
|
||||
Assert.IsTrue(result.Count() == 3);
|
||||
Assert.False(result.Any(p => p.Id == 0));
|
||||
Assert.True(result.Count() == 3);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void CustomGenericFiltersWork()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "Latest",
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _comments);
|
||||
|
||||
Assert.IsFalse(result.Any(p => p.Id == 0));
|
||||
Assert.IsTrue(result.Count() == 2);
|
||||
Assert.False(result.Any(p => p.Id == 0));
|
||||
Assert.True(result.Count() == 2);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void CustomFiltersWithOperatorsWork()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "HasInTitle==A",
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
|
||||
Assert.IsTrue(result.Any(p => p.Id == 0));
|
||||
Assert.IsTrue(result.Count() == 1);
|
||||
Assert.True(result.Any(p => p.Id == 0));
|
||||
Assert.True(result.Count() == 1);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void CustomFiltersMixedWithUsualWork1()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "Isnew,CategoryId==2",
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
|
||||
Assert.IsTrue(result.Any(p => p.Id == 3));
|
||||
Assert.IsTrue(result.Count() == 1);
|
||||
Assert.True(result.Any(p => p.Id == 3));
|
||||
Assert.True(result.Count() == 1);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void CustomFiltersMixedWithUsualWork2()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "CategoryId==2,Isnew",
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
|
||||
Assert.IsTrue(result.Any(p => p.Id == 3));
|
||||
Assert.IsTrue(result.Count() == 1);
|
||||
Assert.True(result.Any(p => p.Id == 3));
|
||||
Assert.True(result.Count() == 1);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void CustomFiltersOnDifferentSourcesCanShareName()
|
||||
{
|
||||
var postModel = new SieveModel()
|
||||
var postModel = new SieveModel
|
||||
{
|
||||
Filters = "CategoryId==2,Isnew",
|
||||
};
|
||||
|
||||
var postResult = _processor.Apply(postModel, _posts);
|
||||
|
||||
Assert.IsTrue(postResult.Any(p => p.Id == 3));
|
||||
Assert.AreEqual(1, postResult.Count());
|
||||
Assert.True(postResult.Any(p => p.Id == 3));
|
||||
Assert.Equal(1, postResult.Count());
|
||||
|
||||
var commentModel = new SieveModel()
|
||||
var commentModel = new SieveModel
|
||||
{
|
||||
Filters = "Isnew",
|
||||
};
|
||||
|
||||
var commentResult = _processor.Apply(commentModel, _comments);
|
||||
|
||||
Assert.IsTrue(commentResult.Any(c => c.Id == 2));
|
||||
Assert.AreEqual(2, commentResult.Count());
|
||||
Assert.True(commentResult.Any(c => c.Id == 2));
|
||||
Assert.Equal(2, commentResult.Count());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void CustomSortsWork()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Sorts = "Popularity",
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
|
||||
Assert.IsFalse(result.First().Id == 0);
|
||||
Assert.False(result.First().Id == 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void CustomGenericSortsWork()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Sorts = "Oldest",
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
|
||||
Assert.IsTrue(result.Last().Id == 0);
|
||||
Assert.True(result.Last().Id == 0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void MethodNotFoundExceptionWork()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "does not exist",
|
||||
};
|
||||
|
||||
Assert.ThrowsException<SieveMethodNotFoundException>(() => _processor.Apply(model, _posts));
|
||||
Assert.Throws<SieveMethodNotFoundException>(() => _processor.Apply(model, _posts));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void IncompatibleMethodExceptionsWork()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "TestComment",
|
||||
};
|
||||
|
||||
Assert.ThrowsException<SieveIncompatibleMethodException>(() => _processor.Apply(model, _posts));
|
||||
Assert.Throws<SieveIncompatibleMethodException>(() => _processor.Apply(model, _posts));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void OrNameFilteringWorks()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "(Title|LikeCount)==3",
|
||||
};
|
||||
@ -349,17 +382,17 @@ namespace SieveUnitTests
|
||||
var entry = result.FirstOrDefault();
|
||||
var resultCount = result.Count();
|
||||
|
||||
Assert.IsNotNull(entry);
|
||||
Assert.AreEqual(1, resultCount);
|
||||
Assert.AreEqual(3, entry.Id);
|
||||
Assert.NotNull(entry);
|
||||
Assert.Equal(1, resultCount);
|
||||
Assert.Equal(3, entry.Id);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("CategoryId==1,(CategoryId|LikeCount)==50")]
|
||||
[DataRow("(CategoryId|LikeCount)==50,CategoryId==1")]
|
||||
[Theory]
|
||||
[InlineData("CategoryId==1,(CategoryId|LikeCount)==50")]
|
||||
[InlineData("(CategoryId|LikeCount)==50,CategoryId==1")]
|
||||
public void CombinedAndOrFilterIndependentOfOrder(string filter)
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = filter,
|
||||
};
|
||||
@ -368,14 +401,14 @@ namespace SieveUnitTests
|
||||
var entry = result.FirstOrDefault();
|
||||
var resultCount = result.Count();
|
||||
|
||||
Assert.IsNotNull(entry);
|
||||
Assert.AreEqual(1, resultCount);
|
||||
Assert.NotNull(entry);
|
||||
Assert.Equal(1, resultCount);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void CombinedAndOrWithSpaceFilteringWorks()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "Title==D, (Title|LikeCount)==3",
|
||||
};
|
||||
@ -384,97 +417,98 @@ namespace SieveUnitTests
|
||||
var entry = result.FirstOrDefault();
|
||||
var resultCount = result.Count();
|
||||
|
||||
Assert.IsNotNull(entry);
|
||||
Assert.AreEqual(1, resultCount);
|
||||
Assert.AreEqual(3, entry.Id);
|
||||
Assert.NotNull(entry);
|
||||
Assert.Equal(1, resultCount);
|
||||
Assert.Equal(3, entry.Id);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void OrValueFilteringWorks()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "Title==C|D",
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
Assert.AreEqual(2, result.Count());
|
||||
Assert.IsTrue(result.Any(p => p.Id == 2));
|
||||
Assert.IsTrue(result.Any(p => p.Id == 3));
|
||||
Assert.Equal(2, result.Count());
|
||||
Assert.True(result.Any(p => p.Id == 2));
|
||||
Assert.True(result.Any(p => p.Id == 3));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void OrValueFilteringWorks2()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "Text@=(|)",
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _comments);
|
||||
Assert.AreEqual(1, result.Count());
|
||||
Assert.AreEqual(2, result.FirstOrDefault().Id);
|
||||
Assert.Equal(1, result.Count());
|
||||
Assert.Equal(2, result.FirstOrDefault()?.Id);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void NestedFilteringWorks()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "TopComment.Text!@=A",
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
Assert.AreEqual(3, result.Count());
|
||||
Assert.Equal(3, result.Count());
|
||||
var posts = result.ToList();
|
||||
Assert.IsTrue(posts[0].TopComment.Text.Contains("B"));
|
||||
Assert.IsTrue(posts[1].TopComment.Text.Contains("C"));
|
||||
Assert.IsTrue(posts[2].TopComment.Text.Contains("D"));
|
||||
Assert.Contains("B", posts[0].TopComment.Text);
|
||||
Assert.Contains("C", posts[1].TopComment.Text);
|
||||
Assert.Contains("D", posts[2].TopComment.Text);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void NestedSortingWorks()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Sorts = "TopComment.Id",
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
Assert.AreEqual(4, result.Count());
|
||||
Assert.Equal(4, result.Count());
|
||||
var posts = result.ToList();
|
||||
Assert.AreEqual(posts[0].Id, 0);
|
||||
Assert.AreEqual(posts[1].Id, 3);
|
||||
Assert.AreEqual(posts[2].Id, 2);
|
||||
Assert.AreEqual(posts[3].Id, 1);
|
||||
Assert.Equal(0, posts[0].Id);
|
||||
Assert.Equal(3, posts[1].Id);
|
||||
Assert.Equal(2, posts[2].Id);
|
||||
Assert.Equal(1, posts[3].Id);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void NestedFilteringWithIdenticTypesWorks()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "(topc|featc)@=*2",
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
Assert.AreEqual(4, result.Count());
|
||||
Assert.Equal(4, result.Count());
|
||||
|
||||
model = new SieveModel()
|
||||
model = new SieveModel
|
||||
{
|
||||
Filters = "(topc|featc)@=*B",
|
||||
};
|
||||
|
||||
result = _processor.Apply(model, _posts);
|
||||
Assert.AreEqual(1, result.Count());
|
||||
Assert.Equal(1, result.Count());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void FilteringNullsWorks()
|
||||
{
|
||||
var posts = new List<Post>
|
||||
{
|
||||
new Post() {
|
||||
new Post
|
||||
{
|
||||
Id = 1,
|
||||
Title = null,
|
||||
LikeCount = 0,
|
||||
@ -485,21 +519,22 @@ namespace SieveUnitTests
|
||||
},
|
||||
}.AsQueryable();
|
||||
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "FeaturedComment.Text!@=Some value",
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, posts);
|
||||
Assert.AreEqual(0, result.Count());
|
||||
Assert.Equal(0, result.Count());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void SortingNullsWorks()
|
||||
{
|
||||
var posts = new List<Post>
|
||||
{
|
||||
new Post() {
|
||||
new Post
|
||||
{
|
||||
Id = 1,
|
||||
Title = null,
|
||||
LikeCount = 0,
|
||||
@ -508,7 +543,8 @@ namespace SieveUnitTests
|
||||
TopComment = new Comment { Id = 1 },
|
||||
FeaturedComment = null
|
||||
},
|
||||
new Post() {
|
||||
new Post
|
||||
{
|
||||
Id = 2,
|
||||
Title = null,
|
||||
LikeCount = 0,
|
||||
@ -519,24 +555,25 @@ namespace SieveUnitTests
|
||||
},
|
||||
}.AsQueryable();
|
||||
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Sorts = "TopComment.Id",
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, posts);
|
||||
Assert.AreEqual(2, result.Count());
|
||||
Assert.Equal(2, result.Count());
|
||||
var sortedPosts = result.ToList();
|
||||
Assert.AreEqual(sortedPosts[0].Id, 2);
|
||||
Assert.AreEqual(sortedPosts[1].Id, 1);
|
||||
Assert.Equal(2, sortedPosts[0].Id);
|
||||
Assert.Equal(1, sortedPosts[1].Id);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void FilteringOnNullWorks()
|
||||
{
|
||||
var posts = new List<Post>
|
||||
{
|
||||
new Post() {
|
||||
new Post
|
||||
{
|
||||
Id = 1,
|
||||
Title = null,
|
||||
LikeCount = 0,
|
||||
@ -545,7 +582,8 @@ namespace SieveUnitTests
|
||||
TopComment = null,
|
||||
FeaturedComment = null
|
||||
},
|
||||
new Post() {
|
||||
new Post
|
||||
{
|
||||
Id = 2,
|
||||
Title = null,
|
||||
LikeCount = 0,
|
||||
@ -556,33 +594,33 @@ namespace SieveUnitTests
|
||||
},
|
||||
}.AsQueryable();
|
||||
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "FeaturedComment.Text==null",
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, posts);
|
||||
Assert.AreEqual(1, result.Count());
|
||||
Assert.Equal(1, result.Count());
|
||||
var filteredPosts = result.ToList();
|
||||
Assert.AreEqual(filteredPosts[0].Id, 2);
|
||||
Assert.Equal(2, filteredPosts[0].Id);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Fact]
|
||||
public void BaseDefinedPropertyMappingSortingWorks_WithCustomName()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Sorts = "-CreateDate"
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
Assert.AreEqual(4, result.Count());
|
||||
Assert.Equal(4, result.Count());
|
||||
|
||||
var posts = result.ToList();
|
||||
Assert.AreEqual(posts[0].Id, 3);
|
||||
Assert.AreEqual(posts[1].Id, 2);
|
||||
Assert.AreEqual(posts[2].Id, 1);
|
||||
Assert.AreEqual(posts[3].Id, 0);
|
||||
Assert.Equal(3,posts[0].Id);
|
||||
Assert.Equal(2,posts[1].Id);
|
||||
Assert.Equal(1,posts[2].Id);
|
||||
Assert.Equal(0,posts[3].Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,40 +1,38 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Sieve.Exceptions;
|
||||
using Sieve.Models;
|
||||
using Sieve.Services;
|
||||
using SieveUnitTests.Entities;
|
||||
using SieveUnitTests.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace SieveUnitTests
|
||||
{
|
||||
[TestClass]
|
||||
public class Mapper
|
||||
{
|
||||
private readonly ApplicationSieveProcessor _processor;
|
||||
private readonly IQueryable<Post> _posts;
|
||||
|
||||
public Mapper()
|
||||
{
|
||||
_processor = new ApplicationSieveProcessor(new SieveOptionsAccessor(),
|
||||
new SieveCustomSortMethods(),
|
||||
new SieveCustomFilterMethods());
|
||||
|
||||
_posts = new List<Post>
|
||||
{
|
||||
new Post() {
|
||||
new Post
|
||||
{
|
||||
Id = 1,
|
||||
ThisHasNoAttributeButIsAccessible = "A",
|
||||
ThisHasNoAttribute = "A",
|
||||
OnlySortableViaFluentApi = 100
|
||||
},
|
||||
new Post() {
|
||||
new Post
|
||||
{
|
||||
Id = 2,
|
||||
ThisHasNoAttributeButIsAccessible = "B",
|
||||
ThisHasNoAttribute = "B",
|
||||
OnlySortableViaFluentApi = 50
|
||||
},
|
||||
new Post() {
|
||||
new Post
|
||||
{
|
||||
Id = 3,
|
||||
ThisHasNoAttributeButIsAccessible = "C",
|
||||
ThisHasNoAttribute = "C",
|
||||
@ -43,43 +41,63 @@ namespace SieveUnitTests
|
||||
}.AsQueryable();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MapperWorks()
|
||||
/// <summary>
|
||||
/// Processors with the same mappings but configured via a different method.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static IEnumerable<object[]> GetProcessors()
|
||||
{
|
||||
var model = new SieveModel()
|
||||
yield return new object[] {
|
||||
new ApplicationSieveProcessor(
|
||||
new SieveOptionsAccessor(),
|
||||
new SieveCustomSortMethods(),
|
||||
new SieveCustomFilterMethods())};
|
||||
yield return new object[] {
|
||||
new ModularConfigurationSieveProcessor(
|
||||
new SieveOptionsAccessor(),
|
||||
new SieveCustomSortMethods(),
|
||||
new SieveCustomFilterMethods())};
|
||||
yield return new object[] {
|
||||
new ModularConfigurationWithScanSieveProcessor(
|
||||
new SieveOptionsAccessor(),
|
||||
new SieveCustomSortMethods(),
|
||||
new SieveCustomFilterMethods())};
|
||||
}
|
||||
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(GetProcessors))]
|
||||
public void MapperWorks(ISieveProcessor processor)
|
||||
{
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "shortname@=A",
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts);
|
||||
var result = processor.Apply(model, _posts);
|
||||
|
||||
Assert.AreEqual(result.First().ThisHasNoAttributeButIsAccessible, "A");
|
||||
Assert.Equal("A", result.First().ThisHasNoAttributeButIsAccessible);
|
||||
|
||||
Assert.IsTrue(result.Count() == 1);
|
||||
Assert.True(result.Count() == 1);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MapperSortOnlyWorks()
|
||||
[Theory]
|
||||
[MemberData(nameof(GetProcessors))]
|
||||
public void MapperSortOnlyWorks(ISieveProcessor processor)
|
||||
{
|
||||
var model = new SieveModel()
|
||||
var model = new SieveModel
|
||||
{
|
||||
Filters = "OnlySortableViaFluentApi@=50",
|
||||
Sorts = "OnlySortableViaFluentApi"
|
||||
};
|
||||
|
||||
var result = _processor.Apply(model, _posts, applyFiltering: false, applyPagination: false);
|
||||
var result = processor.Apply(model, _posts, applyFiltering: false, applyPagination: false);
|
||||
|
||||
Assert.ThrowsException<SieveMethodNotFoundException>(() => _processor.Apply(model, _posts));
|
||||
Assert.Throws<SieveMethodNotFoundException>(() => processor.Apply(model, _posts));
|
||||
|
||||
Assert.AreEqual(result.First().Id, 3);
|
||||
Assert.Equal(3, result.First().Id);
|
||||
|
||||
Assert.IsTrue(result.Count() == 3);
|
||||
Assert.True(result.Count() == 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
//Sorts = "LikeCount",
|
||||
//Page = 1,
|
||||
//PageSize = 10
|
||||
//
|
@ -0,0 +1,26 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using Sieve.Models;
|
||||
using Sieve.Services;
|
||||
using SieveUnitTests.Abstractions.Entity;
|
||||
using SieveUnitTests.Entities;
|
||||
|
||||
namespace SieveUnitTests.Services
|
||||
{
|
||||
public class ModularConfigurationSieveProcessor : SieveProcessor
|
||||
{
|
||||
public ModularConfigurationSieveProcessor(
|
||||
IOptions<SieveOptions> options,
|
||||
ISieveCustomSortMethods customSortMethods,
|
||||
ISieveCustomFilterMethods customFilterMethods)
|
||||
: base(options, customSortMethods, customFilterMethods)
|
||||
{
|
||||
}
|
||||
|
||||
protected override SievePropertyMapper MapProperties(SievePropertyMapper mapper)
|
||||
{
|
||||
return mapper
|
||||
.ApplyConfiguration<SieveConfigurationForPost>()
|
||||
.ApplyConfiguration<SieveConfigurationForIPost>();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using Sieve.Models;
|
||||
using Sieve.Services;
|
||||
|
||||
namespace SieveUnitTests.Services
|
||||
{
|
||||
public class ModularConfigurationWithScanSieveProcessor : SieveProcessor
|
||||
{
|
||||
public ModularConfigurationWithScanSieveProcessor(
|
||||
IOptions<SieveOptions> options,
|
||||
ISieveCustomSortMethods customSortMethods,
|
||||
ISieveCustomFilterMethods customFilterMethods)
|
||||
: base(options, customSortMethods, customFilterMethods)
|
||||
{
|
||||
}
|
||||
|
||||
protected override SievePropertyMapper MapProperties(SievePropertyMapper mapper) =>
|
||||
mapper.ApplyConfigurationsFromAssembly(typeof(ModularConfigurationWithScanSieveProcessor).Assembly);
|
||||
}
|
||||
}
|
@ -1,15 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="1.2.0" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="1.2.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="2.0.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="1.3.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
168
SieveUnitTests/StringFilterNullTests.cs
Normal file
168
SieveUnitTests/StringFilterNullTests.cs
Normal file
@ -0,0 +1,168 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Sieve.Models;
|
||||
using Sieve.Services;
|
||||
using SieveUnitTests.Entities;
|
||||
using SieveUnitTests.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace SieveUnitTests
|
||||
{
|
||||
public class StringFilterNullTests
|
||||
{
|
||||
private readonly IQueryable<Comment> _comments;
|
||||
private readonly SieveProcessor _processor;
|
||||
|
||||
public StringFilterNullTests()
|
||||
{
|
||||
_processor = new SieveProcessor(new SieveOptionsAccessor());
|
||||
|
||||
_comments = new List<Comment>
|
||||
{
|
||||
new Comment
|
||||
{
|
||||
Id = 0,
|
||||
DateCreated = DateTimeOffset.UtcNow,
|
||||
Text = "This text contains null somewhere in the middle of a string",
|
||||
Author = "Dog",
|
||||
},
|
||||
new Comment
|
||||
{
|
||||
Id = 1,
|
||||
DateCreated = DateTimeOffset.UtcNow,
|
||||
Text = "null is here in the text",
|
||||
Author = "Cat",
|
||||
},
|
||||
new Comment
|
||||
{
|
||||
Id = 2,
|
||||
DateCreated = DateTimeOffset.UtcNow,
|
||||
Text = "Regular comment without n*ll",
|
||||
Author = "Mouse",
|
||||
},
|
||||
new Comment
|
||||
{
|
||||
Id = 100,
|
||||
DateCreated = DateTimeOffset.UtcNow,
|
||||
Text = null,
|
||||
Author = "null",
|
||||
}
|
||||
}.AsQueryable();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Text==null")]
|
||||
[InlineData("Text==*null")]
|
||||
public void Filter_Equals_Null(string filter)
|
||||
{
|
||||
var model = new SieveModel {Filters = filter};
|
||||
|
||||
var result = _processor.Apply(model, _comments);
|
||||
|
||||
Assert.Equal(100, result.Single().Id);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Text!=null")]
|
||||
[InlineData("Text!=*null")]
|
||||
public void Filter_NotEquals_Null(string filter)
|
||||
{
|
||||
var model = new SieveModel {Filters = filter};
|
||||
|
||||
var result = _processor.Apply(model, _comments);
|
||||
|
||||
Assert.Equal(new[] {0, 1, 2}, result.Select(p => p.Id));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Text@=null")]
|
||||
[InlineData("Text@=*null")]
|
||||
[InlineData("Text@=*NULL")]
|
||||
[InlineData("Text@=*NulL")]
|
||||
[InlineData("Text@=*null|text")]
|
||||
public void Filter_Contains_NullString(string filter)
|
||||
{
|
||||
var model = new SieveModel {Filters = filter};
|
||||
|
||||
var result = _processor.Apply(model, _comments);
|
||||
|
||||
Assert.Equal(new[] {0, 1}, result.Select(p => p.Id));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Text|Author==null", 100)]
|
||||
[InlineData("Text|Author@=null", 0, 1, 100)]
|
||||
[InlineData("Text|Author@=*null", 0, 1, 100)]
|
||||
[InlineData("Text|Author_=null", 1, 100)]
|
||||
[InlineData("Text|Author_=*null", 1, 100)]
|
||||
public void MultiFilter_Contains_NullString(string filter, params int[] expectedIds)
|
||||
{
|
||||
var model = new SieveModel {Filters = filter};
|
||||
|
||||
var result = _processor.Apply(model, _comments);
|
||||
|
||||
Assert.Equal(expectedIds, result.Select(p => p.Id));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"Author==\null", 100)]
|
||||
[InlineData(@"Author==*\null", 100)]
|
||||
[InlineData(@"Author==*\NuLl", 100)]
|
||||
[InlineData(@"Author!=*\null", 0, 1, 2)]
|
||||
[InlineData(@"Author!=*\NulL", 0, 1, 2)]
|
||||
[InlineData(@"Author!=\null", 0, 1, 2)]
|
||||
public void SingleFilter_Equals_NullStringEscaped(string filter, params int[] expectedIds)
|
||||
{
|
||||
var model = new SieveModel {Filters = filter};
|
||||
|
||||
var result = _processor.Apply(model, _comments);
|
||||
|
||||
Assert.Equal(expectedIds, result.Select(p => p.Id));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Text_=null")]
|
||||
[InlineData("Text_=*null")]
|
||||
[InlineData("Text_=*NULL")]
|
||||
[InlineData("Text_=*NulL")]
|
||||
[InlineData("Text_=*null|text")]
|
||||
public void Filter_StartsWith_NullString(string filter)
|
||||
{
|
||||
var model = new SieveModel {Filters = filter};
|
||||
|
||||
var result = _processor.Apply(model, _comments);
|
||||
|
||||
Assert.Equal(new[] {1}, result.Select(p => p.Id));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Text!@=null")]
|
||||
[InlineData("Text!@=*null")]
|
||||
[InlineData("Text!@=*NULL")]
|
||||
[InlineData("Text!@=*NulL")]
|
||||
[InlineData("Text!@=*null|text")]
|
||||
public void Filter_DoesNotContain_NullString(string filter)
|
||||
{
|
||||
var model = new SieveModel {Filters = filter};
|
||||
|
||||
var result = _processor.Apply(model, _comments);
|
||||
|
||||
Assert.Equal(new[] {2}, result.Select(p => p.Id));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Text!_=null")]
|
||||
[InlineData("Text!_=*null")]
|
||||
[InlineData("Text!_=*NULL")]
|
||||
[InlineData("Text!_=*NulL")]
|
||||
public void Filter_DoesNotStartsWith_NullString(string filter)
|
||||
{
|
||||
var model = new SieveModel {Filters = filter};
|
||||
|
||||
var result = _processor.Apply(model, _comments);
|
||||
|
||||
Assert.Equal(new[] {0, 2}, result.Select(p => p.Id));
|
||||
}
|
||||
}
|
||||
}
|
7
build.cmd
Executable file
7
build.cmd
Executable file
@ -0,0 +1,7 @@
|
||||
:; set -eo pipefail
|
||||
:; SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
|
||||
:; ${SCRIPT_DIR}/build.sh "$@"
|
||||
:; exit $?
|
||||
|
||||
@ECHO OFF
|
||||
powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0build.ps1" %*
|
69
build.ps1
Normal file
69
build.ps1
Normal file
@ -0,0 +1,69 @@
|
||||
[CmdletBinding()]
|
||||
Param(
|
||||
[Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)]
|
||||
[string[]]$BuildArguments
|
||||
)
|
||||
|
||||
Write-Output "PowerShell $($PSVersionTable.PSEdition) version $($PSVersionTable.PSVersion)"
|
||||
|
||||
Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { Write-Error $_ -ErrorAction Continue; exit 1 }
|
||||
$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent
|
||||
|
||||
###########################################################################
|
||||
# CONFIGURATION
|
||||
###########################################################################
|
||||
|
||||
$BuildProjectFile = "$PSScriptRoot\build\_build.csproj"
|
||||
$TempDirectory = "$PSScriptRoot\\.nuke\temp"
|
||||
|
||||
$DotNetGlobalFile = "$PSScriptRoot\\global.json"
|
||||
$DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1"
|
||||
$DotNetChannel = "Current"
|
||||
|
||||
$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 1
|
||||
$env:DOTNET_CLI_TELEMETRY_OPTOUT = 1
|
||||
$env:DOTNET_MULTILEVEL_LOOKUP = 0
|
||||
|
||||
###########################################################################
|
||||
# EXECUTION
|
||||
###########################################################################
|
||||
|
||||
function ExecSafe([scriptblock] $cmd) {
|
||||
& $cmd
|
||||
if ($LASTEXITCODE) { exit $LASTEXITCODE }
|
||||
}
|
||||
|
||||
# If dotnet CLI is installed globally and it matches requested version, use for execution
|
||||
if ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue) -and `
|
||||
$(dotnet --version) -and $LASTEXITCODE -eq 0) {
|
||||
$env:DOTNET_EXE = (Get-Command "dotnet").Path
|
||||
}
|
||||
else {
|
||||
# Download install script
|
||||
$DotNetInstallFile = "$TempDirectory\dotnet-install.ps1"
|
||||
New-Item -ItemType Directory -Path $TempDirectory -Force | Out-Null
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
(New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile)
|
||||
|
||||
# If global.json exists, load expected version
|
||||
if (Test-Path $DotNetGlobalFile) {
|
||||
$DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json)
|
||||
if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) {
|
||||
$DotNetVersion = $DotNetGlobal.sdk.version
|
||||
}
|
||||
}
|
||||
|
||||
# Install by channel or version
|
||||
$DotNetDirectory = "$TempDirectory\dotnet-win"
|
||||
if (!(Test-Path variable:DotNetVersion)) {
|
||||
ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath }
|
||||
} else {
|
||||
ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath }
|
||||
}
|
||||
$env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe"
|
||||
}
|
||||
|
||||
Write-Output "Microsoft (R) .NET Core SDK version $(& $env:DOTNET_EXE --version)"
|
||||
|
||||
ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet }
|
||||
ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments }
|
62
build.sh
Executable file
62
build.sh
Executable file
@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
bash --version 2>&1 | head -n 1
|
||||
|
||||
set -eo pipefail
|
||||
SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
|
||||
|
||||
###########################################################################
|
||||
# CONFIGURATION
|
||||
###########################################################################
|
||||
|
||||
BUILD_PROJECT_FILE="$SCRIPT_DIR/build/_build.csproj"
|
||||
TEMP_DIRECTORY="$SCRIPT_DIR//.nuke/temp"
|
||||
|
||||
DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json"
|
||||
DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh"
|
||||
DOTNET_CHANNEL="Current"
|
||||
|
||||
export DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
|
||||
export DOTNET_MULTILEVEL_LOOKUP=0
|
||||
|
||||
###########################################################################
|
||||
# EXECUTION
|
||||
###########################################################################
|
||||
|
||||
function FirstJsonValue {
|
||||
perl -nle 'print $1 if m{"'"$1"'": "([^"]+)",?}' <<< "${@:2}"
|
||||
}
|
||||
|
||||
# If dotnet CLI is installed globally and it matches requested version, use for execution
|
||||
if [ -x "$(command -v dotnet)" ] && dotnet --version &>/dev/null; then
|
||||
export DOTNET_EXE="$(command -v dotnet)"
|
||||
else
|
||||
# Download install script
|
||||
DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh"
|
||||
mkdir -p "$TEMP_DIRECTORY"
|
||||
curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL"
|
||||
chmod +x "$DOTNET_INSTALL_FILE"
|
||||
|
||||
# If global.json exists, load expected version
|
||||
if [[ -f "$DOTNET_GLOBAL_FILE" ]]; then
|
||||
DOTNET_VERSION=$(FirstJsonValue "version" "$(cat "$DOTNET_GLOBAL_FILE")")
|
||||
if [[ "$DOTNET_VERSION" == "" ]]; then
|
||||
unset DOTNET_VERSION
|
||||
fi
|
||||
fi
|
||||
|
||||
# Install by channel or version
|
||||
DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix"
|
||||
if [[ -z ${DOTNET_VERSION+x} ]]; then
|
||||
"$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path
|
||||
else
|
||||
"$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path
|
||||
fi
|
||||
export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet"
|
||||
fi
|
||||
|
||||
echo "Microsoft (R) .NET Core SDK version $("$DOTNET_EXE" --version)"
|
||||
|
||||
"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet
|
||||
"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@"
|
11
build/.editorconfig
Normal file
11
build/.editorconfig
Normal file
@ -0,0 +1,11 @@
|
||||
[*.cs]
|
||||
dotnet_style_qualification_for_field = false:warning
|
||||
dotnet_style_qualification_for_property = false:warning
|
||||
dotnet_style_qualification_for_method = false:warning
|
||||
dotnet_style_qualification_for_event = false:warning
|
||||
dotnet_style_require_accessibility_modifiers = never:warning
|
||||
|
||||
csharp_style_expression_bodied_methods = true:silent
|
||||
csharp_style_expression_bodied_properties = true:warning
|
||||
csharp_style_expression_bodied_indexers = true:warning
|
||||
csharp_style_expression_bodied_accessors = true:warning
|
123
build/Build.cs
Normal file
123
build/Build.cs
Normal file
@ -0,0 +1,123 @@
|
||||
using System.Linq;
|
||||
using GlobExpressions;
|
||||
using Nuke.Common;
|
||||
using Nuke.Common.CI;
|
||||
using Nuke.Common.CI.GitHubActions;
|
||||
using Nuke.Common.Execution;
|
||||
using Nuke.Common.Git;
|
||||
using Nuke.Common.IO;
|
||||
using Nuke.Common.ProjectModel;
|
||||
using Nuke.Common.Tools.DotNet;
|
||||
using Nuke.Common.Tools.GitVersion;
|
||||
using Nuke.Common.Utilities.Collections;
|
||||
using static Nuke.Common.IO.FileSystemTasks;
|
||||
using static Nuke.Common.Tools.DotNet.DotNetTasks;
|
||||
|
||||
[CheckBuildProjectConfigurations]
|
||||
[ShutdownDotNetAfterServerBuild]
|
||||
[GitHubActions("ci", GitHubActionsImage.UbuntuLatest,
|
||||
OnPullRequestBranches = new[] {"master", "releases/*"},
|
||||
AutoGenerate = true,
|
||||
InvokedTargets = new[] {nameof(Ci)},
|
||||
CacheKeyFiles = new string[0])]
|
||||
[GitHubActions("ci_publish", GitHubActionsImage.UbuntuLatest,
|
||||
OnPushBranches = new[] { "releases/*" },
|
||||
OnPushTags = new[] { "v*" },
|
||||
AutoGenerate = true,
|
||||
InvokedTargets = new[] {nameof(CiPublish)},
|
||||
CacheKeyFiles = new string[0],
|
||||
ImportSecrets = new[] {"NUGET_API_KEY"})]
|
||||
class Build : NukeBuild
|
||||
{
|
||||
[Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")]
|
||||
readonly Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release;
|
||||
|
||||
[GitRepository] readonly GitRepository GitRepository;
|
||||
|
||||
[GitVersion(Framework = "netcoreapp3.1")] readonly GitVersion GitVersion;
|
||||
|
||||
[Solution] readonly Solution Solution;
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
[Parameter] string NUGET_API_KEY;
|
||||
|
||||
Project SieveProject => Solution.AllProjects.First(p => p.Name == "Sieve");
|
||||
|
||||
AbsolutePath OutputDirectory => RootDirectory / "output";
|
||||
|
||||
Target Clean => _ => _
|
||||
.Executes(() =>
|
||||
{
|
||||
DotNetClean();
|
||||
EnsureCleanDirectory(OutputDirectory);
|
||||
});
|
||||
|
||||
Target Restore => _ => _
|
||||
.DependsOn(Clean)
|
||||
.Executes(() =>
|
||||
{
|
||||
DotNetRestore(s => s
|
||||
.SetProjectFile(Solution));
|
||||
});
|
||||
|
||||
Target Compile => _ => _
|
||||
.DependsOn(Restore)
|
||||
.Executes(() =>
|
||||
{
|
||||
DotNetBuild(s => s
|
||||
.SetProjectFile(Solution)
|
||||
.SetConfiguration(Configuration)
|
||||
.SetAssemblyVersion(GitVersion.AssemblySemVer)
|
||||
.SetFileVersion(GitVersion.AssemblySemFileVer)
|
||||
.SetInformationalVersion(GitVersion.InformationalVersion)
|
||||
.EnableNoRestore());
|
||||
});
|
||||
|
||||
Target Test => _ => _
|
||||
.DependsOn(Compile)
|
||||
.Executes(() =>
|
||||
{
|
||||
DotNetTest(s => s
|
||||
.SetProjectFile(Solution));
|
||||
});
|
||||
|
||||
Target Package => _ => _
|
||||
.DependsOn(Test)
|
||||
.Executes(() =>
|
||||
|
||||
{
|
||||
DotNetPack(s => s
|
||||
.SetProject(SieveProject)
|
||||
.SetConfiguration(Configuration)
|
||||
.SetOutputDirectory(OutputDirectory)
|
||||
.SetVersion(GitVersion.NuGetVersionV2)
|
||||
.EnableNoRestore()
|
||||
.EnableNoBuild());
|
||||
});
|
||||
|
||||
Target Publish => _ => _
|
||||
.DependsOn(Package)
|
||||
.Requires(() => IsServerBuild)
|
||||
.Requires(() => NUGET_API_KEY)
|
||||
.Requires(() => Configuration.Equals(Configuration.Release))
|
||||
.Executes(() =>
|
||||
{
|
||||
Glob.Files(OutputDirectory, "*.nupkg")
|
||||
.NotEmpty()
|
||||
.ForEach(x =>
|
||||
{
|
||||
DotNetNuGetPush(s => s
|
||||
.SetTargetPath(OutputDirectory / x)
|
||||
.SetSource("https://api.nuget.org/v3/index.json")
|
||||
.SetApiKey(NUGET_API_KEY));
|
||||
});
|
||||
});
|
||||
|
||||
Target Ci => _ => _
|
||||
.DependsOn(Test);
|
||||
|
||||
Target CiPublish => _ => _
|
||||
.DependsOn(Publish);
|
||||
|
||||
public static int Main() => Execute<Build>(x => x.Package);
|
||||
}
|
16
build/Configuration.cs
Normal file
16
build/Configuration.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using Nuke.Common.Tooling;
|
||||
|
||||
[TypeConverter(typeof(TypeConverter<Configuration>))]
|
||||
public class Configuration : Enumeration
|
||||
{
|
||||
public static Configuration Debug = new Configuration { Value = nameof(Debug) };
|
||||
public static Configuration Release = new Configuration { Value = nameof(Release) };
|
||||
|
||||
public static implicit operator string(Configuration configuration)
|
||||
{
|
||||
return configuration.Value;
|
||||
}
|
||||
}
|
8
build/Directory.Build.props
Normal file
8
build/Directory.Build.props
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
|
||||
<!-- This file prevents unintended imports of unrelated MSBuild files -->
|
||||
<!-- Uncomment to include parent Directory.Build.props file -->
|
||||
<!--<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />-->
|
||||
|
||||
</Project>
|
8
build/Directory.Build.targets
Normal file
8
build/Directory.Build.targets
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
|
||||
<!-- This file prevents unintended imports of unrelated MSBuild files -->
|
||||
<!-- Uncomment to include parent Directory.Build.targets file -->
|
||||
<!--<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.targets', '$(MSBuildThisFileDirectory)../'))" />-->
|
||||
|
||||
</Project>
|
17
build/_build.csproj
Normal file
17
build/_build.csproj
Normal file
@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<RootNamespace></RootNamespace>
|
||||
<NoWarn>CS0649;CS0169</NoWarn>
|
||||
<NukeRootDirectory>..</NukeRootDirectory>
|
||||
<NukeScriptDirectory>..</NukeScriptDirectory>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Nuke.Common" Version="5.1.1" />
|
||||
<PackageDownload Include="GitVersion.Tool" Version="[5.6.7]" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
27
build/_build.csproj.DotSettings
Normal file
27
build/_build.csproj.DotSettings
Normal file
@ -0,0 +1,27 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=HeapView_002EDelegateAllocation/@EntryIndexedValue">DO_NOT_SHOW</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=VariableHidesOuterVariable/@EntryIndexedValue">DO_NOT_SHOW</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ClassNeverInstantiated_002EGlobal/@EntryIndexedValue">DO_NOT_SHOW</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MemberCanBeMadeStatic_002ELocal/@EntryIndexedValue">DO_NOT_SHOW</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/DEFAULT_INTERNAL_MODIFIER/@EntryValue">Implicit</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/DEFAULT_PRIVATE_MODIFIER/@EntryValue">Implicit</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/METHOD_OR_OPERATOR_BODY/@EntryValue">ExpressionBody</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/ThisQualifier/INSTANCE_MEMBERS_QUALIFY_MEMBERS/@EntryValue">0</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ANONYMOUS_METHOD_DECLARATION_BRACES/@EntryValue">NEXT_LINE</s:String>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/KEEP_USER_LINEBREAKS/@EntryValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_AFTER_INVOCATION_LPAR/@EntryValue">False</s:Boolean>
|
||||
<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/MAX_ATTRIBUTE_LENGTH_FOR_SAME_LINE/@EntryValue">120</s:Int64>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_FIELD_ATTRIBUTE_ON_SAME_LINE_EX/@EntryValue">IF_OWNER_IS_SINGLE_LINE</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_ARGUMENTS_STYLE/@EntryValue">WRAP_IF_LONG</s:String>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_SIMPLE_ANONYMOUSMETHOD_ON_SINGLE_LINE/@EntryValue">False</s:Boolean>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticFields/@EntryIndexedValue"><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></s:String>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpAttributeForSingleLineMethodUpgrade/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpRenamePlacementToArrangementMigration/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAddAccessorOwnerDeclarationBracesMigration/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002ECSharpPlaceAttributeOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateThisQualifierSettings/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
Loading…
Reference in New Issue
Block a user