Merge branch 'master' into master

This commit is contained in:
Hasan Manzak 2022-01-13 00:56:47 +03:00 committed by GitHub
commit b992778f0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 2150 additions and 1060 deletions

38
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View 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
View 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
View 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
View 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
View File

@ -260,4 +260,10 @@ paket-files/
# Python Tools for Visual Studio (PTVS) # Python Tools for Visual Studio (PTVS)
__pycache__/ __pycache__/
*.pyc *.pyc
# Nuke output
/output
# Sample database
Sieve.Sample/Sieve.db

115
.nuke/build.schema.json Normal file
View 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
View File

@ -0,0 +1,4 @@
{
"$schema": "./build.schema.json",
"Solution": "Sieve.sln"
}

3
GitVersion.yml Normal file
View File

@ -0,0 +1,3 @@
branches:
release:
mode: ContinuousDeployment

190
LICENSE
View File

@ -1,192 +1,4 @@
Apache License Copyright 2018 Biarity, 2021 Ashish Patel and Kevin Sommer
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]
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@ -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**. ⚗️ 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. 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/) [Get Sieve on nuget](https://www.nuget.org/packages/Sieve/)
@ -74,7 +75,7 @@ Where `SieveCustomSortMethodsOfPosts` for example is:
```C# ```C#
public class SieveCustomSortMethods : ISieveCustomSortMethods 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 ? var result = useThenBy ?
((IOrderedQueryable<Post>)source).ThenBy(p => p.LikeCount) : // ThenBy only works on IOrderedQueryable<TEntity> ((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", "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).", "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)", "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 * `pageSize` is the number of items returned per page
Notes: 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 * 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)) * 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) * 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: Now you should inject the new class instead:
```C# ```C#
services.AddScoped<ISieveProcessor, ApplicationSieveProcessor>(); services.AddScoped<ISieveProcessor, ApplicationSieveProcessor>();
``` ```
Find More on Sieve's Fluent API [here](https://github.com/Biarity/Sieve/issues/4#issuecomment-364629048). 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 ## 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.2.0 introduced OR logic for filter values. This means your custom filters will need to accept multiple values rather than just the one.

View File

@ -2,10 +2,10 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Sieve.Models; using Sieve.Models;
using Sieve.Sample.Entities;
using Sieve.Services; using Sieve.Services;
using SieveTests.Entities;
namespace SieveTests.Controllers namespace Sieve.Sample.Controllers
{ {
[Route("api/[controller]/[action]")] [Route("api/[controller]/[action]")]
public class PostsController : Controller public class PostsController : Controller

View File

@ -1,6 +1,6 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace SieveTests.Entities namespace Sieve.Sample.Entities
{ {
public class ApplicationDbContext : DbContext public class ApplicationDbContext : DbContext
{ {

View File

@ -2,14 +2,14 @@
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using Sieve.Attributes; using Sieve.Attributes;
namespace SieveTests.Entities namespace Sieve.Sample.Entities
{ {
public class Post public class Post
{ {
public int Id { get; set; } public int Id { get; set; }
[Sieve(CanFilter = true, CanSort = true)] [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)] [Sieve(CanFilter = true, CanSort = true)]
public int LikeCount { get; set; } = new Random().Next(0, 1000); public int LikeCount { get; set; } = new Random().Next(0, 1000);

View 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");
}
}
}

View 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
}
}
}

View File

@ -1,10 +1,9 @@
using System; using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations; 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) protected override void Up(MigrationBuilder migrationBuilder)
{ {
@ -13,14 +12,18 @@ namespace SieveTests.Migrations
columns: table => new columns: table => new
{ {
Id = table.Column<int>(nullable: false) 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), CommentCount = table.Column<int>(nullable: false),
DateCreated = table.Column<DateTimeOffset>(nullable: false), DateCreated = table.Column<DateTimeOffset>(nullable: false),
LikeCount = table.Column<int>(nullable: false), DateLastViewed = table.Column<DateTime>(type: "datetime", nullable: false),
Title = table.Column<string>(nullable: true),
CategoryId = table.Column<int>(nullable: true) 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) protected override void Down(MigrationBuilder migrationBuilder)

View 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
}
}
}

View File

@ -1,7 +1,7 @@
using Microsoft.AspNetCore; using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
namespace SieveTests namespace Sieve.Sample
{ {
public static class Program public static class Program
{ {

View File

@ -1,9 +1,9 @@
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Sieve.Models; using Sieve.Models;
using Sieve.Sample.Entities;
using Sieve.Services; using Sieve.Services;
using SieveTests.Entities;
namespace SieveTests.Services namespace Sieve.Sample.Services
{ {
public class ApplicationSieveProcessor : SieveProcessor public class ApplicationSieveProcessor : SieveProcessor
{ {
@ -13,11 +13,18 @@ namespace SieveTests.Services
protected override SievePropertyMapper MapProperties(SievePropertyMapper mapper) protected override SievePropertyMapper MapProperties(SievePropertyMapper mapper)
{ {
// Option 1: Map all properties centrally
mapper.Property<Post>(p => p.Title) mapper.Property<Post>(p => p.Title)
.CanSort() .CanSort()
.CanFilter() .CanFilter()
.HasName("CustomTitleName"); .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; return mapper;
} }
} }

View File

@ -1,8 +1,8 @@
using System.Linq; using System.Linq;
using Sieve.Sample.Entities;
using Sieve.Services; using Sieve.Services;
using SieveTests.Entities;
namespace SieveTests.Services namespace Sieve.Sample.Services
{ {
public class SieveCustomFilterMethods : ISieveCustomFilterMethods public class SieveCustomFilterMethods : ISieveCustomFilterMethods
{ {

View File

@ -1,8 +1,8 @@
using System.Linq; using System.Linq;
using Sieve.Sample.Entities;
using Sieve.Services; using Sieve.Services;
using SieveTests.Entities;
namespace SieveTests.Services namespace Sieve.Sample.Services
{ {
public class SieveCustomSortMethods : ISieveCustomSortMethods public class SieveCustomSortMethods : ISieveCustomSortMethods
{ {

View 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
View 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();
}
}
}

View File

@ -1,6 +1,5 @@
{ {
"Logging": { "Logging": {
"IncludeScopes": false,
"LogLevel": { "LogLevel": {
"Default": "Debug", "Default": "Debug",
"System": "Information", "System": "Information",

View File

@ -4,10 +4,10 @@
}, },
"Sieve": { "Sieve": {
"CaseSensitive": false, "CaseSensitive": false,
"DefaultPageSize": 10 "DefaultPageSize": 10,
"IgnoreNullsOnNotEqual": true
}, },
"Logging": { "Logging": {
"IncludeScopes": false,
"Debug": { "Debug": {
"LogLevel": { "LogLevel": {
"Default": "Warning" "Default": "Warning"

View File

@ -5,7 +5,7 @@ VisualStudioVersion = 15.0.27130.2027
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sieve", "Sieve\Sieve.csproj", "{B32B8B33-94B0-40E3-8FE5-D54602222717}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sieve", "Sieve\Sieve.csproj", "{B32B8B33-94B0-40E3-8FE5-D54602222717}"
EndProject 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 EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SieveUnitTests", "SieveUnitTests\SieveUnitTests.csproj", "{21C3082D-F40E-457F-BE2E-AA099E19E199}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SieveUnitTests", "SieveUnitTests\SieveUnitTests.csproj", "{21C3082D-F40E-457F-BE2E-AA099E19E199}"
EndProject EndProject
@ -15,12 +15,16 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
README.md = README.md README.md = README.md
EndProjectSection EndProjectSection
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "build\_build.csproj", "{C8829BE9-BC1E-4C67-ACEA-EC5DD3633EE2}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU Release|Any CPU = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution 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.ActiveCfg = Debug|Any CPU
{B32B8B33-94B0-40E3-8FE5-D54602222717}.Debug|Any CPU.Build.0 = 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 {B32B8B33-94B0-40E3-8FE5-D54602222717}.Release|Any CPU.ActiveCfg = Release|Any CPU

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -6,45 +7,49 @@ namespace Sieve.Models
{ {
public class FilterTerm : IFilterTerm, IEquatable<FilterTerm> 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 HashSet<string> _escapedSequences = new HashSet<string>
{
private static readonly string[] Operators = new string[] { @"\|",
"!@=*", @"\\"
"!_=*",
"!=*",
"!@=",
"!_=",
"==*",
"@=*",
"_=*",
"==",
"!=",
">=",
"<=",
">",
"<",
"@=",
"_="
}; };
public string Filter public string Filter
{ {
set set
{ {
var filterSplits = value.Split(Operators, StringSplitOptions.RemoveEmptyEntries) var filterSplits = Regex.Split(value,EscapeNegPatternForOper).Select(t => t.Trim()).ToArray();
.Select(t => t.Trim()).ToArray();
Names = Regex.Split(filterSplits[0], EscapedPipePattern).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); OperatorParsed = GetOperatorParsed(Operator);
OperatorIsCaseInsensitive = Operator.EndsWith("*"); OperatorIsCaseInsensitive = Operator.EndsWith("*");
OperatorIsNegated = OperatorParsed != FilterOperator.NotEquals && Operator.StartsWith("!"); 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 string[] Names { get; private set; }
public FilterOperator OperatorParsed { get; private set; } public FilterOperator OperatorParsed { get; private set; }
@ -90,6 +95,5 @@ namespace Sieve.Models
&& Values.SequenceEqual(other.Values) && Values.SequenceEqual(other.Values)
&& Operator == other.Operator; && Operator == other.Operator;
} }
} }
} }

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using System.Runtime.Serialization; using System.Runtime.Serialization;
@ -13,7 +14,15 @@ namespace Sieve.Models
where TFilterTerm : IFilterTerm, new() where TFilterTerm : IFilterTerm, new()
where TSortTerm : ISortTerm, new() where TSortTerm : ISortTerm, new()
{ {
/// <summary>
/// Pattern used to split filters and sorts by comma.
/// </summary>
private const string EscapedCommaPattern = @"(?<!($|[^\\])(\\\\)*?\\),\s*"; private const string EscapedCommaPattern = @"(?<!($|[^\\])(\\\\)*?\\),\s*";
/// <summary>
/// Escaped comma e.g. used in filter filter string.
/// </summary>
private const string EscapedComma = @"\,";
[DataMember] [DataMember]
public string Filters { get; set; } public string Filters { get; set; }
@ -34,15 +43,20 @@ namespace Sieve.Models
var value = new List<TFilterTerm>(); var value = new List<TFilterTerm>();
foreach (var filter in Regex.Split(Filters, EscapedCommaPattern)) 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("(")) if (filter.StartsWith("("))
{ {
var filterOpAndVal = filter.Substring(filter.LastIndexOf(")") + 1); var filterOpAndVal = filterValue[(filterValue.LastIndexOf(")", StringComparison.Ordinal) + 1)..];
var subfilters = filter.Replace(filterOpAndVal, "").Replace("(", "").Replace(")", ""); var subFilters = filterValue.Replace(filterOpAndVal, "").Replace("(", "").Replace(")", "");
var filterTerm = new TFilterTerm var filterTerm = new TFilterTerm
{ {
Filter = subfilters + filterOpAndVal Filter = subFilters + filterOpAndVal
}; };
value.Add(filterTerm); value.Add(filterTerm);
} }
@ -50,7 +64,7 @@ namespace Sieve.Models
{ {
var filterTerm = new TFilterTerm var filterTerm = new TFilterTerm
{ {
Filter = filter Filter = filterValue
}; };
value.Add(filterTerm); value.Add(filterTerm);
} }
@ -65,29 +79,28 @@ namespace Sieve.Models
public List<TSortTerm> GetSortsParsed() public List<TSortTerm> GetSortsParsed()
{ {
if (Sorts != null) 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
{ {
return 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;
} }
} }
} }

View File

@ -1,4 +1,4 @@
namespace Sieve.Models namespace Sieve.Models
{ {
public class SieveOptions public class SieveOptions
{ {
@ -10,6 +10,8 @@
public bool ThrowExceptions { get; set; } = false; public bool ThrowExceptions { get; set; } = false;
public bool IgnoreNullsOnNotEqual { get; set; } = true;
public string CultureNameOfTypeConversion { get; set; } = "en"; public string CultureNameOfTypeConversion { get; set; } = "en";
} }
} }

View 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;
}
}
}

View File

@ -15,40 +15,50 @@ namespace Sieve.Services
{ {
public class SieveProcessor : SieveProcessor<SieveModel, FilterTerm, SortTerm>, ISieveProcessor 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 TFilterTerm : IFilterTerm, new()
where TSortTerm : ISortTerm, 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 TFilterTerm : IFilterTerm, new()
where TSortTerm : ISortTerm, new() where TSortTerm : ISortTerm, new()
{ {
private const string nullFilterValue = "null"; private const string NullFilterValue = "null";
private readonly IOptions<SieveOptions> _options; private const char EscapeChar = '\\';
private readonly ISieveCustomSortMethods _customSortMethods; private readonly ISieveCustomSortMethods _customSortMethods;
private readonly ISieveCustomFilterMethods _customFilterMethods; private readonly ISieveCustomFilterMethods _customFilterMethods;
private readonly SievePropertyMapper mapper = new SievePropertyMapper(); private readonly SievePropertyMapper _mapper = new SievePropertyMapper();
public SieveProcessor(IOptions<SieveOptions> options, public SieveProcessor(IOptions<SieveOptions> options,
ISieveCustomSortMethods customSortMethods, ISieveCustomSortMethods customSortMethods,
ISieveCustomFilterMethods customFilterMethods) ISieveCustomFilterMethods customFilterMethods)
{ {
mapper = MapProperties(mapper); _mapper = MapProperties(_mapper);
_options = options; Options = options;
_customSortMethods = customSortMethods; _customSortMethods = customSortMethods;
_customFilterMethods = customFilterMethods; _customFilterMethods = customFilterMethods;
} }
@ -77,25 +87,27 @@ namespace Sieve.Services
public SieveProcessor(IOptions<SieveOptions> options, public SieveProcessor(IOptions<SieveOptions> options,
ISieveCustomSortMethods customSortMethods) ISieveCustomSortMethods customSortMethods)
{ {
mapper = MapProperties(mapper); _mapper = MapProperties(_mapper);
_options = options; Options = options;
_customSortMethods = customSortMethods; _customSortMethods = customSortMethods;
} }
public SieveProcessor(IOptions<SieveOptions> options, public SieveProcessor(IOptions<SieveOptions> options,
ISieveCustomFilterMethods customFilterMethods) ISieveCustomFilterMethods customFilterMethods)
{ {
mapper = MapProperties(mapper); _mapper = MapProperties(_mapper);
_options = options; Options = options;
_customFilterMethods = customFilterMethods; _customFilterMethods = customFilterMethods;
} }
public SieveProcessor(IOptions<SieveOptions> options) public SieveProcessor(IOptions<SieveOptions> options)
{ {
mapper = MapProperties(mapper); _mapper = MapProperties(_mapper);
_options = options; Options = options;
} }
protected IOptions<SieveOptions> Options { get; }
/// <summary> /// <summary>
/// Apply filtering, sorting, and pagination parameters found in `model` to `source` /// Apply filtering, sorting, and pagination parameters found in `model` to `source`
/// </summary> /// </summary>
@ -107,12 +119,8 @@ namespace Sieve.Services
/// <param name="applySorting">Should the data be sorted? Defaults to true.</param> /// <param name="applySorting">Should the data be sorted? Defaults to true.</param>
/// <param name="applyPagination">Should the data be paginated? Defaults to true.</param> /// <param name="applyPagination">Should the data be paginated? Defaults to true.</param>
/// <returns>Returns a transformed version of `source`</returns> /// <returns>Returns a transformed version of `source`</returns>
public IQueryable<TEntity> Apply<TEntity>( public IQueryable<TEntity> Apply<TEntity>(TSieveModel model, IQueryable<TEntity> source,
TSieveModel model, object[] dataForCustomMethods = null, bool applyFiltering = true, bool applySorting = true,
IQueryable<TEntity> source,
object[] dataForCustomMethods = null,
bool applyFiltering = true,
bool applySorting = true,
bool applyPagination = true) bool applyPagination = true)
{ {
var result = source; var result = source;
@ -124,19 +132,16 @@ namespace Sieve.Services
try try
{ {
// Filter
if (applyFiltering) if (applyFiltering)
{ {
result = ApplyFiltering(model, result, dataForCustomMethods); result = ApplyFiltering(model, result, dataForCustomMethods);
} }
// Sort
if (applySorting) if (applySorting)
{ {
result = ApplySorting(model, result, dataForCustomMethods); result = ApplySorting(model, result, dataForCustomMethods);
} }
// Paginate
if (applyPagination) if (applyPagination)
{ {
result = ApplyPagination(model, result); result = ApplyPagination(model, result);
@ -146,25 +151,21 @@ namespace Sieve.Services
} }
catch (Exception ex) catch (Exception ex)
{ {
if (_options.Value.ThrowExceptions) if (!Options.Value.ThrowExceptions)
{
if (ex is SieveException)
{
throw;
}
throw new SieveException(ex.Message, ex);
}
else
{ {
return result; return result;
} }
if (ex is SieveException)
{
throw;
}
throw new SieveException(ex.Message, ex);
} }
} }
private IQueryable<TEntity> ApplyFiltering<TEntity>( protected virtual IQueryable<TEntity> ApplyFiltering<TEntity>(TSieveModel model, IQueryable<TEntity> result,
TSieveModel model,
IQueryable<TEntity> result,
object[] dataForCustomMethods = null) object[] dataForCustomMethods = null)
{ {
if (model?.GetFiltersParsed() == null) if (model?.GetFiltersParsed() == null)
@ -184,38 +185,33 @@ namespace Sieve.Services
var (fullPropertyName, property) = GetSieveProperty<TEntity>(false, true, filterTermName); var (fullPropertyName, property) = GetSieveProperty<TEntity>(false, true, filterTermName);
if (property != null) if (property != null)
{ {
Expression propertyValue = parameter; if (filterTerm.Values == null)
Expression nullCheck = null;
var names = fullPropertyName.Split('.');
for (var i = 0; i < names.Length; i++)
{ {
propertyValue = Expression.PropertyOrField(propertyValue, names[i]); continue;
if (i != names.Length - 1 && propertyValue.Type.IsNullable())
{
nullCheck = GenerateFilterNullCheckExpression(propertyValue, nullCheck);
}
} }
if (filterTerm.Values == null) continue;
var converter = TypeDescriptor.GetConverter(property.PropertyType); var converter = TypeDescriptor.GetConverter(property.PropertyType);
foreach (var filterTermValue in filterTerm.Values) 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 var filterValue = isFilterTermValueNull
? Expression.Constant(null, property.PropertyType) ? Expression.Constant(null, property.PropertyType)
: ConvertStringValueToConstantExpression(filterTermValue, property, converter, cultureInfoToUseForTypeConversion); : ConvertStringValueToConstantExpression(filterTermValue, property, converter, cultureInfoToUseForTypeConversion);
if (filterTerm.OperatorIsCaseInsensitive) if (filterTerm.OperatorIsCaseInsensitive && !isFilterTermValueNull)
{ {
propertyValue = Expression.Call(propertyValue, propertyValue = Expression.Call(propertyValue,
typeof(string).GetMethods() 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, filterValue = Expression.Call(filterValue,
typeof(string).GetMethods() 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); var expression = GetExpression(filterTerm, filterValue, propertyValue);
@ -225,61 +221,103 @@ namespace Sieve.Services
expression = Expression.Not(expression); expression = Expression.Not(expression);
} }
var filterValueNullCheck = !isFilterTermValueNull && propertyValue.Type.IsNullable() if (expression.NodeType != ExpressionType.NotEqual || Options.Value.IgnoreNullsOnNotEqual)
? GenerateFilterNullCheckExpression(propertyValue, nullCheck)
: nullCheck;
if (filterValueNullCheck != null)
{ {
expression = Expression.AndAlso(filterValueNullCheck, expression); var filterValueNullCheck = GetFilterValueNullCheck(parameter, fullPropertyName, isFilterTermValueNull);
if (filterValueNullCheck != null)
{
expression = Expression.AndAlso(filterValueNullCheck, expression);
}
} }
if (innerExpression == null) innerExpression = innerExpression == null
{ ? expression
innerExpression = expression; : Expression.OrElse(innerExpression, expression);
}
else
{
innerExpression = Expression.OrElse(innerExpression, expression);
}
} }
} }
else else
{ {
result = ApplyCustomMethod(result, filterTermName, _customFilterMethods, result = ApplyCustomMethod(result, filterTermName, _customFilterMethods,
new object[] { new object[] {result, filterTerm.Operator, filterTerm.Values}, dataForCustomMethods);
result,
filterTerm.Operator,
filterTerm.Values
}, dataForCustomMethods);
} }
} }
if (outerExpression == null) if (outerExpression == null)
{ {
outerExpression = innerExpression; outerExpression = innerExpression;
continue; continue;
} }
if (innerExpression == null) if (innerExpression == null)
{ {
continue; continue;
} }
outerExpression = Expression.AndAlso(outerExpression, innerExpression); outerExpression = Expression.AndAlso(outerExpression, innerExpression);
} }
return outerExpression == null return outerExpression == null
? result ? result
: result.Where(Expression.Lambda<Func<TEntity, bool>>(outerExpression, parameter)); : 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 return nullCheckExpression == null
? Expression.NotEqual(propertyValue, Expression.Default(propertyValue.Type)) ? 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)) dynamic constantVal = converter.CanConvertFrom(typeof(string))
? converter.ConvertFrom(null, cultureInfo, value) ? converter.ConvertFrom(null, cultureInfo, value)
: Convert.ChangeType(value, property.PropertyType); : Convert.ChangeType(value, property.PropertyType);
@ -289,47 +327,32 @@ namespace Sieve.Services
private static Expression GetExpression(TFilterTerm filterTerm, dynamic filterValue, dynamic propertyValue) private static Expression GetExpression(TFilterTerm filterTerm, dynamic filterValue, dynamic propertyValue)
{ {
switch (filterTerm.OperatorParsed) return filterTerm.OperatorParsed switch
{ {
case FilterOperator.Equals: FilterOperator.Equals => Expression.Equal(propertyValue, filterValue),
return Expression.Equal(propertyValue, filterValue); FilterOperator.NotEquals => Expression.NotEqual(propertyValue, filterValue),
case FilterOperator.NotEquals: FilterOperator.GreaterThan => Expression.GreaterThan(propertyValue, filterValue),
return Expression.NotEqual(propertyValue, filterValue); FilterOperator.LessThan => Expression.LessThan(propertyValue, filterValue),
case FilterOperator.GreaterThan: FilterOperator.GreaterThanOrEqualTo => Expression.GreaterThanOrEqual(propertyValue, filterValue),
return Expression.GreaterThan(propertyValue, filterValue); FilterOperator.LessThanOrEqualTo => Expression.LessThanOrEqual(propertyValue, filterValue),
case FilterOperator.LessThan: FilterOperator.Contains => Expression.Call(propertyValue,
return Expression.LessThan(propertyValue, filterValue); typeof(string).GetMethods().First(m => m.Name == "Contains" && m.GetParameters().Length == 1),
case FilterOperator.GreaterThanOrEqualTo: filterValue),
return Expression.GreaterThanOrEqual(propertyValue, filterValue); FilterOperator.StartsWith => Expression.Call(propertyValue,
case FilterOperator.LessThanOrEqualTo: typeof(string).GetMethods().First(m => m.Name == "StartsWith" && m.GetParameters().Length == 1),
return Expression.LessThanOrEqual(propertyValue, filterValue); filterValue),
case FilterOperator.Contains: _ => Expression.Equal(propertyValue, filterValue)
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);
}
} }
// Workaround to ensure that the filter value gets passed as a parameter in generated SQL from EF Core // 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 private static Expression GetClosureOverConstant<T>(T constant, Type targetType)
// 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)
{ {
return Expression.Constant(constant, targetType); Expression<Func<T>> hoistedConstant = () => constant;
return Expression.Convert(hoistedConstant.Body, targetType);
} }
private IQueryable<TEntity> ApplySorting<TEntity>( protected virtual IQueryable<TEntity> ApplySorting<TEntity>(TSieveModel model, IQueryable<TEntity> result,
TSieveModel model,
IQueryable<TEntity> result,
object[] dataForCustomMethods = null) object[] dataForCustomMethods = null)
{ {
if (model?.GetSortsParsed() == null) if (model?.GetSortsParsed() == null)
@ -349,33 +372,29 @@ namespace Sieve.Services
else else
{ {
result = ApplyCustomMethod(result, sortTerm.Name, _customSortMethods, result = ApplyCustomMethod(result, sortTerm.Name, _customSortMethods,
new object[] new object[] {result, useThenBy, sortTerm.Descending}, dataForCustomMethods);
{
result,
useThenBy,
sortTerm.Descending
}, dataForCustomMethods);
} }
useThenBy = true; useThenBy = true;
} }
return result; return result;
} }
private IQueryable<TEntity> ApplyPagination<TEntity>( protected virtual IQueryable<TEntity> ApplyPagination<TEntity>(TSieveModel model, IQueryable<TEntity> result)
TSieveModel model,
IQueryable<TEntity> result)
{ {
var page = model?.Page ?? 1; var page = model?.Page ?? 1;
var pageSize = model?.PageSize ?? _options.Value.DefaultPageSize; var pageSize = model?.PageSize ?? Options.Value.DefaultPageSize;
var maxPageSize = _options.Value.MaxPageSize > 0 ? _options.Value.MaxPageSize : pageSize; var maxPageSize = Options.Value.MaxPageSize > 0 ? Options.Value.MaxPageSize : pageSize;
if (pageSize > 0) if (pageSize <= 0)
{ {
result = result.Skip((page - 1) * pageSize); return result;
result = result.Take(Math.Min(pageSize, maxPageSize));
} }
result = result.Skip((page - 1) * pageSize);
result = result.Take(Math.Min(pageSize, maxPageSize));
return result; return result;
} }
@ -384,51 +403,52 @@ namespace Sieve.Services
return mapper; return mapper;
} }
private (string, PropertyInfo) GetSieveProperty<TEntity>( private (string, PropertyInfo) GetSieveProperty<TEntity>(bool canSortRequired, bool canFilterRequired,
bool canSortRequired,
bool canFilterRequired,
string name) string name)
{ {
var property = mapper.FindProperty<TEntity>(canSortRequired, canFilterRequired, name, _options.Value.CaseSensitive); var property = _mapper.FindProperty<TEntity>(canSortRequired, canFilterRequired, name,
if (property.Item1 == null) Options.Value.CaseSensitive);
if (property.Item1 != null)
{ {
var prop = FindPropertyBySieveAttribute<TEntity>(canSortRequired, canFilterRequired, name, _options.Value.CaseSensitive); return property;
return (prop?.Name, prop);
} }
return property;
var prop = FindPropertyBySieveAttribute<TEntity>(canSortRequired, canFilterRequired, name,
Options.Value.CaseSensitive);
return (prop?.Name, prop);
} }
private PropertyInfo FindPropertyBySieveAttribute<TEntity>( private static PropertyInfo FindPropertyBySieveAttribute<TEntity>(bool canSortRequired, bool canFilterRequired,
bool canSortRequired, string name, bool isCaseSensitive)
bool canFilterRequired,
string name,
bool isCaseSensitive)
{ {
return Array.Find(typeof(TEntity).GetProperties(), p => return Array.Find(typeof(TEntity).GetProperties(),
{ p => p.GetCustomAttribute(typeof(SieveAttribute)) is SieveAttribute SieveAttribute
return p.GetCustomAttribute(typeof(SieveAttribute)) is SieveAttribute sieveAttribute && (!canSortRequired || SieveAttribute.CanSort)
&& (!canSortRequired || sieveAttribute.CanSort) && (!canFilterRequired || SieveAttribute.CanFilter)
&& (!canFilterRequired || sieveAttribute.CanFilter) && (SieveAttribute.Name ?? p.Name).Equals(name,
&& (sieveAttribute.Name ?? p.Name).Equals(name, isCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase); 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() var customMethod = parent?.GetType()
.GetMethodExt(name, .GetMethodExt(name,
_options.Value.CaseSensitive ? BindingFlags.Default : BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance, Options.Value.CaseSensitive
typeof(IQueryable<TEntity>)); ? BindingFlags.Default
: BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance,
typeof(IQueryable<TEntity>));
if (customMethod == null) if (customMethod == null)
{ {
// Find generic methods `public IQueryable<T> Filter<T>(IQueryable<T> source, ...)` // Find generic methods `public IQueryable<T> Filter<T>(IQueryable<T> source, ...)`
var genericCustomMethod = parent?.GetType() var genericCustomMethod = parent?.GetType()
.GetMethodExt(name, .GetMethodExt(name,
_options.Value.CaseSensitive ? BindingFlags.Default : BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance, Options.Value.CaseSensitive
typeof(IQueryable<>)); ? BindingFlags.Default
: BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance,
typeof(IQueryable<>));
if (genericCustomMethod != null && if (genericCustomMethod != null &&
genericCustomMethod.ReturnType.IsGenericType && genericCustomMethod.ReturnType.IsGenericType &&
@ -436,7 +456,8 @@ namespace Sieve.Services
{ {
var genericBaseType = genericCustomMethod.ReturnType.GenericTypeArguments[0]; var genericBaseType = genericCustomMethod.ReturnType.GenericTypeArguments[0];
var constraints = genericBaseType.GetGenericParameterConstraints(); 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)); customMethod = genericCustomMethod.MakeGenericMethod(typeof(TEntity));
} }
@ -465,40 +486,34 @@ namespace Sieve.Services
} }
else else
{ {
var incompatibleCustomMethods = parent? var incompatibleCustomMethods =
.GetType() parent?
.GetMethods .GetType()
( .GetMethods(Options.Value.CaseSensitive
_options.Value.CaseSensitive ? BindingFlags.Default
? BindingFlags.Default : BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance)
: BindingFlags.IgnoreCase | BindingFlags.Public | .Where(method => string.Equals(method.Name, name,
BindingFlags.Instance Options.Value.CaseSensitive
) ? StringComparison.InvariantCulture
.Where(method => string.Equals(method.Name, name, : StringComparison.InvariantCultureIgnoreCase))
_options.Value.CaseSensitive .ToList()
? StringComparison.InvariantCulture ?? new List<MethodInfo>();
: StringComparison.InvariantCultureIgnoreCase))
.ToList()
??
new List<MethodInfo>();
if (incompatibleCustomMethods.Any()) 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
{ {
throw new SieveMethodNotFoundException(name, $"{name} not found."); 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; return result;

View File

@ -1,22 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.1</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>
<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/ <Authors>Biarity;Ashish Patel;Kevin Sommer</Authors>
</Description>
<Copyright>Copyright 2018</Copyright> <PackageTags>Filter;Sort;Page;Paging;</PackageTags>
<PackageLicenseUrl>https://github.com/Biarity/Sieve/blob/master/LICENSE</PackageLicenseUrl>
<PackageProjectUrl>https://github.com/Biarity/Sieve</PackageProjectUrl> <PackageProjectUrl>https://github.com/Biarity/Sieve</PackageProjectUrl>
<PackageIconUrl>https://emojipedia-us.s3.amazonaws.com/thumbs/240/twitter/120/alembic_2697.png</PackageIconUrl> <PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<RepositoryUrl></RepositoryUrl>
<PackageReleaseNotes>Only Skip when pageSize &gt; 0 (#63)
Added support for generic filter and sort methods (#60)
Don't process when filterTerm.Values is null (#59)
</PackageReleaseNotes>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance> <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> </PropertyGroup>
<ItemGroup> <ItemGroup>

Binary file not shown.

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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");
}
}
}

View File

@ -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
}
}
}

View File

@ -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>

View File

@ -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();
}
}
}

View File

@ -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");
}
}
}

View File

@ -7,5 +7,8 @@ namespace SieveUnitTests.Entities
{ {
[Sieve(CanFilter = true)] [Sieve(CanFilter = true)]
public string Text { get; set; } public string Text { get; set; }
[Sieve(CanFilter = true)]
public string Author { get; set; }
} }
} }

View 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");
}
}
}

View File

@ -1,32 +1,42 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Sieve.Exceptions; using Sieve.Exceptions;
using Sieve.Models; using Sieve.Models;
using Sieve.Services; using Sieve.Services;
using SieveUnitTests.Abstractions.Entity;
using SieveUnitTests.Entities; using SieveUnitTests.Entities;
using SieveUnitTests.Services; using SieveUnitTests.Services;
using Xunit;
using Xunit.Abstractions;
namespace SieveUnitTests namespace SieveUnitTests
{ {
[TestClass]
public class General public class General
{ {
private readonly ITestOutputHelper _testOutputHelper;
private readonly SieveProcessor _processor; private readonly SieveProcessor _processor;
private readonly SieveProcessor _nullableProcessor;
private readonly IQueryable<Post> _posts; private readonly IQueryable<Post> _posts;
private readonly IQueryable<Comment> _comments; 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(), _processor = new ApplicationSieveProcessor(new SieveOptionsAccessor(),
new SieveCustomSortMethods(), new SieveCustomSortMethods(),
new SieveCustomFilterMethods()); new SieveCustomFilterMethods());
_nullableProcessor = new ApplicationSieveProcessor(nullableAccessor,
new SieveCustomSortMethods(),
new SieveCustomFilterMethods());
_posts = new List<Post> _posts = new List<Post>
{ {
new Post() { new Post
{
Id = 0, Id = 0,
Title = "A", Title = "A",
LikeCount = 100, LikeCount = 100,
@ -35,7 +45,8 @@ namespace SieveUnitTests
TopComment = new Comment { Id = 0, Text = "A1" }, TopComment = new Comment { Id = 0, Text = "A1" },
FeaturedComment = new Comment { Id = 4, Text = "A2" } FeaturedComment = new Comment { Id = 4, Text = "A2" }
}, },
new Post() { new Post
{
Id = 1, Id = 1,
Title = "B", Title = "B",
LikeCount = 50, LikeCount = 50,
@ -44,7 +55,8 @@ namespace SieveUnitTests
TopComment = new Comment { Id = 3, Text = "B1" }, TopComment = new Comment { Id = 3, Text = "B1" },
FeaturedComment = new Comment { Id = 5, Text = "B2" } FeaturedComment = new Comment { Id = 5, Text = "B2" }
}, },
new Post() { new Post
{
Id = 2, Id = 2,
Title = "C", Title = "C",
LikeCount = 0, LikeCount = 0,
@ -52,7 +64,8 @@ namespace SieveUnitTests
TopComment = new Comment { Id = 2, Text = "C1" }, TopComment = new Comment { Id = 2, Text = "C1" },
FeaturedComment = new Comment { Id = 6, Text = "C2" } FeaturedComment = new Comment { Id = 6, Text = "C2" }
}, },
new Post() { new Post
{
Id = 3, Id = 3,
Title = "D", Title = "D",
LikeCount = 3, LikeCount = 3,
@ -60,286 +73,323 @@ namespace SieveUnitTests
CategoryId = 2, CategoryId = 2,
TopComment = new Comment { Id = 1, Text = "D1" }, TopComment = new Comment { Id = 1, Text = "D1" },
FeaturedComment = new Comment { Id = 7, Text = "D2" } FeaturedComment = new Comment { Id = 7, Text = "D2" }
}, }
}.AsQueryable(); }.AsQueryable();
_comments = new List<Comment> _comments = new List<Comment>
{ {
new Comment() { new Comment
{
Id = 0, Id = 0,
DateCreated = DateTimeOffset.UtcNow.AddDays(-20), DateCreated = DateTimeOffset.UtcNow.AddDays(-20),
Text = "This is an old comment." Text = "This is an old comment."
}, },
new Comment() { new Comment
{
Id = 1, Id = 1,
DateCreated = DateTimeOffset.UtcNow.AddDays(-1), DateCreated = DateTimeOffset.UtcNow.AddDays(-1),
Text = "This is a fairly new comment." Text = "This is a fairly new comment."
}, },
new Comment() { new Comment
{
Id = 2, Id = 2,
DateCreated = DateTimeOffset.UtcNow, 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(); }.AsQueryable();
} }
[TestMethod] [Fact]
public void ContainsCanBeCaseInsensitive() public void ContainsCanBeCaseInsensitive()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "Title@=*a" Filters = "Title@=*a"
}; };
var result = _processor.Apply(model, _posts); var result = _processor.Apply(model, _posts);
Assert.AreEqual(result.First().Id, 0); Assert.Equal(0, result.First().Id);
Assert.IsTrue(result.Count() == 1); Assert.True(result.Count() == 1);
} }
[TestMethod] [Fact]
public void NotEqualsCanBeCaseInsensitive() public void NotEqualsCanBeCaseInsensitive()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "Title!=*a" Filters = "Title!=*a"
}; };
var result = _processor.Apply(model, _posts); var result = _processor.Apply(model, _posts);
Assert.AreEqual(result.First().Id, 1); Assert.Equal(1, result.First().Id);
Assert.IsTrue(result.Count() == 3); Assert.True(result.Count() == 3);
} }
[TestMethod] [Fact]
public void ContainsIsCaseSensitive() public void ContainsIsCaseSensitive()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "Title@=a", Filters = "Title@=a",
}; };
var result = _processor.Apply(model, _posts); var result = _processor.Apply(model, _posts);
Assert.IsTrue(result.Count() == 0); Assert.True(!result.Any());
} }
[TestMethod] [Fact]
public void NotContainsWorks() public void NotContainsWorks()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "Title!@=D", Filters = "Title!@=D",
}; };
var result = _processor.Apply(model, _posts); var result = _processor.Apply(model, _posts);
Assert.IsTrue(result.Count() == 3); Assert.True(result.Count() == 3);
} }
[TestMethod] [Fact]
public void CanFilterBools() public void CanFilterBools()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "IsDraft==false" Filters = "IsDraft==false"
}; };
var result = _processor.Apply(model, _posts); var result = _processor.Apply(model, _posts);
Assert.IsTrue(result.Count() == 2); Assert.True(result.Count() == 2);
} }
[TestMethod] [Fact]
public void CanSortBools() public void CanSortBools()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Sorts = "-IsDraft" Sorts = "-IsDraft"
}; };
var result = _processor.Apply(model, _posts); var result = _processor.Apply(model, _posts);
Assert.AreEqual(result.First().Id, 0); Assert.Equal(0, result.First().Id);
} }
[TestMethod] [Fact]
public void CanFilterNullableInts() public void CanFilterNullableInts()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "CategoryId==1" Filters = "CategoryId==1"
}; };
var result = _processor.Apply(model, _posts); 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] [Fact]
public void EqualsDoesntFailWithNonStringTypes() public void CanFilterNullableIntsWithNotEqual()
{ {
var model = new SieveModel() 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", Filters = "LikeCount==50",
}; };
Console.WriteLine(model.GetFiltersParsed()[0].Values); _testOutputHelper.WriteLine(model.GetFiltersParsed()[0].Values.ToString());
Console.WriteLine(model.GetFiltersParsed()[0].Operator); _testOutputHelper.WriteLine(model.GetFiltersParsed()[0].Operator);
Console.WriteLine(model.GetFiltersParsed()[0].OperatorParsed); _testOutputHelper.WriteLine(model.GetFiltersParsed()[0].OperatorParsed.ToString());
var result = _processor.Apply(model, _posts); var result = _processor.Apply(model, _posts);
Assert.AreEqual(result.First().Id, 1); Assert.Equal(1, result.First().Id);
Assert.IsTrue(result.Count() == 1); Assert.True(result.Count() == 1);
} }
[TestMethod] [Fact]
public void CustomFiltersWork() public void CustomFiltersWork()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "Isnew", Filters = "Isnew",
}; };
var result = _processor.Apply(model, _posts); var result = _processor.Apply(model, _posts);
Assert.IsFalse(result.Any(p => p.Id == 0)); Assert.False(result.Any(p => p.Id == 0));
Assert.IsTrue(result.Count() == 3); Assert.True(result.Count() == 3);
} }
[TestMethod] [Fact]
public void CustomGenericFiltersWork() public void CustomGenericFiltersWork()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "Latest", Filters = "Latest",
}; };
var result = _processor.Apply(model, _comments); var result = _processor.Apply(model, _comments);
Assert.IsFalse(result.Any(p => p.Id == 0)); Assert.False(result.Any(p => p.Id == 0));
Assert.IsTrue(result.Count() == 2); Assert.True(result.Count() == 2);
} }
[TestMethod] [Fact]
public void CustomFiltersWithOperatorsWork() public void CustomFiltersWithOperatorsWork()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "HasInTitle==A", Filters = "HasInTitle==A",
}; };
var result = _processor.Apply(model, _posts); var result = _processor.Apply(model, _posts);
Assert.IsTrue(result.Any(p => p.Id == 0)); Assert.True(result.Any(p => p.Id == 0));
Assert.IsTrue(result.Count() == 1); Assert.True(result.Count() == 1);
} }
[TestMethod] [Fact]
public void CustomFiltersMixedWithUsualWork1() public void CustomFiltersMixedWithUsualWork1()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "Isnew,CategoryId==2", Filters = "Isnew,CategoryId==2",
}; };
var result = _processor.Apply(model, _posts); var result = _processor.Apply(model, _posts);
Assert.IsTrue(result.Any(p => p.Id == 3)); Assert.True(result.Any(p => p.Id == 3));
Assert.IsTrue(result.Count() == 1); Assert.True(result.Count() == 1);
} }
[TestMethod] [Fact]
public void CustomFiltersMixedWithUsualWork2() public void CustomFiltersMixedWithUsualWork2()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "CategoryId==2,Isnew", Filters = "CategoryId==2,Isnew",
}; };
var result = _processor.Apply(model, _posts); var result = _processor.Apply(model, _posts);
Assert.IsTrue(result.Any(p => p.Id == 3)); Assert.True(result.Any(p => p.Id == 3));
Assert.IsTrue(result.Count() == 1); Assert.True(result.Count() == 1);
} }
[TestMethod] [Fact]
public void CustomFiltersOnDifferentSourcesCanShareName() public void CustomFiltersOnDifferentSourcesCanShareName()
{ {
var postModel = new SieveModel() var postModel = new SieveModel
{ {
Filters = "CategoryId==2,Isnew", Filters = "CategoryId==2,Isnew",
}; };
var postResult = _processor.Apply(postModel, _posts); var postResult = _processor.Apply(postModel, _posts);
Assert.IsTrue(postResult.Any(p => p.Id == 3)); Assert.True(postResult.Any(p => p.Id == 3));
Assert.AreEqual(1, postResult.Count()); Assert.Equal(1, postResult.Count());
var commentModel = new SieveModel() var commentModel = new SieveModel
{ {
Filters = "Isnew", Filters = "Isnew",
}; };
var commentResult = _processor.Apply(commentModel, _comments); var commentResult = _processor.Apply(commentModel, _comments);
Assert.IsTrue(commentResult.Any(c => c.Id == 2)); Assert.True(commentResult.Any(c => c.Id == 2));
Assert.AreEqual(2, commentResult.Count()); Assert.Equal(2, commentResult.Count());
} }
[TestMethod] [Fact]
public void CustomSortsWork() public void CustomSortsWork()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Sorts = "Popularity", Sorts = "Popularity",
}; };
var result = _processor.Apply(model, _posts); var result = _processor.Apply(model, _posts);
Assert.IsFalse(result.First().Id == 0); Assert.False(result.First().Id == 0);
} }
[TestMethod] [Fact]
public void CustomGenericSortsWork() public void CustomGenericSortsWork()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Sorts = "Oldest", Sorts = "Oldest",
}; };
var result = _processor.Apply(model, _posts); var result = _processor.Apply(model, _posts);
Assert.IsTrue(result.Last().Id == 0); Assert.True(result.Last().Id == 0);
} }
[TestMethod] [Fact]
public void MethodNotFoundExceptionWork() public void MethodNotFoundExceptionWork()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "does not exist", Filters = "does not exist",
}; };
Assert.ThrowsException<SieveMethodNotFoundException>(() => _processor.Apply(model, _posts)); Assert.Throws<SieveMethodNotFoundException>(() => _processor.Apply(model, _posts));
} }
[TestMethod] [Fact]
public void IncompatibleMethodExceptionsWork() public void IncompatibleMethodExceptionsWork()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "TestComment", Filters = "TestComment",
}; };
Assert.ThrowsException<SieveIncompatibleMethodException>(() => _processor.Apply(model, _posts)); Assert.Throws<SieveIncompatibleMethodException>(() => _processor.Apply(model, _posts));
} }
[TestMethod] [Fact]
public void OrNameFilteringWorks() public void OrNameFilteringWorks()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "(Title|LikeCount)==3", Filters = "(Title|LikeCount)==3",
}; };
@ -348,17 +398,17 @@ namespace SieveUnitTests
var entry = result.FirstOrDefault(); var entry = result.FirstOrDefault();
var resultCount = result.Count(); var resultCount = result.Count();
Assert.IsNotNull(entry); Assert.NotNull(entry);
Assert.AreEqual(1, resultCount); Assert.Equal(1, resultCount);
Assert.AreEqual(3, entry.Id); Assert.Equal(3, entry.Id);
} }
[DataTestMethod] [Theory]
[DataRow("CategoryId==1,(CategoryId|LikeCount)==50")] [InlineData("CategoryId==1,(CategoryId|LikeCount)==50")]
[DataRow("(CategoryId|LikeCount)==50,CategoryId==1")] [InlineData("(CategoryId|LikeCount)==50,CategoryId==1")]
public void CombinedAndOrFilterIndependentOfOrder(string filter) public void CombinedAndOrFilterIndependentOfOrder(string filter)
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = filter, Filters = filter,
}; };
@ -367,14 +417,14 @@ namespace SieveUnitTests
var entry = result.FirstOrDefault(); var entry = result.FirstOrDefault();
var resultCount = result.Count(); var resultCount = result.Count();
Assert.IsNotNull(entry); Assert.NotNull(entry);
Assert.AreEqual(1, resultCount); Assert.Equal(1, resultCount);
} }
[TestMethod] [Fact]
public void CombinedAndOrWithSpaceFilteringWorks() public void CombinedAndOrWithSpaceFilteringWorks()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "Title==D, (Title|LikeCount)==3", Filters = "Title==D, (Title|LikeCount)==3",
}; };
@ -383,97 +433,98 @@ namespace SieveUnitTests
var entry = result.FirstOrDefault(); var entry = result.FirstOrDefault();
var resultCount = result.Count(); var resultCount = result.Count();
Assert.IsNotNull(entry); Assert.NotNull(entry);
Assert.AreEqual(1, resultCount); Assert.Equal(1, resultCount);
Assert.AreEqual(3, entry.Id); Assert.Equal(3, entry.Id);
} }
[TestMethod] [Fact]
public void OrValueFilteringWorks() public void OrValueFilteringWorks()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "Title==C|D", Filters = "Title==C|D",
}; };
var result = _processor.Apply(model, _posts); var result = _processor.Apply(model, _posts);
Assert.AreEqual(2, result.Count()); Assert.Equal(2, result.Count());
Assert.IsTrue(result.Any(p => p.Id == 2)); Assert.True(result.Any(p => p.Id == 2));
Assert.IsTrue(result.Any(p => p.Id == 3)); Assert.True(result.Any(p => p.Id == 3));
} }
[TestMethod] [Fact]
public void OrValueFilteringWorks2() public void OrValueFilteringWorks2()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "Text@=(|)", Filters = "Text@=(|)",
}; };
var result = _processor.Apply(model, _comments); var result = _processor.Apply(model, _comments);
Assert.AreEqual(1, result.Count()); Assert.Equal(1, result.Count());
Assert.AreEqual(2, result.FirstOrDefault().Id); Assert.Equal(2, result.FirstOrDefault()?.Id);
} }
[TestMethod] [Fact]
public void NestedFilteringWorks() public void NestedFilteringWorks()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "TopComment.Text!@=A", Filters = "TopComment.Text!@=A",
}; };
var result = _processor.Apply(model, _posts); var result = _processor.Apply(model, _posts);
Assert.AreEqual(3, result.Count()); Assert.Equal(3, result.Count());
var posts = result.ToList(); var posts = result.ToList();
Assert.IsTrue(posts[0].TopComment.Text.Contains("B")); Assert.Contains("B", posts[0].TopComment.Text);
Assert.IsTrue(posts[1].TopComment.Text.Contains("C")); Assert.Contains("C", posts[1].TopComment.Text);
Assert.IsTrue(posts[2].TopComment.Text.Contains("D")); Assert.Contains("D", posts[2].TopComment.Text);
} }
[TestMethod] [Fact]
public void NestedSortingWorks() public void NestedSortingWorks()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Sorts = "TopComment.Id", Sorts = "TopComment.Id",
}; };
var result = _processor.Apply(model, _posts); var result = _processor.Apply(model, _posts);
Assert.AreEqual(4, result.Count()); Assert.Equal(4, result.Count());
var posts = result.ToList(); var posts = result.ToList();
Assert.AreEqual(posts[0].Id, 0); Assert.Equal(0, posts[0].Id);
Assert.AreEqual(posts[1].Id, 3); Assert.Equal(3, posts[1].Id);
Assert.AreEqual(posts[2].Id, 2); Assert.Equal(2, posts[2].Id);
Assert.AreEqual(posts[3].Id, 1); Assert.Equal(1, posts[3].Id);
} }
[TestMethod] [Fact]
public void NestedFilteringWithIdenticTypesWorks() public void NestedFilteringWithIdenticTypesWorks()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "(topc|featc)@=*2", Filters = "(topc|featc)@=*2",
}; };
var result = _processor.Apply(model, _posts); 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", Filters = "(topc|featc)@=*B",
}; };
result = _processor.Apply(model, _posts); result = _processor.Apply(model, _posts);
Assert.AreEqual(1, result.Count()); Assert.Equal(1, result.Count());
} }
[TestMethod] [Fact]
public void FilteringNullsWorks() public void FilteringNullsWorks()
{ {
var posts = new List<Post> var posts = new List<Post>
{ {
new Post() { new Post
{
Id = 1, Id = 1,
Title = null, Title = null,
LikeCount = 0, LikeCount = 0,
@ -484,21 +535,22 @@ namespace SieveUnitTests
}, },
}.AsQueryable(); }.AsQueryable();
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "FeaturedComment.Text!@=Some value", Filters = "FeaturedComment.Text!@=Some value",
}; };
var result = _processor.Apply(model, posts); var result = _processor.Apply(model, posts);
Assert.AreEqual(0, result.Count()); Assert.Equal(0, result.Count());
} }
[TestMethod] [Fact]
public void SortingNullsWorks() public void SortingNullsWorks()
{ {
var posts = new List<Post> var posts = new List<Post>
{ {
new Post() { new Post
{
Id = 1, Id = 1,
Title = null, Title = null,
LikeCount = 0, LikeCount = 0,
@ -507,7 +559,8 @@ namespace SieveUnitTests
TopComment = new Comment { Id = 1 }, TopComment = new Comment { Id = 1 },
FeaturedComment = null FeaturedComment = null
}, },
new Post() { new Post
{
Id = 2, Id = 2,
Title = null, Title = null,
LikeCount = 0, LikeCount = 0,
@ -518,24 +571,25 @@ namespace SieveUnitTests
}, },
}.AsQueryable(); }.AsQueryable();
var model = new SieveModel() var model = new SieveModel
{ {
Sorts = "TopComment.Id", Sorts = "TopComment.Id",
}; };
var result = _processor.Apply(model, posts); var result = _processor.Apply(model, posts);
Assert.AreEqual(2, result.Count()); Assert.Equal(2, result.Count());
var sortedPosts = result.ToList(); var sortedPosts = result.ToList();
Assert.AreEqual(sortedPosts[0].Id, 2); Assert.Equal(2, sortedPosts[0].Id);
Assert.AreEqual(sortedPosts[1].Id, 1); Assert.Equal(1, sortedPosts[1].Id);
} }
[TestMethod] [Fact]
public void FilteringOnNullWorks() public void FilteringOnNullWorks()
{ {
var posts = new List<Post> var posts = new List<Post>
{ {
new Post() { new Post
{
Id = 1, Id = 1,
Title = null, Title = null,
LikeCount = 0, LikeCount = 0,
@ -544,7 +598,8 @@ namespace SieveUnitTests
TopComment = null, TopComment = null,
FeaturedComment = null FeaturedComment = null
}, },
new Post() { new Post
{
Id = 2, Id = 2,
Title = null, Title = null,
LikeCount = 0, LikeCount = 0,
@ -555,33 +610,190 @@ namespace SieveUnitTests
}, },
}.AsQueryable(); }.AsQueryable();
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "FeaturedComment.Text==null", Filters = "FeaturedComment.Text==null",
}; };
var result = _processor.Apply(model, posts); var result = _processor.Apply(model, posts);
Assert.AreEqual(1, result.Count()); Assert.Equal(1, result.Count());
var filteredPosts = result.ToList(); var filteredPosts = result.ToList();
Assert.AreEqual(filteredPosts[0].Id, 2); Assert.Equal(2, filteredPosts[0].Id);
} }
[TestMethod] [Fact]
public void BaseDefinedPropertyMappingSortingWorks_WithCustomName() public void BaseDefinedPropertyMappingSortingWorks_WithCustomName()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Sorts = "-CreateDate" Sorts = "-CreateDate"
}; };
var result = _processor.Apply(model, _posts); var result = _processor.Apply(model, _posts);
Assert.AreEqual(4, result.Count()); Assert.Equal(4, result.Count());
var posts = result.ToList(); var posts = result.ToList();
Assert.AreEqual(posts[0].Id, 3); Assert.Equal(3,posts[0].Id);
Assert.AreEqual(posts[1].Id, 2); Assert.Equal(2,posts[1].Id);
Assert.AreEqual(posts[2].Id, 1); Assert.Equal(1,posts[2].Id);
Assert.AreEqual(posts[3].Id, 0); 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);
}
} }
} }

View File

@ -1,33 +1,43 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Sieve.Exceptions; using Sieve.Exceptions;
using Sieve.Models; using Sieve.Models;
using Sieve.Services; using Sieve.Services;
using SieveUnitTests.Abstractions.Entity; using SieveUnitTests.Abstractions.Entity;
using SieveUnitTests.Entities; using SieveUnitTests.Entities;
using SieveUnitTests.Services; using SieveUnitTests.Services;
using Xunit;
using Xunit.Abstractions;
namespace SieveUnitTests namespace SieveUnitTests
{ {
[TestClass]
public class GeneralWithInterfaces public class GeneralWithInterfaces
{ {
private readonly ITestOutputHelper _testOutputHelper;
private readonly SieveProcessor _processor; private readonly SieveProcessor _processor;
private readonly SieveProcessor _nullableProcessor;
private readonly IQueryable<IPost> _posts; private readonly IQueryable<IPost> _posts;
private readonly IQueryable<Comment> _comments; 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(), _processor = new ApplicationSieveProcessor(new SieveOptionsAccessor(),
new SieveCustomSortMethods(), new SieveCustomSortMethods(),
new SieveCustomFilterMethods()); new SieveCustomFilterMethods());
_nullableProcessor = new ApplicationSieveProcessor(nullableAccessor,
new SieveCustomSortMethods(),
new SieveCustomFilterMethods());
_posts = new List<IPost> _posts = new List<IPost>
{ {
new Post() { new Post
{
Id = 0, Id = 0,
Title = "A", Title = "A",
LikeCount = 100, LikeCount = 100,
@ -36,7 +46,8 @@ namespace SieveUnitTests
TopComment = new Comment { Id = 0, Text = "A1" }, TopComment = new Comment { Id = 0, Text = "A1" },
FeaturedComment = new Comment { Id = 4, Text = "A2" } FeaturedComment = new Comment { Id = 4, Text = "A2" }
}, },
new Post() { new Post
{
Id = 1, Id = 1,
Title = "B", Title = "B",
LikeCount = 50, LikeCount = 50,
@ -45,7 +56,8 @@ namespace SieveUnitTests
TopComment = new Comment { Id = 3, Text = "B1" }, TopComment = new Comment { Id = 3, Text = "B1" },
FeaturedComment = new Comment { Id = 5, Text = "B2" } FeaturedComment = new Comment { Id = 5, Text = "B2" }
}, },
new Post() { new Post
{
Id = 2, Id = 2,
Title = "C", Title = "C",
LikeCount = 0, LikeCount = 0,
@ -53,7 +65,8 @@ namespace SieveUnitTests
TopComment = new Comment { Id = 2, Text = "C1" }, TopComment = new Comment { Id = 2, Text = "C1" },
FeaturedComment = new Comment { Id = 6, Text = "C2" } FeaturedComment = new Comment { Id = 6, Text = "C2" }
}, },
new Post() { new Post
{
Id = 3, Id = 3,
Title = "D", Title = "D",
LikeCount = 3, LikeCount = 3,
@ -66,17 +79,20 @@ namespace SieveUnitTests
_comments = new List<Comment> _comments = new List<Comment>
{ {
new Comment() { new Comment
{
Id = 0, Id = 0,
DateCreated = DateTimeOffset.UtcNow.AddDays(-20), DateCreated = DateTimeOffset.UtcNow.AddDays(-20),
Text = "This is an old comment." Text = "This is an old comment."
}, },
new Comment() { new Comment
{
Id = 1, Id = 1,
DateCreated = DateTimeOffset.UtcNow.AddDays(-1), DateCreated = DateTimeOffset.UtcNow.AddDays(-1),
Text = "This is a fairly new comment." Text = "This is a fairly new comment."
}, },
new Comment() { new Comment
{
Id = 2, Id = 2,
DateCreated = DateTimeOffset.UtcNow, DateCreated = DateTimeOffset.UtcNow,
Text = "This is a brand new comment. (Text in braces)" Text = "This is a brand new comment. (Text in braces)"
@ -84,263 +100,280 @@ namespace SieveUnitTests
}.AsQueryable(); }.AsQueryable();
} }
[TestMethod] [Fact]
public void ContainsCanBeCaseInsensitive() public void ContainsCanBeCaseInsensitive()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "Title@=*a" Filters = "Title@=*a"
}; };
var result = _processor.Apply(model, _posts); var result = _processor.Apply(model, _posts);
Assert.AreEqual(result.First().Id, 0); Assert.Equal(0, result.First().Id);
Assert.IsTrue(result.Count() == 1); Assert.True(result.Count() == 1);
} }
[TestMethod] [Fact]
public void NotEqualsCanBeCaseInsensitive() public void NotEqualsCanBeCaseInsensitive()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "Title!=*a" Filters = "Title!=*a"
}; };
var result = _processor.Apply(model, _posts); var result = _processor.Apply(model, _posts);
Assert.AreEqual(result.First().Id, 1); Assert.Equal(1, result.First().Id);
Assert.IsTrue(result.Count() == 3); Assert.True(result.Count() == 3);
} }
[TestMethod] [Fact]
public void ContainsIsCaseSensitive() public void ContainsIsCaseSensitive()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "Title@=a", Filters = "Title@=a",
}; };
var result = _processor.Apply(model, _posts); var result = _processor.Apply(model, _posts);
Assert.IsTrue(result.Count() == 0); Assert.True(!result.Any());
} }
[TestMethod] [Fact]
public void NotContainsWorks() public void NotContainsWorks()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "Title!@=D", Filters = "Title!@=D",
}; };
var result = _processor.Apply(model, _posts); var result = _processor.Apply(model, _posts);
Assert.IsTrue(result.Count() == 3); Assert.True(result.Count() == 3);
} }
[TestMethod] [Fact]
public void CanFilterBools() public void CanFilterBools()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "IsDraft==false" Filters = "IsDraft==false"
}; };
var result = _processor.Apply(model, _posts); var result = _processor.Apply(model, _posts);
Assert.IsTrue(result.Count() == 2); Assert.True(result.Count() == 2);
} }
[TestMethod] [Fact]
public void CanSortBools() public void CanSortBools()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Sorts = "-IsDraft" Sorts = "-IsDraft"
}; };
var result = _processor.Apply(model, _posts); var result = _processor.Apply(model, _posts);
Assert.AreEqual(result.First().Id, 0); Assert.Equal(0, result.First().Id);
} }
[TestMethod] [Fact]
public void CanFilterNullableInts() public void CanFilterNullableInts()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "CategoryId==1" Filters = "CategoryId==1"
}; };
var result = _processor.Apply(model, _posts); 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] [Fact]
public void EqualsDoesntFailWithNonStringTypes() public void CanFilterNullableIntsWithNotEqual()
{ {
var model = new SieveModel() 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", Filters = "LikeCount==50",
}; };
Console.WriteLine(model.GetFiltersParsed()[0].Values); _testOutputHelper.WriteLine(model.GetFiltersParsed()[0].Values.ToString());
Console.WriteLine(model.GetFiltersParsed()[0].Operator); _testOutputHelper.WriteLine(model.GetFiltersParsed()[0].Operator);
Console.WriteLine(model.GetFiltersParsed()[0].OperatorParsed); _testOutputHelper.WriteLine(model.GetFiltersParsed()[0].OperatorParsed.ToString());
var result = _processor.Apply(model, _posts); var result = _processor.Apply(model, _posts);
Assert.AreEqual(result.First().Id, 1); Assert.Equal(1, result.First().Id);
Assert.IsTrue(result.Count() == 1); Assert.True(result.Count() == 1);
} }
[TestMethod] [Fact]
public void CustomFiltersWork() public void CustomFiltersWork()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "Isnew", Filters = "Isnew",
}; };
var result = _processor.Apply(model, _posts); var result = _processor.Apply(model, _posts);
Assert.IsFalse(result.Any(p => p.Id == 0)); Assert.False(result.Any(p => p.Id == 0));
Assert.IsTrue(result.Count() == 3); Assert.True(result.Count() == 3);
} }
[TestMethod] [Fact]
public void CustomGenericFiltersWork() public void CustomGenericFiltersWork()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "Latest", Filters = "Latest",
}; };
var result = _processor.Apply(model, _comments); var result = _processor.Apply(model, _comments);
Assert.IsFalse(result.Any(p => p.Id == 0)); Assert.False(result.Any(p => p.Id == 0));
Assert.IsTrue(result.Count() == 2); Assert.True(result.Count() == 2);
} }
[TestMethod] [Fact]
public void CustomFiltersWithOperatorsWork() public void CustomFiltersWithOperatorsWork()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "HasInTitle==A", Filters = "HasInTitle==A",
}; };
var result = _processor.Apply(model, _posts); var result = _processor.Apply(model, _posts);
Assert.IsTrue(result.Any(p => p.Id == 0)); Assert.True(result.Any(p => p.Id == 0));
Assert.IsTrue(result.Count() == 1); Assert.True(result.Count() == 1);
} }
[TestMethod] [Fact]
public void CustomFiltersMixedWithUsualWork1() public void CustomFiltersMixedWithUsualWork1()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "Isnew,CategoryId==2", Filters = "Isnew,CategoryId==2",
}; };
var result = _processor.Apply(model, _posts); var result = _processor.Apply(model, _posts);
Assert.IsTrue(result.Any(p => p.Id == 3)); Assert.True(result.Any(p => p.Id == 3));
Assert.IsTrue(result.Count() == 1); Assert.True(result.Count() == 1);
} }
[TestMethod] [Fact]
public void CustomFiltersMixedWithUsualWork2() public void CustomFiltersMixedWithUsualWork2()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "CategoryId==2,Isnew", Filters = "CategoryId==2,Isnew",
}; };
var result = _processor.Apply(model, _posts); var result = _processor.Apply(model, _posts);
Assert.IsTrue(result.Any(p => p.Id == 3)); Assert.True(result.Any(p => p.Id == 3));
Assert.IsTrue(result.Count() == 1); Assert.True(result.Count() == 1);
} }
[TestMethod] [Fact]
public void CustomFiltersOnDifferentSourcesCanShareName() public void CustomFiltersOnDifferentSourcesCanShareName()
{ {
var postModel = new SieveModel() var postModel = new SieveModel
{ {
Filters = "CategoryId==2,Isnew", Filters = "CategoryId==2,Isnew",
}; };
var postResult = _processor.Apply(postModel, _posts); var postResult = _processor.Apply(postModel, _posts);
Assert.IsTrue(postResult.Any(p => p.Id == 3)); Assert.True(postResult.Any(p => p.Id == 3));
Assert.AreEqual(1, postResult.Count()); Assert.Equal(1, postResult.Count());
var commentModel = new SieveModel() var commentModel = new SieveModel
{ {
Filters = "Isnew", Filters = "Isnew",
}; };
var commentResult = _processor.Apply(commentModel, _comments); var commentResult = _processor.Apply(commentModel, _comments);
Assert.IsTrue(commentResult.Any(c => c.Id == 2)); Assert.True(commentResult.Any(c => c.Id == 2));
Assert.AreEqual(2, commentResult.Count()); Assert.Equal(2, commentResult.Count());
} }
[TestMethod] [Fact]
public void CustomSortsWork() public void CustomSortsWork()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Sorts = "Popularity", Sorts = "Popularity",
}; };
var result = _processor.Apply(model, _posts); var result = _processor.Apply(model, _posts);
Assert.IsFalse(result.First().Id == 0); Assert.False(result.First().Id == 0);
} }
[TestMethod] [Fact]
public void CustomGenericSortsWork() public void CustomGenericSortsWork()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Sorts = "Oldest", Sorts = "Oldest",
}; };
var result = _processor.Apply(model, _posts); var result = _processor.Apply(model, _posts);
Assert.IsTrue(result.Last().Id == 0); Assert.True(result.Last().Id == 0);
} }
[TestMethod] [Fact]
public void MethodNotFoundExceptionWork() public void MethodNotFoundExceptionWork()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "does not exist", Filters = "does not exist",
}; };
Assert.ThrowsException<SieveMethodNotFoundException>(() => _processor.Apply(model, _posts)); Assert.Throws<SieveMethodNotFoundException>(() => _processor.Apply(model, _posts));
} }
[TestMethod] [Fact]
public void IncompatibleMethodExceptionsWork() public void IncompatibleMethodExceptionsWork()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "TestComment", Filters = "TestComment",
}; };
Assert.ThrowsException<SieveIncompatibleMethodException>(() => _processor.Apply(model, _posts)); Assert.Throws<SieveIncompatibleMethodException>(() => _processor.Apply(model, _posts));
} }
[TestMethod] [Fact]
public void OrNameFilteringWorks() public void OrNameFilteringWorks()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "(Title|LikeCount)==3", Filters = "(Title|LikeCount)==3",
}; };
@ -349,17 +382,17 @@ namespace SieveUnitTests
var entry = result.FirstOrDefault(); var entry = result.FirstOrDefault();
var resultCount = result.Count(); var resultCount = result.Count();
Assert.IsNotNull(entry); Assert.NotNull(entry);
Assert.AreEqual(1, resultCount); Assert.Equal(1, resultCount);
Assert.AreEqual(3, entry.Id); Assert.Equal(3, entry.Id);
} }
[DataTestMethod] [Theory]
[DataRow("CategoryId==1,(CategoryId|LikeCount)==50")] [InlineData("CategoryId==1,(CategoryId|LikeCount)==50")]
[DataRow("(CategoryId|LikeCount)==50,CategoryId==1")] [InlineData("(CategoryId|LikeCount)==50,CategoryId==1")]
public void CombinedAndOrFilterIndependentOfOrder(string filter) public void CombinedAndOrFilterIndependentOfOrder(string filter)
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = filter, Filters = filter,
}; };
@ -368,14 +401,14 @@ namespace SieveUnitTests
var entry = result.FirstOrDefault(); var entry = result.FirstOrDefault();
var resultCount = result.Count(); var resultCount = result.Count();
Assert.IsNotNull(entry); Assert.NotNull(entry);
Assert.AreEqual(1, resultCount); Assert.Equal(1, resultCount);
} }
[TestMethod] [Fact]
public void CombinedAndOrWithSpaceFilteringWorks() public void CombinedAndOrWithSpaceFilteringWorks()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "Title==D, (Title|LikeCount)==3", Filters = "Title==D, (Title|LikeCount)==3",
}; };
@ -384,97 +417,98 @@ namespace SieveUnitTests
var entry = result.FirstOrDefault(); var entry = result.FirstOrDefault();
var resultCount = result.Count(); var resultCount = result.Count();
Assert.IsNotNull(entry); Assert.NotNull(entry);
Assert.AreEqual(1, resultCount); Assert.Equal(1, resultCount);
Assert.AreEqual(3, entry.Id); Assert.Equal(3, entry.Id);
} }
[TestMethod] [Fact]
public void OrValueFilteringWorks() public void OrValueFilteringWorks()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "Title==C|D", Filters = "Title==C|D",
}; };
var result = _processor.Apply(model, _posts); var result = _processor.Apply(model, _posts);
Assert.AreEqual(2, result.Count()); Assert.Equal(2, result.Count());
Assert.IsTrue(result.Any(p => p.Id == 2)); Assert.True(result.Any(p => p.Id == 2));
Assert.IsTrue(result.Any(p => p.Id == 3)); Assert.True(result.Any(p => p.Id == 3));
} }
[TestMethod] [Fact]
public void OrValueFilteringWorks2() public void OrValueFilteringWorks2()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "Text@=(|)", Filters = "Text@=(|)",
}; };
var result = _processor.Apply(model, _comments); var result = _processor.Apply(model, _comments);
Assert.AreEqual(1, result.Count()); Assert.Equal(1, result.Count());
Assert.AreEqual(2, result.FirstOrDefault().Id); Assert.Equal(2, result.FirstOrDefault()?.Id);
} }
[TestMethod] [Fact]
public void NestedFilteringWorks() public void NestedFilteringWorks()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "TopComment.Text!@=A", Filters = "TopComment.Text!@=A",
}; };
var result = _processor.Apply(model, _posts); var result = _processor.Apply(model, _posts);
Assert.AreEqual(3, result.Count()); Assert.Equal(3, result.Count());
var posts = result.ToList(); var posts = result.ToList();
Assert.IsTrue(posts[0].TopComment.Text.Contains("B")); Assert.Contains("B", posts[0].TopComment.Text);
Assert.IsTrue(posts[1].TopComment.Text.Contains("C")); Assert.Contains("C", posts[1].TopComment.Text);
Assert.IsTrue(posts[2].TopComment.Text.Contains("D")); Assert.Contains("D", posts[2].TopComment.Text);
} }
[TestMethod] [Fact]
public void NestedSortingWorks() public void NestedSortingWorks()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Sorts = "TopComment.Id", Sorts = "TopComment.Id",
}; };
var result = _processor.Apply(model, _posts); var result = _processor.Apply(model, _posts);
Assert.AreEqual(4, result.Count()); Assert.Equal(4, result.Count());
var posts = result.ToList(); var posts = result.ToList();
Assert.AreEqual(posts[0].Id, 0); Assert.Equal(0, posts[0].Id);
Assert.AreEqual(posts[1].Id, 3); Assert.Equal(3, posts[1].Id);
Assert.AreEqual(posts[2].Id, 2); Assert.Equal(2, posts[2].Id);
Assert.AreEqual(posts[3].Id, 1); Assert.Equal(1, posts[3].Id);
} }
[TestMethod] [Fact]
public void NestedFilteringWithIdenticTypesWorks() public void NestedFilteringWithIdenticTypesWorks()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "(topc|featc)@=*2", Filters = "(topc|featc)@=*2",
}; };
var result = _processor.Apply(model, _posts); 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", Filters = "(topc|featc)@=*B",
}; };
result = _processor.Apply(model, _posts); result = _processor.Apply(model, _posts);
Assert.AreEqual(1, result.Count()); Assert.Equal(1, result.Count());
} }
[TestMethod] [Fact]
public void FilteringNullsWorks() public void FilteringNullsWorks()
{ {
var posts = new List<Post> var posts = new List<Post>
{ {
new Post() { new Post
{
Id = 1, Id = 1,
Title = null, Title = null,
LikeCount = 0, LikeCount = 0,
@ -485,21 +519,22 @@ namespace SieveUnitTests
}, },
}.AsQueryable(); }.AsQueryable();
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "FeaturedComment.Text!@=Some value", Filters = "FeaturedComment.Text!@=Some value",
}; };
var result = _processor.Apply(model, posts); var result = _processor.Apply(model, posts);
Assert.AreEqual(0, result.Count()); Assert.Equal(0, result.Count());
} }
[TestMethod] [Fact]
public void SortingNullsWorks() public void SortingNullsWorks()
{ {
var posts = new List<Post> var posts = new List<Post>
{ {
new Post() { new Post
{
Id = 1, Id = 1,
Title = null, Title = null,
LikeCount = 0, LikeCount = 0,
@ -508,7 +543,8 @@ namespace SieveUnitTests
TopComment = new Comment { Id = 1 }, TopComment = new Comment { Id = 1 },
FeaturedComment = null FeaturedComment = null
}, },
new Post() { new Post
{
Id = 2, Id = 2,
Title = null, Title = null,
LikeCount = 0, LikeCount = 0,
@ -519,24 +555,25 @@ namespace SieveUnitTests
}, },
}.AsQueryable(); }.AsQueryable();
var model = new SieveModel() var model = new SieveModel
{ {
Sorts = "TopComment.Id", Sorts = "TopComment.Id",
}; };
var result = _processor.Apply(model, posts); var result = _processor.Apply(model, posts);
Assert.AreEqual(2, result.Count()); Assert.Equal(2, result.Count());
var sortedPosts = result.ToList(); var sortedPosts = result.ToList();
Assert.AreEqual(sortedPosts[0].Id, 2); Assert.Equal(2, sortedPosts[0].Id);
Assert.AreEqual(sortedPosts[1].Id, 1); Assert.Equal(1, sortedPosts[1].Id);
} }
[TestMethod] [Fact]
public void FilteringOnNullWorks() public void FilteringOnNullWorks()
{ {
var posts = new List<Post> var posts = new List<Post>
{ {
new Post() { new Post
{
Id = 1, Id = 1,
Title = null, Title = null,
LikeCount = 0, LikeCount = 0,
@ -545,7 +582,8 @@ namespace SieveUnitTests
TopComment = null, TopComment = null,
FeaturedComment = null FeaturedComment = null
}, },
new Post() { new Post
{
Id = 2, Id = 2,
Title = null, Title = null,
LikeCount = 0, LikeCount = 0,
@ -556,33 +594,33 @@ namespace SieveUnitTests
}, },
}.AsQueryable(); }.AsQueryable();
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "FeaturedComment.Text==null", Filters = "FeaturedComment.Text==null",
}; };
var result = _processor.Apply(model, posts); var result = _processor.Apply(model, posts);
Assert.AreEqual(1, result.Count()); Assert.Equal(1, result.Count());
var filteredPosts = result.ToList(); var filteredPosts = result.ToList();
Assert.AreEqual(filteredPosts[0].Id, 2); Assert.Equal(2, filteredPosts[0].Id);
} }
[TestMethod] [Fact]
public void BaseDefinedPropertyMappingSortingWorks_WithCustomName() public void BaseDefinedPropertyMappingSortingWorks_WithCustomName()
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Sorts = "-CreateDate" Sorts = "-CreateDate"
}; };
var result = _processor.Apply(model, _posts); var result = _processor.Apply(model, _posts);
Assert.AreEqual(4, result.Count()); Assert.Equal(4, result.Count());
var posts = result.ToList(); var posts = result.ToList();
Assert.AreEqual(posts[0].Id, 3); Assert.Equal(3,posts[0].Id);
Assert.AreEqual(posts[1].Id, 2); Assert.Equal(2,posts[1].Id);
Assert.AreEqual(posts[2].Id, 1); Assert.Equal(1,posts[2].Id);
Assert.AreEqual(posts[3].Id, 0); Assert.Equal(0,posts[3].Id);
} }
} }
} }

View File

@ -1,40 +1,38 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Sieve.Exceptions; using Sieve.Exceptions;
using Sieve.Models; using Sieve.Models;
using Sieve.Services;
using SieveUnitTests.Entities; using SieveUnitTests.Entities;
using SieveUnitTests.Services; using SieveUnitTests.Services;
using Xunit;
namespace SieveUnitTests namespace SieveUnitTests
{ {
[TestClass]
public class Mapper public class Mapper
{ {
private readonly ApplicationSieveProcessor _processor;
private readonly IQueryable<Post> _posts; private readonly IQueryable<Post> _posts;
public Mapper() public Mapper()
{ {
_processor = new ApplicationSieveProcessor(new SieveOptionsAccessor(),
new SieveCustomSortMethods(),
new SieveCustomFilterMethods());
_posts = new List<Post> _posts = new List<Post>
{ {
new Post() { new Post
{
Id = 1, Id = 1,
ThisHasNoAttributeButIsAccessible = "A", ThisHasNoAttributeButIsAccessible = "A",
ThisHasNoAttribute = "A", ThisHasNoAttribute = "A",
OnlySortableViaFluentApi = 100 OnlySortableViaFluentApi = 100
}, },
new Post() { new Post
{
Id = 2, Id = 2,
ThisHasNoAttributeButIsAccessible = "B", ThisHasNoAttributeButIsAccessible = "B",
ThisHasNoAttribute = "B", ThisHasNoAttribute = "B",
OnlySortableViaFluentApi = 50 OnlySortableViaFluentApi = 50
}, },
new Post() { new Post
{
Id = 3, Id = 3,
ThisHasNoAttributeButIsAccessible = "C", ThisHasNoAttributeButIsAccessible = "C",
ThisHasNoAttribute = "C", ThisHasNoAttribute = "C",
@ -43,43 +41,63 @@ namespace SieveUnitTests
}.AsQueryable(); }.AsQueryable();
} }
[TestMethod] /// <summary>
public void MapperWorks() /// 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", 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] [Theory]
public void MapperSortOnlyWorks() [MemberData(nameof(GetProcessors))]
public void MapperSortOnlyWorks(ISieveProcessor processor)
{ {
var model = new SieveModel() var model = new SieveModel
{ {
Filters = "OnlySortableViaFluentApi@=50", Filters = "OnlySortableViaFluentApi@=50",
Sorts = "OnlySortableViaFluentApi" 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
//

View File

@ -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>();
}
}
}

View File

@ -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);
}
}

View File

@ -1,15 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="MSTest.TestAdapter" Version="1.2.0" /> <PackageReference Include="Microsoft.Extensions.Options" Version="2.0.0" />
<PackageReference Include="MSTest.TestFramework" Version="1.2.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>
<ItemGroup> <ItemGroup>

View 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
View 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
View 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
View 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
View 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
View 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
View 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;
}
}

View 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>

View 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
View 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>

View 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">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</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>